fix(guohetec): parse status frames by advertised length

Status replies include the CRC within the one-byte payload length.
Reading a fixed 40-byte reply and indexing nonexistent byte 32 caused
timeouts and rejected valid 32-byte firmware frames.

Centralize status framing and CRC validation for PMR-171 and Q900.
Reuse it during probing and add socketpair tests driven by captured
wire data.
This commit is contained in:
David Christle 2026-07-31 23:04:21 -07:00
parent 5bc6deefe1
commit ebeb4bb767
No known key found for this signature in database
6 changed files with 648 additions and 396 deletions

View file

@ -19,7 +19,6 @@
*
*/
#include <string.h>
#include "hamlib/rig.h"
#include "hamlib/port.h"
#include "iofunc.h"
@ -27,24 +26,23 @@
#include "riglist.h"
#include "guohetec.h"
// Common response validation function implementations
int validate_packet_header(const unsigned char *reply, const char *func_name)
enum
{
GUOHE_FRAME_HEADER_LENGTH = 5,
GUOHE_STATUS_MIN_PACKET_LENGTH = 15,
GUOHE_STATUS_COMMAND = 0x0B
};
static int validate_packet_header(const unsigned char *reply,
const char *func_name)
{
if (reply[0] != 0xA5 || reply[1] != 0xA5 ||
reply[2] != 0xA5 || reply[3] != 0xA5) {
rig_debug(RIG_DEBUG_ERR, "%s: Invalid packet header, using cached values\n", func_name);
return -1;
return -RIG_EPROTO;
}
return 0;
}
int validate_data_length(const unsigned char *reply, int reply_size, const char *func_name)
{
if (reply[4] == 0 || reply[4] > reply_size - 5) {
rig_debug(RIG_DEBUG_ERR, "%s: Invalid data length %d, using cached values\n", func_name, reply[4]);
return -1;
}
return 0;
return RIG_OK;
}
// CRC16/CCITT-FALSE
@ -157,154 +155,156 @@ uint16_t CRC16Check(const unsigned char *buf, int len)
return result;
}
// Common response validation functions
/**
* Read rig response with validation
* @param rig RIG structure
* @param reply Reply buffer
* @param reply_size Size of reply buffer
* @param func_name Function name for debug messages
* @return 0 on success, -1 on error
*/
int read_rig_response(RIG *rig, unsigned char *reply, int reply_size,
const char *func_name)
int guohetec_read_response(RIG *rig, unsigned char *reply, size_t reply_size,
const char *func_name)
{
hamlib_port_t *rp = RIGPORT(rig);
int ret;
// Read header
ret = read_block(rp, reply, 5);
if (ret < 0) {
if (reply_size < GUOHE_FRAME_HEADER_LENGTH)
{
return -RIG_EINVAL;
}
ret = read_block(rp, reply, GUOHE_FRAME_HEADER_LENGTH);
if (ret < 0)
{
rig_debug(RIG_DEBUG_ERR, "%s: Failed to read header, using cached values\n", func_name);
return -1;
return ret;
}
// Validate data length
if (reply[4] == 0 || reply[4] > reply_size - 5) {
if (validate_packet_header(reply, func_name) < 0)
{
return -RIG_EPROTO;
}
if (reply[4] == 0 || reply[4] > reply_size - GUOHE_FRAME_HEADER_LENGTH)
{
rig_debug(RIG_DEBUG_ERR, "%s: Invalid data length %d, using cached values\n", func_name, reply[4]);
return -1;
return -RIG_EPROTO;
}
// Read data section
ret = read_block(rp, &reply[5], reply[4]);
if (ret < 0) {
ret = read_block(rp, &reply[GUOHE_FRAME_HEADER_LENGTH], reply[4]);
if (ret < 0)
{
rig_debug(RIG_DEBUG_ERR, "%s: Failed to read data, using cached values\n", func_name);
return -1;
return ret;
}
// Validate response length matches expected
if (ret != reply[4]) {
rig_debug(RIG_DEBUG_ERR, "%s: Data read mismatch: expected %d, got %d, using cached values\n",
func_name, reply[4], ret);
return -1;
if (ret != reply[4])
{
rig_debug(RIG_DEBUG_ERR,
"%s: Data read mismatch: expected %d, got %d, using cached values\n",
func_name, reply[4], ret);
return -RIG_EPROTO;
}
return 0;
return GUOHE_FRAME_HEADER_LENGTH + ret;
}
/**
* Validate basic rig response
* @param rig RIG structure
* @param reply Reply buffer
* @param reply_size Size of reply buffer
* @param func_name Function name for debug messages
* @return 0 on success, -1 on error
*/
int validate_rig_response(RIG *rig, const unsigned char *reply, int reply_size,
const char *func_name)
int guohetec_decode_status(const unsigned char *reply, size_t reply_size,
struct guohetec_status *status,
const char *func_name)
{
// Validate packet header
if (validate_packet_header(reply, func_name) < 0) {
return -1;
size_t expected_size;
size_t crc_offset;
uint16_t received_crc;
uint16_t calculated_crc;
if (reply == NULL || status == NULL || func_name == NULL)
{
return -RIG_EINVAL;
}
// Validate data length
if (validate_data_length(reply, reply_size, func_name) < 0) {
return -1;
if (reply_size < GUOHE_FRAME_HEADER_LENGTH ||
validate_packet_header(reply, func_name) < 0)
{
return -RIG_EPROTO;
}
return 0;
if (reply[4] < GUOHE_STATUS_MIN_PACKET_LENGTH)
{
rig_debug(RIG_DEBUG_ERR,
"%s: Status response too short: %u, using cached values\n",
func_name, reply[4]);
return -RIG_EPROTO;
}
expected_size = GUOHE_FRAME_HEADER_LENGTH + reply[4];
if (reply_size != expected_size)
{
rig_debug(RIG_DEBUG_ERR,
"%s: Status response length mismatch: expected %zu, got %zu, using cached values\n",
func_name, expected_size, reply_size);
return -RIG_EPROTO;
}
if (reply[5] != GUOHE_STATUS_COMMAND)
{
rig_debug(RIG_DEBUG_ERR,
"%s: Unexpected status response command: 0x%02X, using cached values\n",
func_name, reply[5]);
return -RIG_EPROTO;
}
crc_offset = expected_size - 2;
received_crc = ((uint16_t)reply[crc_offset] << 8) |
reply[crc_offset + 1];
calculated_crc = CRC16Check(&reply[4], reply[4] - 1);
if (received_crc != calculated_crc)
{
rig_debug(RIG_DEBUG_ERR,
"%s: CRC check failed (received: %04X, calculated: %04X), using cached values\n",
func_name, received_crc, calculated_crc);
return -RIG_EPROTO;
}
status->ptt = reply[6];
status->mode_a = reply[7];
status->mode_b = reply[8];
status->freq_a = (uint32_t)from_be(&reply[9], 4);
status->freq_b = (uint32_t)from_be(&reply[13], 4);
status->vfo = reply[17] == 1 ? RIG_VFO_B : RIG_VFO_A;
return RIG_OK;
}
/**
* Validate frequency response with CRC check
* @param rig RIG structure
* @param reply Reply buffer
* @param reply_size Size of reply buffer
* @param func_name Function name for debug messages
* @return 0 on success, -1 on error
*/
int validate_freq_response(RIG *rig, const unsigned char *reply, int reply_size,
const char *func_name)
int guohetec_get_status(RIG *rig, struct guohetec_status *status,
const char *func_name)
{
// Basic validation
if (validate_rig_response(rig, reply, reply_size, func_name) < 0) {
return -1;
}
// Validate buffer boundaries for CRC
int expected_total_length = 5 + reply[4] + 2; // header(5) + data_length + CRC(2)
if (expected_total_length > reply_size) {
rig_debug(RIG_DEBUG_ERR, "%s: Response too large for buffer: %d > %d, using cached values\n",
func_name, expected_total_length, reply_size);
return -1;
}
// CRC check
uint16_t recv_crc = (reply[31] << 8) | reply[32]; // Last 2 bytes are CRC
uint16_t calc_crc = CRC16Check(&reply[4], 27);
if (recv_crc != calc_crc) {
rig_debug(RIG_DEBUG_ERR, "%s: CRC check failed (received: %04X, calculated: %04X), using cached values\n",
func_name, recv_crc, calc_crc);
return -1;
}
// Validate frequency field offset
int freq_b_offset = 13; // VFOB frequency starting position
if (freq_b_offset + 3 >= expected_total_length - 2) { // -2 for CRC
rig_debug(RIG_DEBUG_ERR, "%s: Frequency field offset out of bounds, using cached values\n", func_name);
return -1;
}
return 0;
}
unsigned char command[GUOHE_STATUS_CMD_LENGTH] = {
0xA5, 0xA5, 0xA5, 0xA5, 0x03, GUOHE_STATUS_COMMAND, 0x00, 0x00
};
unsigned char reply[GUOHE_MAX_FRAME_LENGTH];
hamlib_port_t *rp = RIGPORT(rig);
uint16_t crc;
int ret;
/**
* Validate mode response with bounds checking
* @param rig RIG structure
* @param reply Reply buffer
* @param reply_size Size of reply buffer
* @param func_name Function name for debug messages
* @param min_length Minimum required data length
* @return 0 on success, -1 on error
*/
int validate_mode_response(RIG *rig, const unsigned char *reply, int reply_size,
const char *func_name, int min_length)
{
// Basic validation
if (validate_rig_response(rig, reply, reply_size, func_name) < 0) {
return -1;
crc = CRC16Check(&command[4], 2);
command[6] = crc >> 8;
command[7] = crc & 0xFF;
rig_flush(rp);
ret = write_block(rp, command, sizeof(command));
if (ret != RIG_OK)
{
rig_debug(RIG_DEBUG_ERR, "%s: Failed to write status request\n", func_name);
return ret;
}
// Validate minimum length for mode data
if (reply[4] < min_length) {
rig_debug(RIG_DEBUG_ERR, "%s: Response too short for mode data, using cached values\n", func_name);
return -1;
ret = guohetec_read_response(rig, reply, sizeof(reply), func_name);
if (ret < 0)
{
return ret;
}
// Validate mode field indices are within bounds
if (reply[7] >= GUOHE_MODE_TABLE_MAX) {
rig_debug(RIG_DEBUG_ERR, "%s: Invalid mode A index %d, using cached values\n", func_name, reply[7]);
return -1;
}
if (reply[8] >= GUOHE_MODE_TABLE_MAX) {
rig_debug(RIG_DEBUG_ERR, "%s: Invalid mode B index %d, using cached values\n", func_name, reply[8]);
return -1;
}
return 0;
return guohetec_decode_status(reply, (size_t)ret, status, func_name);
}
// Initialization function
@ -321,7 +321,7 @@ DECLARE_INITRIG_BACKEND(guohetec) {
// Probe function implementation
DECLARE_PROBERIG_BACKEND(guohetec) {
uint8_t cmd[PMR171_CMD_LENGTH] = {
uint8_t cmd[GUOHE_STATUS_CMD_LENGTH] = {
0xA5, 0xA5, 0xA5, 0xA5,
0x03,
0x0B,
@ -334,7 +334,8 @@ DECLARE_PROBERIG_BACKEND(guohetec) {
const int rates[] = {9600, 19200, 38400, 57600, 115200, 0};
for (int i = 0; rates[i]; i++) {
uint8_t reply[PMR171_REPLY_LENGTH];
uint8_t reply[GUOHE_MAX_FRAME_LENGTH];
struct guohetec_status status;
port->parm.serial.rate = rates[i];
port->timeout = 500;
@ -345,39 +346,35 @@ DECLARE_PROBERIG_BACKEND(guohetec) {
rig_flush(port);
int retval = write_block(port, cmd, PMR171_CMD_LENGTH);
int retval = write_block(port, cmd, GUOHE_STATUS_CMD_LENGTH);
if (retval != RIG_OK) {
continue;
}
retval = read_block(port, reply, 6);
if (retval < 6 || memcmp(reply, "\xA5\xA5\xA5\xA5", 4) != 0) {
retval = read_block(port, reply, GUOHE_FRAME_HEADER_LENGTH);
if (retval < GUOHE_FRAME_HEADER_LENGTH ||
validate_packet_header(reply, __func__) < 0) {
continue;
}
uint8_t pkt_len = reply[4];
if (pkt_len < 2 || pkt_len > (PMR171_REPLY_LENGTH - 5)) {
if (pkt_len == 0 ||
pkt_len > (GUOHE_MAX_FRAME_LENGTH - GUOHE_FRAME_HEADER_LENGTH)) {
continue;
}
retval = read_block(port, &reply[6], pkt_len - 1);
if (retval < (pkt_len - 1)) {
retval = read_block(port, &reply[GUOHE_FRAME_HEADER_LENGTH], pkt_len);
if (retval != pkt_len) {
continue;
}
uint16_t recv_crc = (reply[pkt_len + 4] << 8) | reply[pkt_len + 5];
uint16_t calc_crc = CRC16Check(&reply[4], pkt_len + 1);
if (recv_crc != calc_crc) {
if (guohetec_decode_status(reply,
GUOHE_FRAME_HEADER_LENGTH + pkt_len,
&status, __func__) < 0) {
continue;
}
if (reply[5] != 0x0B) {
continue;
}
uint32_t freq = (reply[9] << 24) | (reply[10] << 16) |
(reply[11] << 8) | reply[12];
uint32_t freq = status.freq_a;
if (freq < 100000 || freq > 470000000) {
continue;
}
@ -397,4 +394,3 @@ DECLARE_PROBERIG_BACKEND(guohetec) {
return RIG_MODEL_NONE;
}

View file

@ -25,8 +25,8 @@
#include "hamlib/rig.h"
#define PMR171_CMD_LENGTH 8
#define PMR171_REPLY_LENGTH 24
#define GUOHE_STATUS_CMD_LENGTH 8
#define GUOHE_MAX_FRAME_LENGTH 260
#define GUOHE_MODE_TABLE_MAX 8
@ -52,43 +52,31 @@
return RIG_OK; \
} while(0)
// Common response validation function declarations
int validate_packet_header(const unsigned char *reply, const char *func_name);
int validate_data_length(const unsigned char *reply, int reply_size, const char *func_name);
// Keep the macro for backward compatibility
#define VALIDATE_PACKET_HEADER(reply, func_name) validate_packet_header(reply, func_name)
#define VALIDATE_DATA_LENGTH(reply, reply_size, func_name) validate_data_length(reply, reply_size, func_name)
#define VALIDATE_READ_RESULT(ret, expected, func_name) do { \
if (ret < 0) { \
rig_debug(RIG_DEBUG_ERR, "%s: Failed to read data, using cached values\n", func_name); \
return -1; \
} \
if (ret != expected) { \
rig_debug(RIG_DEBUG_ERR, "%s: Data read mismatch: expected %d, got %d, using cached values\n", \
func_name, expected, ret); \
return -1; \
} \
} while(0)
extern struct rig_caps pmr171_caps;
extern struct rig_caps q900_caps;
struct guohetec_status
{
unsigned char ptt;
unsigned char mode_a;
unsigned char mode_b;
uint32_t freq_a;
uint32_t freq_b;
vfo_t vfo;
};
uint16_t CRC16Check(const unsigned char *buf, int len);
rmode_t guohe2rmode(unsigned char mode, const rmode_t mode_table[]);
unsigned char rmode2guohe(rmode_t mode, const rmode_t mode_table[]);
unsigned char *to_be(unsigned char data[], unsigned long long freq, unsigned int byte_len);
unsigned long long from_be(const unsigned char data[],unsigned int byte_len);
// Common response validation functions
int validate_rig_response(RIG *rig, const unsigned char *reply, int reply_size,
const char *func_name);
int read_rig_response(RIG *rig, unsigned char *reply, int reply_size,
const char *func_name);
int validate_freq_response(RIG *rig, const unsigned char *reply, int reply_size,
const char *func_name);
int validate_mode_response(RIG *rig, const unsigned char *reply, int reply_size,
const char *func_name, int min_length);
int guohetec_decode_status(const unsigned char *reply, size_t reply_size,
struct guohetec_status *status,
const char *func_name);
int guohetec_read_response(RIG *rig, unsigned char *reply, size_t reply_size,
const char *func_name);
int guohetec_get_status(RIG *rig, struct guohetec_status *status,
const char *func_name);
#endif // _guohetec_H_

View file

@ -355,72 +355,25 @@ static int pmr171_open(RIG *rig)
return RIG_OK;
}
static int pmr171_send_cmd1(RIG *rig, unsigned char cmd, unsigned char *reply)
{
hamlib_port_t *rp = RIGPORT(rig);
rig_debug(RIG_DEBUG_VERBOSE, "%s: called\n", __func__);
unsigned char buf[8] = { 0xa5, 0xa5, 0xa5, 0xa5, 0x03, 0x00, 0x00, 0x00 };
buf[5] = cmd;
unsigned int crc = CRC16Check(&buf[4], 2);
buf[6] = crc >> 8;
buf[7] = crc & 0xff;
rig_flush(rp);
write_block(rp, buf, 8);
return RIG_OK;
}
/* ---------------------------------------------------------------------- */
static int pmr171_get_freq(RIG *rig, vfo_t vfo, freq_t *freq)
{
// Send status query command (0x0B)
unsigned char cmd[8] = {
0xA5, 0xA5, 0xA5, 0xA5,
0x03,
0x0B,
0x00, 0x00
};
// Calculate CRC and fill
uint16_t crc = CRC16Check(&cmd[4], 2);
cmd[6] = crc >> 8;
cmd[7] = crc & 0xFF;
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[40];
pmr171_send(rig, cmd, sizeof(cmd), reply, sizeof(reply));
// Validate response using common function
if (validate_freq_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_FREQ(rig, vfo, freq);
}
// Parse frequency (big-endian)
int freq_a_offset = 9; // VFOA frequency starting position
int freq_b_offset = 13; // VFOB frequency starting position
uint32_t freq_a = (reply[freq_a_offset] << 24) |
(reply[freq_a_offset+1] << 16) |
(reply[freq_a_offset+2] << 8) |
reply[freq_a_offset+3];
uint32_t freq_b = (reply[freq_b_offset] << 24) |
(reply[freq_b_offset+1] << 16) |
(reply[freq_b_offset+2] << 8) |
reply[freq_b_offset+3];
// Update cache
CACHE(rig)->freqMainA = (freq_t)freq_a;
CACHE(rig)->freqMainB = (freq_t)freq_b;
// Return requested VFO frequency
*freq = (vfo == RIG_VFO_A) ? CACHE(rig)->freqMainA : CACHE(rig)->freqMainB;
rig_debug(RIG_DEBUG_VERBOSE, "%s: Successfully got VFOA=%.0f Hz, VFOB=%.0f Hz\n",
__func__, CACHE(rig)->freqMainA, CACHE(rig)->freqMainB);
RETURN_CACHED_FREQ(rig, vfo, freq);
}
CACHE(rig)->freqMainA = (freq_t)status.freq_a;
CACHE(rig)->freqMainB = (freq_t)status.freq_b;
*freq = vfo == RIG_VFO_A ? CACHE(rig)->freqMainA : CACHE(rig)->freqMainB;
rig_debug(RIG_DEBUG_VERBOSE,
"%s: Successfully got VFOA=%.0f Hz, VFOB=%.0f Hz\n",
__func__, CACHE(rig)->freqMainA, CACHE(rig)->freqMainB);
return RIG_OK;
}
@ -428,25 +381,27 @@ static int pmr171_open(RIG *rig)
{
struct rig_cache *cachep = CACHE(rig);
const pmr171_data_t *p = (pmr171_data_t *) STATE(rig)->priv;
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[40];
// Get latest status from hardware
pmr171_send_cmd1(rig, 0x0b, 0);
// Read and validate response using common function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
// Validate mode response using common function
if (validate_mode_response(rig, reply, sizeof(reply), __func__, 5) < 0) {
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
// Update cache
cachep->modeMainA = guohe2rmode(reply[7], pmr171_modes);
cachep->modeMainB = guohe2rmode(reply[8], pmr171_modes);
// Return requested mode
*mode = (vfo == RIG_VFO_A) ? cachep->modeMainA : cachep->modeMainB;
*width = p->filterBW;
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
if (status.mode_a >= GUOHE_MODE_TABLE_MAX ||
status.mode_b >= GUOHE_MODE_TABLE_MAX)
{
rig_debug(RIG_DEBUG_ERR,
"%s: Invalid mode indices %u/%u, using cached values\n",
__func__, status.mode_a, status.mode_b);
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
cachep->modeMainA = guohe2rmode(status.mode_a, pmr171_modes);
cachep->modeMainB = guohe2rmode(status.mode_b, pmr171_modes);
*mode = vfo == RIG_VFO_A ? cachep->modeMainA : cachep->modeMainB;
*width = p->filterBW;
return RIG_OK;
}
@ -463,22 +418,15 @@ static int pmr171_open(RIG *rig)
static int pmr171_get_vfo(RIG *rig, vfo_t *vfo)
{
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[40];
// Send status sync command to get current VFO state
pmr171_send_cmd1(rig, 0x0b, 0);
// Read and validate response using common function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_VFO(rig, vfo);
}
// Validate VFO status field index won't overflow
if (reply[4] < 13) { // Need at least 13 bytes to access reply[17]
rig_debug(RIG_DEBUG_ERR, "%s: Response too short for VFO data, using cached values\n", __func__);
RETURN_CACHED_VFO(rig, vfo);
}
// According to protocol doc, reply[17] is A/B frequency status
*vfo = (reply[17] == 1) ? RIG_VFO_B : RIG_VFO_A;
RETURN_CACHED_VFO(rig, vfo);
}
*vfo = status.vfo;
return RIG_OK;
}
@ -486,22 +434,16 @@ static int pmr171_open(RIG *rig)
static int pmr171_get_ptt(RIG *rig, vfo_t vfo, ptt_t *ptt)
{
struct rig_cache *cachep = CACHE(rig);
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[40];
pmr171_send_cmd1(rig, 0x0b, 0);
// Read and validate response using common function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_PTT(rig, ptt, cachep);
}
// Validate PTT status field index won't overflow
if (reply[4] < 2) { // Need at least 2 bytes to access reply[6]
rig_debug(RIG_DEBUG_ERR, "%s: Response too short for PTT data, using cached values\n", __func__);
RETURN_CACHED_PTT(rig, ptt, cachep);
}
// Get PTT status
cachep->ptt = reply[6];
*ptt = cachep->ptt;
RETURN_CACHED_PTT(rig, ptt, cachep);
}
cachep->ptt = status.ptt;
*ptt = cachep->ptt;
return RIG_OK;
}
@ -551,7 +493,7 @@ static int pmr171_open(RIG *rig)
{
unsigned char reply[40];
// Use common response reading function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
if (guohetec_read_response(rig, reply, sizeof(reply), __func__) < 0) {
return RIG_OK; // Return OK to use cached values
}
}
@ -658,7 +600,7 @@ static int pmr171_open(RIG *rig)
write_block(rp, cmd, 10);
// Use common response reading function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
if (guohetec_read_response(rig, reply, sizeof(reply), __func__) < 0) {
// Update cache with requested mode even if response failed
if (vfo == RIG_VFO_B)
{

View file

@ -354,63 +354,25 @@ static int q900_open(RIG *rig)
return RIG_OK;
}
static int q900_send_cmd1(RIG *rig, unsigned char cmd, unsigned char *reply)
{
hamlib_port_t *rp = RIGPORT(rig);
rig_debug(RIG_DEBUG_VERBOSE, "%s: called\n", __func__);
unsigned char buf[8] = { 0xa5, 0xa5, 0xa5, 0xa5, 0x03, 0x00, 0x00, 0x00 };
buf[5] = cmd;
unsigned int crc = CRC16Check(&buf[4], 2);
buf[6] = crc >> 8;
buf[7] = crc & 0xff;
rig_flush(rp);
write_block(rp, buf, 8);
return RIG_OK;
}
/* ---------------------------------------------------------------------- */
static int q900_get_freq(RIG *rig, vfo_t vfo, freq_t *freq)
{
unsigned char cmd[8] = {
0xA5, 0xA5, 0xA5, 0xA5,
0x03,
0x0B,
0x00, 0x00
};
uint16_t crc = CRC16Check(&cmd[4], 2);
cmd[6] = crc >> 8;
cmd[7] = crc & 0xFF;
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[40];
q900_send(rig, cmd, sizeof(cmd), reply, sizeof(reply));
// Validate response using common function
if (validate_freq_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_FREQ(rig, vfo, freq);
}
// Parse frequency (big-endian)
int freq_a_offset = 9; // VFOA frequency starting position
int freq_b_offset = 13; // VFOB frequency starting position
uint32_t freq_a = (reply[freq_a_offset] << 24) |
(reply[freq_a_offset+1] << 16) |
(reply[freq_a_offset+2] << 8) |
reply[freq_a_offset+3];
uint32_t freq_b = (reply[freq_b_offset] << 24) |
(reply[freq_b_offset+1] << 16) |
(reply[freq_b_offset+2] << 8) |
reply[freq_b_offset+3];
// Update cache
CACHE(rig)->freqMainA = (freq_t)freq_a;
CACHE(rig)->freqMainB = (freq_t)freq_b;
// Return requested VFO frequency
*freq = (vfo == RIG_VFO_A) ? CACHE(rig)->freqMainA : CACHE(rig)->freqMainB;
rig_debug(RIG_DEBUG_VERBOSE, "%s: Successfully got VFOA=%.0f Hz, VFOB=%.0f Hz\n",
__func__, CACHE(rig)->freqMainA, CACHE(rig)->freqMainB);
RETURN_CACHED_FREQ(rig, vfo, freq);
}
CACHE(rig)->freqMainA = (freq_t)status.freq_a;
CACHE(rig)->freqMainB = (freq_t)status.freq_b;
*freq = vfo == RIG_VFO_A ? CACHE(rig)->freqMainA : CACHE(rig)->freqMainB;
rig_debug(RIG_DEBUG_VERBOSE,
"%s: Successfully got VFOA=%.0f Hz, VFOB=%.0f Hz\n",
__func__, CACHE(rig)->freqMainA, CACHE(rig)->freqMainB);
return RIG_OK;
}
@ -418,25 +380,27 @@ static int q900_get_freq(RIG *rig, vfo_t vfo, freq_t *freq)
{
struct rig_cache *cachep = CACHE(rig);
const q900_data_t *p = (q900_data_t *) STATE(rig)->priv;
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[255];
// Get latest status from hardware
q900_send_cmd1(rig, 0x0b, 0);
// Read and validate response using common function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
// Validate mode response using common function
if (validate_mode_response(rig, reply, sizeof(reply), __func__, 5) < 0) {
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
// Update cache
cachep->modeMainA = guohe2rmode(reply[7], q900_modes);
cachep->modeMainB = guohe2rmode(reply[8], q900_modes);
// Return requested mode
*mode = (vfo == RIG_VFO_A) ? cachep->modeMainA : cachep->modeMainB;
*width = p->filterBW;
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
if (status.mode_a >= GUOHE_MODE_TABLE_MAX ||
status.mode_b >= GUOHE_MODE_TABLE_MAX)
{
rig_debug(RIG_DEBUG_ERR,
"%s: Invalid mode indices %u/%u, using cached values\n",
__func__, status.mode_a, status.mode_b);
RETURN_CACHED_MODE(rig, vfo, mode, width, cachep, p);
}
cachep->modeMainA = guohe2rmode(status.mode_a, q900_modes);
cachep->modeMainB = guohe2rmode(status.mode_b, q900_modes);
*mode = vfo == RIG_VFO_A ? cachep->modeMainA : cachep->modeMainB;
*width = p->filterBW;
return RIG_OK;
}
@ -453,22 +417,15 @@ static int q900_get_freq(RIG *rig, vfo_t vfo, freq_t *freq)
static int q900_get_vfo(RIG *rig, vfo_t *vfo)
{
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[255];
// Send status sync command to get current VFO state
q900_send_cmd1(rig, 0x0b, 0);
// Read and validate response using common function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_VFO(rig, vfo);
}
// Validate VFO status field index won't overflow
if (reply[4] < 13) { // Need at least 13 bytes to access reply[17]
rig_debug(RIG_DEBUG_ERR, "%s: Response too short for VFO data, using cached values\n", __func__);
RETURN_CACHED_VFO(rig, vfo);
}
// According to protocol doc, reply[17] is A/B frequency status
*vfo = (reply[17] == 1) ? RIG_VFO_B : RIG_VFO_A;
RETURN_CACHED_VFO(rig, vfo);
}
*vfo = status.vfo;
return RIG_OK;
}
@ -476,22 +433,16 @@ static int q900_get_freq(RIG *rig, vfo_t vfo, freq_t *freq)
static int q900_get_ptt(RIG *rig, vfo_t vfo, ptt_t *ptt)
{
struct rig_cache *cachep = CACHE(rig);
struct guohetec_status status;
if (guohetec_get_status(rig, &status, __func__) < 0)
{
unsigned char reply[255];
q900_send_cmd1(rig, 0x0b, 0);
// Read and validate response using common function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
RETURN_CACHED_PTT(rig, ptt, cachep);
}
// Validate PTT status field index won't overflow
if (reply[4] < 2) { // Need at least 2 bytes to access reply[6]
rig_debug(RIG_DEBUG_ERR, "%s: Response too short for PTT data, using cached values\n", __func__);
RETURN_CACHED_PTT(rig, ptt, cachep);
}
// Get PTT status
cachep->ptt = reply[6];
*ptt = cachep->ptt;
RETURN_CACHED_PTT(rig, ptt, cachep);
}
cachep->ptt = status.ptt;
*ptt = cachep->ptt;
return RIG_OK;
}
@ -544,7 +495,7 @@ static int q900_get_ptt(RIG *rig, vfo_t vfo, ptt_t *ptt)
{
unsigned char reply[256];
// Use common response reading function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
if (guohetec_read_response(rig, reply, sizeof(reply), __func__) < 0) {
return RIG_OK; // Return OK to use cached values
}
}
@ -648,7 +599,7 @@ static int q900_get_ptt(RIG *rig, vfo_t vfo, ptt_t *ptt)
write_block(rp, cmd, 10);
// Use common response reading function
if (read_rig_response(rig, reply, sizeof(reply), __func__) < 0) {
if (guohetec_read_response(rig, reply, sizeof(reply), __func__) < 0) {
// Update cache with requested mode even if response failed
if (vfo == RIG_VFO_B)
{
@ -790,4 +741,3 @@ static int q900_set_ptt(RIG *rig, vfo_t vfo, ptt_t ptt)
}
/* ---------------------------------------------------------------------- */

View file

@ -24,6 +24,7 @@ DIRECT_TESTS = \
testftx1parsers \
testgeministatus \
testgs100 \
testguohetec \
testicomts
GENERATED_TEST_WRAPPERS = \
@ -102,9 +103,11 @@ testdebug_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
testctlparser_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
testgeministatus_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
testgs100_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
testguohetec_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS)
testicomts_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/rigs/icom
testgeministatus_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/amplifiers/gemini
testftx1parsers_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/rigs/yaesu/ftx1
testguohetec_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/rigs/guohetec
if TESTS_HAVE_LIBUSB
rigtestlibusb_CFLAGS = $(AM_CFLAGS) $(PTHREAD_CFLAGS) $(LIBUSB_CFLAGS)
endif
@ -130,6 +133,7 @@ testicomts_LDADD = $(top_builddir)/rigs/icom/libhamlib-icom.la $(LDADD)
testgeministatus_LDADD = $(PTHREAD_LIBS) $(top_builddir)/amplifiers/gemini/libhamlib-gemini.la $(LDADD)
testgs100_LDADD = $(PTHREAD_LIBS) $(top_builddir)/rigs/gomspace/libhamlib-gomspace.la $(LDADD)
testftx1parsers_LDADD = $(top_builddir)/rigs/yaesu/libhamlib-yaesu.la $(LDADD)
testguohetec_LDADD = $(PTHREAD_LIBS) $(top_builddir)/rigs/guohetec/libhamlib-guohetec.la $(LDADD)
if TESTS_HAVE_LIBUSB
rigtestlibusb_LDADD = $(LIBUSB_LIBS)
endif
@ -208,6 +212,9 @@ $(top_builddir)/amplifiers/gemini/libhamlib-gemini.la:
$(top_builddir)/rigs/gomspace/libhamlib-gomspace.la:
$(MAKE) -C $(top_builddir)/rigs/gomspace/ libhamlib-gomspace.la
$(top_builddir)/rigs/guohetec/libhamlib-guohetec.la:
$(MAKE) -C $(top_builddir)/rigs/guohetec/ libhamlib-guohetec.la
$(top_builddir)/rigs/yaesu/libhamlib-yaesu.la:
$(MAKE) -C $(top_builddir)/rigs/yaesu/ libhamlib-yaesu.la

369
tests/testguohetec.c Normal file
View file

@ -0,0 +1,369 @@
/*
* Hamlib GUOHETEC status framing tests
* Copyright (c) 2026 by Hamlib Team
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdio.h>
#include <string.h>
#include "hamlib/rig.h"
#include "hamlib/port.h"
#include "hamlib/rig_state.h"
#include "guohetec.h"
#if defined(HAVE_SOCKETPAIR) && defined(HAVE_SYS_SOCKET_H)
#include <pthread.h>
#include <sys/socket.h>
#include <unistd.h>
#define TEST_GUOHE_TRANSACTIONS 1
#endif
static const unsigned char captured_firmware_3_5_status[] = {
0xA5, 0xA5, 0xA5, 0xA5, 0x1B, 0x0B, 0x00, 0x00,
0x00, 0x01, 0x41, 0xA7, 0xC0, 0x01, 0x41, 0x90,
0x50, 0x00, 0x00, 0x3C, 0x3C, 0x32, 0x00, 0xA6,
0x05, 0x2C, 0x0C, 0x21, 0x01, 0x00, 0x14, 0xB0
};
static const unsigned char documented_status[] = {
0xA5, 0xA5, 0xA5, 0xA5, 0x1B, 0x0B, 0x01, 0x05,
0x04, 0x08, 0xAC, 0x27, 0x60, 0x19, 0xED, 0x92,
0xC0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0xD4
};
static int expect_valid_status(const char *name, const unsigned char *frame,
size_t frame_size, uint32_t freq_a,
uint32_t freq_b, unsigned char ptt,
unsigned char mode_a, unsigned char mode_b,
vfo_t vfo)
{
struct guohetec_status status;
int ret = guohetec_decode_status(frame, frame_size, &status, name);
if (ret != RIG_OK || status.freq_a != freq_a || status.freq_b != freq_b ||
status.ptt != ptt || status.mode_a != mode_a ||
status.mode_b != mode_b || status.vfo != vfo)
{
fprintf(stderr,
"%s: unexpected decode result %d/%u/%u/%u/%u/%u/%d\n",
name, ret, status.freq_a, status.freq_b, status.ptt,
status.mode_a, status.mode_b, status.vfo);
return 1;
}
return 0;
}
static int expect_invalid_status(const char *name, const unsigned char *frame,
size_t frame_size)
{
struct guohetec_status status;
if (guohetec_decode_status(frame, frame_size, &status, name) >= 0)
{
fprintf(stderr, "%s: malformed status response was accepted\n", name);
return 1;
}
return 0;
}
static int test_status_decoder(void)
{
unsigned char invalid[sizeof(captured_firmware_3_5_status)];
static const unsigned char short_status[] = {
0xA5, 0xA5, 0xA5, 0xA5, 0x03, 0x0B, 0x00, 0x00
};
if (expect_valid_status("firmware-3.5", captured_firmware_3_5_status,
sizeof(captured_firmware_3_5_status),
21080000, 21074000, 0, 0, 0, RIG_VFO_A) != 0 ||
expect_valid_status("documented", documented_status,
sizeof(documented_status),
145500000, 435000000, 1, 5, 4,
RIG_VFO_B) != 0)
{
return 1;
}
memcpy(invalid, captured_firmware_3_5_status, sizeof(invalid));
invalid[0] = 0;
if (expect_invalid_status("bad-header", invalid, sizeof(invalid)) != 0)
{
return 1;
}
memcpy(invalid, captured_firmware_3_5_status, sizeof(invalid));
invalid[4]--;
if (expect_invalid_status("length-mismatch", invalid, sizeof(invalid)) != 0 ||
expect_invalid_status("truncated", captured_firmware_3_5_status,
sizeof(captured_firmware_3_5_status) - 1) != 0 ||
expect_invalid_status("short-payload", short_status,
sizeof(short_status)) != 0)
{
return 1;
}
memcpy(invalid, captured_firmware_3_5_status, sizeof(invalid));
invalid[4] = 0xFF;
if (expect_invalid_status("oversized-length", invalid, sizeof(invalid)) != 0)
{
return 1;
}
memcpy(invalid, captured_firmware_3_5_status, sizeof(invalid));
invalid[5] = 0x0A;
if (expect_invalid_status("wrong-command", invalid, sizeof(invalid)) != 0)
{
return 1;
}
memcpy(invalid, captured_firmware_3_5_status, sizeof(invalid));
invalid[sizeof(invalid) - 1] ^= 0x01;
return expect_invalid_status("bad-crc", invalid, sizeof(invalid));
}
#ifdef TEST_GUOHE_TRANSACTIONS
static const unsigned char status_request[] = {
0xA5, 0xA5, 0xA5, 0xA5, 0x03, 0x0B, 0xF9, 0x37
};
struct peer_case
{
int fd;
const unsigned char **responses;
size_t response_count;
int status;
};
static int read_all(int fd, unsigned char *buffer, size_t length)
{
size_t total = 0;
while (total < length)
{
ssize_t count = read(fd, buffer + total, length - total);
if (count <= 0) { return -1; }
total += (size_t)count;
}
return 0;
}
static int write_all(int fd, const unsigned char *buffer, size_t length)
{
size_t total = 0;
while (total < length)
{
ssize_t count = write(fd, buffer + total, length - total);
if (count <= 0) { return -1; }
total += (size_t)count;
}
return 0;
}
static void *run_peer(void *arg)
{
struct peer_case *test = arg;
unsigned char request[sizeof(status_request)];
test->status = -1;
for (size_t i = 0; i < test->response_count; i++)
{
if (read_all(test->fd, request, sizeof(request)) != 0 ||
memcmp(request, status_request, sizeof(request)) != 0 ||
write_all(test->fd, test->responses[i],
sizeof(captured_firmware_3_5_status)) != 0)
{
return NULL;
}
}
test->status = 0;
return NULL;
}
static int open_test_rig(rig_model_t model, int fd, RIG **rig)
{
*rig = rig_init(model);
if (*rig == NULL) { return -1; }
RIGPORT(*rig)->fd = fd;
RIGPORT(*rig)->timeout = 250;
RIGPORT(*rig)->retry = 0;
return 0;
}
static int run_transaction_case(const char *name, rig_model_t model)
{
const unsigned char *responses[] = {
captured_firmware_3_5_status,
documented_status,
captured_firmware_3_5_status
};
struct peer_case test = {
.fd = -1,
.responses = responses,
.response_count = sizeof(responses) / sizeof(responses[0]),
.status = -1
};
int sockets[2];
pthread_t thread;
RIG *rig = NULL;
freq_t freq_a = 0;
freq_t freq_b = 0;
ptt_t ptt = RIG_PTT_ON;
int ret_a = -1;
int ret_b = -1;
int ret_ptt = -1;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0)
{
perror("socketpair");
return 1;
}
test.fd = sockets[1];
if (pthread_create(&thread, NULL, run_peer, &test) != 0)
{
close(sockets[0]);
close(sockets[1]);
return 1;
}
if (open_test_rig(model, sockets[0], &rig) == 0)
{
ret_a = rig->caps->get_freq(rig, RIG_VFO_A, &freq_a);
ret_b = rig->caps->get_freq(rig, RIG_VFO_B, &freq_b);
ret_ptt = rig->caps->get_ptt(rig, RIG_VFO_A, &ptt);
RIGPORT(rig)->fd = -1;
rig_cleanup(rig);
}
close(sockets[0]);
pthread_join(thread, NULL);
close(sockets[1]);
if (test.status != 0 || ret_a != RIG_OK || ret_b != RIG_OK ||
ret_ptt != RIG_OK || freq_a != 21080000 || freq_b != 435000000 ||
ptt != RIG_PTT_OFF)
{
fprintf(stderr,
"%s: transaction mismatch %d/%d/%d/%g/%g/%d peer=%d\n",
name, ret_a, ret_b, ret_ptt, freq_a, freq_b, ptt,
test.status);
return 1;
}
return 0;
}
static int test_cached_fallback(void)
{
unsigned char corrupt_status[sizeof(captured_firmware_3_5_status)];
const unsigned char *responses[] = {
captured_firmware_3_5_status,
corrupt_status
};
struct peer_case test = {
.fd = -1,
.responses = responses,
.response_count = sizeof(responses) / sizeof(responses[0]),
.status = -1
};
int sockets[2];
pthread_t thread;
RIG *rig = NULL;
freq_t first = 0;
freq_t cached = 0;
int first_ret = -1;
int cached_ret = -1;
memcpy(corrupt_status, captured_firmware_3_5_status,
sizeof(corrupt_status));
corrupt_status[sizeof(corrupt_status) - 1] ^= 0x01;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0)
{
perror("socketpair");
return 1;
}
test.fd = sockets[1];
if (pthread_create(&thread, NULL, run_peer, &test) != 0)
{
close(sockets[0]);
close(sockets[1]);
return 1;
}
if (open_test_rig(RIG_MODEL_PMR171, sockets[0], &rig) == 0)
{
first_ret = rig->caps->get_freq(rig, RIG_VFO_A, &first);
cached_ret = rig->caps->get_freq(rig, RIG_VFO_B, &cached);
RIGPORT(rig)->fd = -1;
rig_cleanup(rig);
}
close(sockets[0]);
pthread_join(thread, NULL);
close(sockets[1]);
if (test.status != 0 || first_ret != RIG_OK || cached_ret != RIG_OK ||
first != 21080000 || cached != 21074000)
{
fprintf(stderr,
"cached-fallback: mismatch %d/%d/%g/%g peer=%d\n",
first_ret, cached_ret, first, cached, test.status);
return 1;
}
return 0;
}
#endif
int main(void)
{
if (test_status_decoder() != 0) { return 1; }
#ifdef TEST_GUOHE_TRANSACTIONS
rig_register(&pmr171_caps);
rig_register(&q900_caps);
if (run_transaction_case("PMR-171", RIG_MODEL_PMR171) != 0 ||
run_transaction_case("Q900", RIG_MODEL_Q900) != 0 ||
test_cached_fallback() != 0)
{
return 1;
}
#endif
return 0;
}