feat(kenwood): add TH-D75 backend

Register exact TH-D75 identity and regional capabilities. Replace the fixed-offset D74/D75 FO and ME handling with bounded typed record codecs, and make shared Kenwood mode lookup preserve its unsupported sentinel within the declared table bounds.

Add malformed-input and round-trip codec coverage while leaving memory writes, arbitrary split, clone names, and asynchronous status unadvertised.
This commit is contained in:
David Christle 2026-07-29 17:04:26 -05:00
parent 5bc6deefe1
commit 07d6a7ff71
No known key found for this signature in database
18 changed files with 1616 additions and 379 deletions

View file

@ -869,6 +869,7 @@ class TestClass:
'RIG_MODEL_TCI1X',
'RIG_MODEL_THD72A',
'RIG_MODEL_THD74',
'RIG_MODEL_THD75',
'RIG_MODEL_THD7A',
'RIG_MODEL_THD7AG',
'RIG_MODEL_THETIS',

View file

@ -249,6 +249,7 @@
#define RIG_MODEL_SDRCONSOLE RIG_MAKE_MODEL(RIG_KENWOOD, 56)
#define RIG_MODEL_QRPLABS_QMX RIG_MAKE_MODEL(RIG_KENWOOD, 57)
#define RIG_MODEL_HAMGEEK_USDX RIG_MAKE_MODEL(RIG_KENWOOD, 58)
#define RIG_MODEL_THD75 RIG_MAKE_MODEL(RIG_KENWOOD, 59)
///@}
/*

View file

@ -8,7 +8,7 @@ IC10SRC = ts440.c ts940.c ts711.c ts811.c r5000.c
THSRC = thd7.c thf7.c thg71.c tmd700.c tmv7.c thf6a.c thd72.c tmd710.c thd74.c
KENWOODSRC = kenwood.c kenwood.h th.c th.h ic10.c ic10.h elecraft.c elecraft.h \
KENWOODSRC = kenwood.c kenwood.h th.c th.h thd7x.c thd7x.h ic10.c ic10.h elecraft.c elecraft.h \
transfox.c flex.c flex.h level_gran_kenwood.h level_gran_elecraft.h \
hamgeek.c hamgeek.h

View file

@ -427,7 +427,7 @@ static int flex6k_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width)
{
struct kenwood_priv_caps *caps = kenwood_caps(rig);
char buf[10];
char kmode;
int kmode;
int idx;
int err;
@ -498,7 +498,7 @@ static int powersdr_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width)
{
struct kenwood_priv_caps *caps = kenwood_caps(rig);
char buf[64];
char kmode;
int kmode;
int idx;
int err;

View file

@ -1566,7 +1566,7 @@ static int k3_set_split_mode(RIG *rig, vfo_t vfo, rmode_t tx_mode, pbwidth_t tx_
{
struct kenwood_priv_caps *caps = kenwood_caps(rig);
char buf[32];
char kmode;
int kmode;
int err;
char cmd_m[16];
const struct kenwood_priv_data *priv = STATE(rig)->priv;

View file

@ -129,6 +129,7 @@ static const struct kenwood_id_string kenwood_id_string_list[] =
{ RIG_MODEL_TMD710, "TM-D710" },
{ RIG_MODEL_THD72A, "TH-D72" },
{ RIG_MODEL_THD74, "TH-D74" },
{ RIG_MODEL_THD75, "TH-D75" },
{ RIG_MODEL_TMV7, "TM-V7" },
{ RIG_MODEL_TMV71, "TM-V71" },
{ RIG_MODEL_THF6A, "TH-F6" },
@ -814,7 +815,7 @@ rmode_t kenwood2rmode(unsigned char mode, const rmode_t mode_table[])
return (mode_table[mode]);
}
char rmode2kenwood(rmode_t mode, const rmode_t mode_table[])
int rmode2kenwood(rmode_t mode, const rmode_t mode_table[])
{
rig_debug(RIG_DEBUG_VERBOSE, "%s called, mode=%s\n", __func__,
rig_strrmode(mode));
@ -1174,7 +1175,7 @@ int kenwood_open(RIG *rig)
it's not supported */
}
if (!RIG_IS_THD74 && !RIG_IS_THD7A && !RIG_IS_TMD700)
if (!RIG_IS_THD74 && !RIG_IS_THD75 && !RIG_IS_THD7A && !RIG_IS_TMD700)
{
int retval;
// call get_split to fill in current split and tx_vfo status
@ -2466,7 +2467,7 @@ static int kenwood_set_filter_width(RIG *rig, rmode_t mode, pbwidth_t width)
int kenwood_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width)
{
char c;
char kmode;
int kmode;
char buf[6];
char data_mode = '0';
char *data_cmd = "DA";
@ -5294,6 +5295,7 @@ int kenwood_set_trn(RIG *rig, int trn)
case RIG_MODEL_THD7A:
case RIG_MODEL_THD74:
case RIG_MODEL_THD75:
RETURNFUNC(kenwood_transaction(rig, (trn == RIG_TRN_RIG) ? "AI 1" : "AI 0", buf,
sizeof buf));
@ -5325,7 +5327,7 @@ int kenwood_get_trn(RIG *rig, int *trn)
RETURNFUNC(-RIG_ENAVAIL);
}
if (RIG_IS_THD74 || RIG_IS_THD7A || RIG_IS_TMD700)
if (RIG_IS_THD74 || RIG_IS_THD75 || RIG_IS_THD7A || RIG_IS_TMD700)
{
retval = kenwood_safe_transaction(rig, "AI", trnbuf, 6, 4);
}
@ -5339,7 +5341,7 @@ int kenwood_get_trn(RIG *rig, int *trn)
RETURNFUNC(retval);
}
if (RIG_IS_THD74 || RIG_IS_THD7A || RIG_IS_TMD700)
if (RIG_IS_THD74 || RIG_IS_THD75 || RIG_IS_THD7A || RIG_IS_TMD700)
{
*trn = trnbuf[3] != '0' ? RIG_TRN_RIG : RIG_TRN_OFF;
}
@ -6014,7 +6016,7 @@ int kenwood_get_channel(RIG *rig, vfo_t vfo, channel_t *chan, int read_only)
int kenwood_set_channel(RIG *rig, vfo_t vfo, const channel_t *chan)
{
char buf[128];
char mode, tx_mode = 0;
int mode, tx_mode = 0;
char bank = ' ';
int err;
int tone = 0;
@ -6531,6 +6533,7 @@ DECLARE_INITRIG_BACKEND(kenwood)
rig_register(&thd7a_caps);
rig_register(&thd72a_caps);
rig_register(&thd74_caps);
rig_register(&thd75_caps);
rig_register(&thf7e_caps);
rig_register(&thg71_caps);
rig_register(&tmv7_caps);

View file

@ -95,6 +95,7 @@ extern struct confparams kenwood_cfg_params[];
#define RIG_IS_K4 (rig->caps->rig_model == RIG_MODEL_K4)
#define RIG_IS_THD7A (rig->caps->rig_model == RIG_MODEL_THD7A)
#define RIG_IS_THD74 (rig->caps->rig_model == RIG_MODEL_THD74)
#define RIG_IS_THD75 (rig->caps->rig_model == RIG_MODEL_THD75)
#define RIG_IS_TMD700 (rig->caps->rig_model == RIG_MODEL_TMD700)
#define RIG_IS_TS2000 (rig->caps->rig_model == RIG_MODEL_TS2000)
#define RIG_IS_TS50 (rig->caps->rig_model == RIG_MODEL_TS50)
@ -208,7 +209,7 @@ int kenwood_safe_transaction(RIG *rig, const char *cmd, char *buf,
size_t buf_size, size_t expected);
rmode_t kenwood2rmode(unsigned char mode, const rmode_t mode_table[]);
char rmode2kenwood(rmode_t mode, const rmode_t mode_table[]);
int rmode2kenwood(rmode_t mode, const rmode_t mode_table[]);
int kenwood_init(RIG *rig);
int kenwood_cleanup(RIG *rig);
@ -310,6 +311,7 @@ extern struct rig_caps sdrconsole_caps;
extern struct rig_caps thd7a_caps;
extern struct rig_caps thd72a_caps;
extern struct rig_caps thd74_caps;
extern struct rig_caps thd75_caps;
extern struct rig_caps tmd700_caps;
extern struct rig_caps thf7a_caps;
extern struct rig_caps thf7e_caps;

View file

@ -598,7 +598,7 @@ int pihspdr_set_channel(RIG *rig, vfo_t vfo, const channel_t *chan)
char sqltype;
char shift;
char buf[128];
char mode, tx_mode = 0;
int mode, tx_mode = 0;
int err;
int tone = 0;
int tstep;

View file

@ -286,7 +286,8 @@ th_get_freq(RIG *rig, vfo_t vfo, freq_t *freq)
int
th_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width)
{
char kmode, mdbuf[8];
char mdbuf[8];
int kmode;
const struct kenwood_priv_caps *priv = (const struct kenwood_priv_caps *)
rig->caps->priv;
@ -447,7 +448,8 @@ th_set_vfo(RIG *rig, vfo_t vfo)
/* No "VMC" cmd on THD72A/THD74 */
if (rig->caps->rig_model == RIG_MODEL_THD72A
|| rig->caps->rig_model == RIG_MODEL_THD74)
|| rig->caps->rig_model == RIG_MODEL_THD74
|| rig->caps->rig_model == RIG_MODEL_THD75)
{
return RIG_OK;
}
@ -556,7 +558,8 @@ th_get_vfo_char(RIG *rig, vfo_t *vfo, char *vfoch)
/* No "VMC" on THD72A/THD74 */
if (rig->caps->rig_model == RIG_MODEL_THD72A
|| rig->caps->rig_model == RIG_MODEL_THD74)
|| rig->caps->rig_model == RIG_MODEL_THD74
|| rig->caps->rig_model == RIG_MODEL_THD75)
{
*vfoch = '0'; /* FIXME: fake */

View file

@ -54,7 +54,7 @@
#define THD72_VFO (RIG_VFO_A|RIG_VFO_B)
static rmode_t thd72_mode_table[3] =
static rmode_t thd72_mode_table[KENWOOD_MODE_TABLE_MAX] =
{
[0] = RIG_MODE_FM, /* normal, but narrow compared to broadcast */
[1] = RIG_MODE_FMN, /* what kenwood calls narrow */

File diff suppressed because it is too large Load diff

550
rigs/kenwood/thd7x.c Normal file
View file

@ -0,0 +1,550 @@
/*
* Hamlib Kenwood TH-D74/TH-D75 record codec
* 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.
*/
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "hamlib/rig.h"
#include "thd7x.h"
#define THD7X_FREQUENCY_MAX UINT64_C(9999999999)
struct thd7x_cursor
{
const char *next;
size_t remaining;
};
struct thd7x_field
{
const char *value;
size_t length;
};
static int thd7x_next_field(struct thd7x_cursor *cursor, int final,
struct thd7x_field *field)
{
size_t length = 0;
while (length < cursor->remaining && cursor->next[length] != ',')
{
length++;
}
if ((!final && length == cursor->remaining)
|| (final && length != cursor->remaining))
{
return -RIG_EPROTO;
}
field->value = cursor->next;
field->length = length;
cursor->next += length + (final ? 0 : 1);
cursor->remaining -= length + (final ? 0 : 1);
return RIG_OK;
}
static int thd7x_decimal_field(struct thd7x_cursor *cursor, int final,
size_t width, uint64_t maximum,
uint64_t *value)
{
struct thd7x_field field;
uint64_t parsed = 0;
int retval;
retval = thd7x_next_field(cursor, final, &field);
if (retval != RIG_OK || field.length != width)
{
return -RIG_EPROTO;
}
for (size_t i = 0; i < field.length; i++)
{
uint64_t digit;
if (field.value[i] < '0' || field.value[i] > '9')
{
return -RIG_EPROTO;
}
digit = (uint64_t)(field.value[i] - '0');
if (digit > maximum || parsed > (maximum - digit) / 10)
{
return -RIG_EPROTO;
}
parsed = parsed * 10 + digit;
}
*value = parsed;
return RIG_OK;
}
static int thd7x_step_field(struct thd7x_cursor *cursor, uint8_t *step)
{
struct thd7x_field field;
char code;
int retval;
retval = thd7x_next_field(cursor, 0, &field);
if (retval != RIG_OK || field.length != 1)
{
return -RIG_EPROTO;
}
code = field.value[0];
if (code >= '0' && code <= '9')
{
*step = (uint8_t)(code - '0');
return RIG_OK;
}
if (code == 'A' || code == 'B')
{
*step = (uint8_t)(10 + code - 'A');
return RIG_OK;
}
return -RIG_EPROTO;
}
static int thd7x_urcall_field(struct thd7x_cursor *cursor, char *urcall)
{
struct thd7x_field field;
int retval;
retval = thd7x_next_field(cursor, 0, &field);
if (retval != RIG_OK || field.length > THD7X_URCALL_MAX)
{
return -RIG_EPROTO;
}
for (size_t i = 0; i < field.length; i++)
{
if (field.value[i] == '\0' || field.value[i] == '\r'
|| field.value[i] == '\n')
{
return -RIG_EPROTO;
}
}
memcpy(urcall, field.value, field.length);
urcall[field.length] = '\0';
return RIG_OK;
}
static int thd7x_start_cursor(const char *input, size_t input_len,
const char *prefix,
struct thd7x_cursor *cursor)
{
const size_t prefix_len = 3;
if (input_len > 0 && input[input_len - 1] == '\r')
{
input_len--;
}
if (input_len < prefix_len || memcmp(input, prefix, prefix_len) != 0)
{
return -RIG_EPROTO;
}
cursor->next = input + prefix_len;
cursor->remaining = input_len - prefix_len;
return RIG_OK;
}
static int thd7x_urcall_length(const char *urcall, size_t *length)
{
for (size_t i = 0; i <= THD7X_URCALL_MAX; i++)
{
if (urcall[i] == '\0')
{
*length = i;
return RIG_OK;
}
if (urcall[i] == ',' || urcall[i] == '\r' || urcall[i] == '\n')
{
return -RIG_EINVAL;
}
}
return -RIG_EINVAL;
}
static char thd7x_step_code(uint8_t step)
{
return step < 10 ? (char)('0' + step) : (char)('A' + step - 10);
}
static int thd7x_fo_valid(const struct thd7x_fo_record *record,
size_t *urcall_len)
{
if (record->band > 1 || record->frequency_hz > THD7X_FREQUENCY_MAX
|| record->offset_hz > THD7X_FREQUENCY_MAX
|| record->rx_step > 11 || record->tx_step > 11
|| record->mode > 9 || record->fine_enabled > 1
|| record->fine_step > 3 || record->tone_enabled > 1
|| record->ctcss_enabled > 1 || record->dcs_enabled > 1
|| record->cross_enabled > 1 || record->reverse_enabled > 1
|| record->shift > 3 || record->tone_index > 41
|| record->ctcss_index > 41 || record->dcs_index > 103
|| record->cross_selector > 3
|| record->digital_squelch_type > 2
|| record->digital_squelch_code > 99)
{
return -RIG_EINVAL;
}
return thd7x_urcall_length(record->urcall, urcall_len);
}
static int thd7x_me_valid(const struct thd7x_me_record *record,
size_t *urcall_len)
{
if (record->channel > 999 || record->frequency_hz > THD7X_FREQUENCY_MAX
|| record->offset_hz > THD7X_FREQUENCY_MAX
|| record->rx_step > 11 || record->tx_step > 11
|| record->mode > 9 || record->fine_enabled > 1
|| record->fine_step > 3 || record->tone_enabled > 1
|| record->ctcss_enabled > 1 || record->dcs_enabled > 1
|| record->cross_enabled > 1 || record->reverse_enabled > 1
|| record->odd_split_enabled > 1 || record->shift > 3
|| record->tone_index > 41 || record->ctcss_index > 41
|| record->dcs_index > 103 || record->cross_selector > 3
|| record->digital_squelch_type > 2
|| record->digital_squelch_code > 99
|| record->lockout_enabled > 1)
{
return -RIG_EINVAL;
}
return thd7x_urcall_length(record->urcall, urcall_len);
}
int thd7x_parse_fo(const char *input, size_t input_len,
struct thd7x_fo_record *record)
{
struct thd7x_fo_record parsed = { 0 };
struct thd7x_cursor cursor;
uint64_t value;
if (input == NULL || record == NULL)
{
return -RIG_EINVAL;
}
if (thd7x_start_cursor(input, input_len, "FO ", &cursor) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 1, 1, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.band = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 10, THD7X_FREQUENCY_MAX,
&parsed.frequency_hz) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 10, THD7X_FREQUENCY_MAX,
&parsed.offset_hz) != RIG_OK
|| thd7x_step_field(&cursor, &parsed.rx_step) != RIG_OK
|| thd7x_step_field(&cursor, &parsed.tx_step) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 1, 9, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.mode = (uint8_t)value;
#define THD7X_PARSE_FO_FIELD(member, maximum) \
if (thd7x_decimal_field(&cursor, 0, 1, maximum, &value) != RIG_OK) \
{ \
return -RIG_EPROTO; \
} \
parsed.member = (uint8_t)value
THD7X_PARSE_FO_FIELD(fine_enabled, 1);
THD7X_PARSE_FO_FIELD(fine_step, 3);
THD7X_PARSE_FO_FIELD(tone_enabled, 1);
THD7X_PARSE_FO_FIELD(ctcss_enabled, 1);
THD7X_PARSE_FO_FIELD(dcs_enabled, 1);
THD7X_PARSE_FO_FIELD(cross_enabled, 1);
THD7X_PARSE_FO_FIELD(reverse_enabled, 1);
THD7X_PARSE_FO_FIELD(shift, 3);
#undef THD7X_PARSE_FO_FIELD
if (thd7x_decimal_field(&cursor, 0, 2, 41, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.tone_index = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 2, 41, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.ctcss_index = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 3, 103, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.dcs_index = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 1, 3, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.cross_selector = (uint8_t)value;
if (thd7x_urcall_field(&cursor, parsed.urcall) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 1, 2, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.digital_squelch_type = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 1, 2, 99, &value) != RIG_OK
|| cursor.remaining != 0)
{
return -RIG_EPROTO;
}
parsed.digital_squelch_code = (uint8_t)value;
*record = parsed;
return RIG_OK;
}
int thd7x_serialize_fo(const struct thd7x_fo_record *record, char *output,
size_t output_size, size_t *output_len)
{
size_t urcall_len;
int length;
if (record == NULL || output == NULL)
{
return -RIG_EINVAL;
}
if (thd7x_fo_valid(record, &urcall_len) != RIG_OK)
{
return -RIG_EINVAL;
}
length = snprintf(output, output_size,
"FO %u,%010" PRIu64 ",%010" PRIu64
",%c,%c,%u,%u,%u,%u,%u,%u,%u,%u,%u"
",%02u,%02u,%03u,%u,%.*s,%u,%02u",
(unsigned int)record->band, record->frequency_hz,
record->offset_hz, thd7x_step_code(record->rx_step),
thd7x_step_code(record->tx_step),
(unsigned int)record->mode,
(unsigned int)record->fine_enabled,
(unsigned int)record->fine_step,
(unsigned int)record->tone_enabled,
(unsigned int)record->ctcss_enabled,
(unsigned int)record->dcs_enabled,
(unsigned int)record->cross_enabled,
(unsigned int)record->reverse_enabled,
(unsigned int)record->shift,
(unsigned int)record->tone_index,
(unsigned int)record->ctcss_index,
(unsigned int)record->dcs_index,
(unsigned int)record->cross_selector,
(int)urcall_len, record->urcall,
(unsigned int)record->digital_squelch_type,
(unsigned int)record->digital_squelch_code);
if (length < 0)
{
return -RIG_EINTERNAL;
}
if (output_len != NULL)
{
*output_len = (size_t)length;
}
return (size_t)length < output_size ? RIG_OK : -RIG_ETRUNC;
}
int thd7x_parse_me(const char *input, size_t input_len,
struct thd7x_me_record *record)
{
struct thd7x_me_record parsed = { 0 };
struct thd7x_cursor cursor;
uint64_t value;
if (input == NULL || record == NULL)
{
return -RIG_EINVAL;
}
if (thd7x_start_cursor(input, input_len, "ME ", &cursor) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 3, 999, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.channel = (uint16_t)value;
if (thd7x_decimal_field(&cursor, 0, 10, THD7X_FREQUENCY_MAX,
&parsed.frequency_hz) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 10, THD7X_FREQUENCY_MAX,
&parsed.offset_hz) != RIG_OK
|| thd7x_step_field(&cursor, &parsed.rx_step) != RIG_OK
|| thd7x_step_field(&cursor, &parsed.tx_step) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 1, 9, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.mode = (uint8_t)value;
#define THD7X_PARSE_ME_FIELD(member, maximum) \
if (thd7x_decimal_field(&cursor, 0, 1, maximum, &value) != RIG_OK) \
{ \
return -RIG_EPROTO; \
} \
parsed.member = (uint8_t)value
THD7X_PARSE_ME_FIELD(fine_enabled, 1);
THD7X_PARSE_ME_FIELD(fine_step, 3);
THD7X_PARSE_ME_FIELD(tone_enabled, 1);
THD7X_PARSE_ME_FIELD(ctcss_enabled, 1);
THD7X_PARSE_ME_FIELD(dcs_enabled, 1);
THD7X_PARSE_ME_FIELD(cross_enabled, 1);
THD7X_PARSE_ME_FIELD(reverse_enabled, 1);
THD7X_PARSE_ME_FIELD(odd_split_enabled, 1);
THD7X_PARSE_ME_FIELD(shift, 3);
#undef THD7X_PARSE_ME_FIELD
if (thd7x_decimal_field(&cursor, 0, 2, 41, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.tone_index = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 2, 41, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.ctcss_index = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 3, 103, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.dcs_index = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 1, 3, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.cross_selector = (uint8_t)value;
if (thd7x_urcall_field(&cursor, parsed.urcall) != RIG_OK
|| thd7x_decimal_field(&cursor, 0, 1, 2, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.digital_squelch_type = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 0, 2, 99, &value) != RIG_OK)
{
return -RIG_EPROTO;
}
parsed.digital_squelch_code = (uint8_t)value;
if (thd7x_decimal_field(&cursor, 1, 1, 1, &value) != RIG_OK
|| cursor.remaining != 0)
{
return -RIG_EPROTO;
}
parsed.lockout_enabled = (uint8_t)value;
*record = parsed;
return RIG_OK;
}
int thd7x_serialize_me(const struct thd7x_me_record *record, char *output,
size_t output_size, size_t *output_len)
{
size_t urcall_len;
int length;
if (record == NULL || output == NULL)
{
return -RIG_EINVAL;
}
if (thd7x_me_valid(record, &urcall_len) != RIG_OK)
{
return -RIG_EINVAL;
}
length = snprintf(output, output_size,
"ME %03u,%010" PRIu64 ",%010" PRIu64
",%c,%c,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u"
",%02u,%02u,%03u,%u,%.*s,%u,%02u,%u",
(unsigned int)record->channel, record->frequency_hz,
record->offset_hz, thd7x_step_code(record->rx_step),
thd7x_step_code(record->tx_step),
(unsigned int)record->mode,
(unsigned int)record->fine_enabled,
(unsigned int)record->fine_step,
(unsigned int)record->tone_enabled,
(unsigned int)record->ctcss_enabled,
(unsigned int)record->dcs_enabled,
(unsigned int)record->cross_enabled,
(unsigned int)record->reverse_enabled,
(unsigned int)record->odd_split_enabled,
(unsigned int)record->shift,
(unsigned int)record->tone_index,
(unsigned int)record->ctcss_index,
(unsigned int)record->dcs_index,
(unsigned int)record->cross_selector,
(int)urcall_len, record->urcall,
(unsigned int)record->digital_squelch_type,
(unsigned int)record->digital_squelch_code,
(unsigned int)record->lockout_enabled);
if (length < 0)
{
return -RIG_EINTERNAL;
}
if (output_len != NULL)
{
*output_len = (size_t)length;
}
return (size_t)length < output_size ? RIG_OK : -RIG_ETRUNC;
}

82
rigs/kenwood/thd7x.h Normal file
View file

@ -0,0 +1,82 @@
/*
* Hamlib Kenwood TH-D74/TH-D75 record codec
* 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.
*/
#ifndef HAMLIB_KENWOOD_THD7X_H
#define HAMLIB_KENWOOD_THD7X_H 1
#include <stddef.h>
#include <stdint.h>
#define THD7X_URCALL_MAX 8
#define THD7X_MAX_COMMAND_LENGTH 80
#define THD7X_COMMAND_BUFSIZE (THD7X_MAX_COMMAND_LENGTH + 1)
struct thd7x_fo_record
{
uint8_t band;
uint64_t frequency_hz;
uint64_t offset_hz;
uint8_t rx_step;
uint8_t tx_step;
uint8_t mode;
uint8_t fine_enabled;
uint8_t fine_step;
uint8_t tone_enabled;
uint8_t ctcss_enabled;
uint8_t dcs_enabled;
uint8_t cross_enabled;
uint8_t reverse_enabled;
uint8_t shift;
uint8_t tone_index;
uint8_t ctcss_index;
uint8_t dcs_index;
uint8_t cross_selector;
char urcall[THD7X_URCALL_MAX + 1];
uint8_t digital_squelch_type;
uint8_t digital_squelch_code;
};
struct thd7x_me_record
{
uint16_t channel;
uint64_t frequency_hz;
uint64_t offset_hz;
uint8_t rx_step;
uint8_t tx_step;
uint8_t mode;
uint8_t fine_enabled;
uint8_t fine_step;
uint8_t tone_enabled;
uint8_t ctcss_enabled;
uint8_t dcs_enabled;
uint8_t cross_enabled;
uint8_t reverse_enabled;
uint8_t odd_split_enabled;
uint8_t shift;
uint8_t tone_index;
uint8_t ctcss_index;
uint8_t dcs_index;
uint8_t cross_selector;
char urcall[THD7X_URCALL_MAX + 1];
uint8_t digital_squelch_type;
uint8_t digital_squelch_code;
uint8_t lockout_enabled;
};
int thd7x_parse_fo(const char *input, size_t input_len,
struct thd7x_fo_record *record);
int thd7x_serialize_fo(const struct thd7x_fo_record *record, char *output,
size_t output_size, size_t *output_len);
int thd7x_parse_me(const char *input, size_t input_len,
struct thd7x_me_record *record);
int thd7x_serialize_me(const struct thd7x_me_record *record, char *output,
size_t output_size, size_t *output_len);
#endif

View file

@ -1479,7 +1479,7 @@ int ts2000_set_channel(RIG *rig, vfo_t vfo, const channel_t *chan)
{
char sqltype = '0';
char buf[128];
char mode, tx_mode = 0;
int mode, tx_mode = 0;
char shift = '0';
short dcscode = 0;
short code = 0;

View file

@ -216,7 +216,7 @@ static int sf_fails;
static int ts590_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width)
{
struct kenwood_priv_caps *caps = kenwood_caps(rig);
char kmode = rmode2kenwood(mode, caps->mode_table);
int kmode = rmode2kenwood(mode, caps->mode_table);
char cmd[32], c;
int retval = -RIG_EINTERNAL;
int hwidth = 0;

View file

@ -612,7 +612,7 @@ int ts2000_set_channel(RIG *rig, vfo_t vfo, const channel_t *chan)
{
char sqltype = '0';
char buf[128];
char mode, tx_mode = 0;
int mode, tx_mode = 0;
char shift = '0';
short dcscode = 0;
short code = 0;

View file

@ -24,7 +24,8 @@ DIRECT_TESTS = \
testftx1parsers \
testgeministatus \
testgs100 \
testicomts
testicomts \
testthd7x
GENERATED_TEST_WRAPPERS = \
test2038.sh \

282
tests/testthd7x.c Normal file
View file

@ -0,0 +1,282 @@
/*
* Hamlib Kenwood TH-D74/TH-D75 record codec 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.
*/
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "hamlib/rig.h"
#include "../rigs/kenwood/thd7x.h"
static int failures;
#define EXPECT_TRUE(name, condition) \
do \
{ \
if (!(condition)) \
{ \
fprintf(stderr, "%s:%d: %s failed\n", __FILE__, __LINE__, name); \
failures++; \
} \
} while (0)
#define EXPECT_INT(name, expected, actual) \
do \
{ \
int expected_value = (expected); \
int actual_value = (actual); \
if (expected_value != actual_value) \
{ \
fprintf(stderr, "%s:%d: %s expected %d, got %d\n", \
__FILE__, __LINE__, name, expected_value, actual_value); \
failures++; \
} \
} while (0)
static void expect_string(const char *name, const char *expected,
const char *actual)
{
if (strcmp(expected, actual) != 0)
{
fprintf(stderr, "%s: expected \"%s\", got \"%s\"\n", name,
expected, actual);
failures++;
}
}
static void test_analog_fo(void)
{
static const char command[] =
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00";
struct thd7x_fo_record record;
char output[THD7X_COMMAND_BUFSIZE];
size_t output_len = 0;
EXPECT_INT("parse analog FO", RIG_OK,
thd7x_parse_fo(command, sizeof(command) - 1, &record));
EXPECT_INT("FO band", 0, record.band);
EXPECT_TRUE("FO frequency", record.frequency_hz == UINT64_C(146520000));
EXPECT_TRUE("FO offset", record.offset_hz == UINT64_C(600000));
EXPECT_INT("FO RX step", 0, record.rx_step);
EXPECT_INT("FO mode", 0, record.mode);
EXPECT_INT("FO tone", 1, record.tone_enabled);
EXPECT_INT("FO shift", 2, record.shift);
EXPECT_INT("FO tone index", 8, record.tone_index);
expect_string("FO empty URCALL", "", record.urcall);
EXPECT_INT("serialize analog FO", RIG_OK,
thd7x_serialize_fo(&record, output, sizeof(output), &output_len));
expect_string("analog FO round trip", command, output);
EXPECT_TRUE("analog FO length", output_len == strlen(command));
}
static void test_dv_fo(void)
{
static const char command[] =
"FO 1,0440000000,0000000000,A,B,1,0,0,0,0,0,0,0,0,12,12,047,0,CQCQCQ,2,23";
struct thd7x_fo_record record;
struct thd7x_fo_record reparsed;
char output[THD7X_COMMAND_BUFSIZE];
EXPECT_INT("parse DV FO", RIG_OK,
thd7x_parse_fo(command, sizeof(command) - 1, &record));
EXPECT_INT("DV FO RX step A", 10, record.rx_step);
EXPECT_INT("DV FO TX step B", 11, record.tx_step);
EXPECT_INT("DV FO mode", 1, record.mode);
expect_string("DV FO URCALL", "CQCQCQ", record.urcall);
EXPECT_INT("DV FO squelch type", 2, record.digital_squelch_type);
EXPECT_INT("serialize DV FO", RIG_OK,
thd7x_serialize_fo(&record, output, sizeof(output), NULL));
expect_string("DV FO canonical form", command, output);
EXPECT_INT("reparse DV FO", RIG_OK,
thd7x_parse_fo(output, strlen(output), &reparsed));
EXPECT_INT("reparsed DV FO RX step", record.rx_step, reparsed.rx_step);
expect_string("reparsed DV FO URCALL", record.urcall, reparsed.urcall);
}
static void test_analog_me(void)
{
static const char command[] =
"ME 042,0145370000,0000600000,0,0,0,0,1,1,0,0,0,0,0,2,12,12,000,0,CQCQCQ,0,00,1";
struct thd7x_me_record record;
char output[THD7X_COMMAND_BUFSIZE];
EXPECT_INT("parse analog ME", RIG_OK,
thd7x_parse_me(command, sizeof(command) - 1, &record));
EXPECT_INT("ME channel", 42, record.channel);
EXPECT_TRUE("ME frequency", record.frequency_hz == UINT64_C(145370000));
EXPECT_TRUE("ME offset", record.offset_hz == UINT64_C(600000));
EXPECT_INT("ME mode", 0, record.mode);
EXPECT_INT("ME fine step", 1, record.fine_step);
EXPECT_INT("ME tone index", 12, record.tone_index);
EXPECT_INT("ME lockout", 1, record.lockout_enabled);
EXPECT_INT("serialize analog ME", RIG_OK,
thd7x_serialize_me(&record, output, sizeof(output), NULL));
expect_string("analog ME round trip", command, output);
}
static void test_79_byte_dv_me(void)
{
static const char command[] =
"ME 010,0145370000,0000600000,0,0,1,0,1,1,0,0,0,0,0,2,12,12,000,0,CQCQCQ,0,00,0\r";
static const char canonical[] =
"ME 010,0145370000,0000600000,0,0,1,0,1,1,0,0,0,0,0,2,12,12,000,0,CQCQCQ,0,00,0";
struct thd7x_me_record record;
struct thd7x_me_record reparsed;
char output[THD7X_COMMAND_BUFSIZE];
size_t output_len = 0;
EXPECT_TRUE("DV ME fixture is 79 bytes", sizeof(command) - 1 == 79);
EXPECT_INT("parse 79-byte DV ME", RIG_OK,
thd7x_parse_me(command, sizeof(command) - 1, &record));
EXPECT_INT("DV ME mode", 1, record.mode);
expect_string("DV ME URCALL", "CQCQCQ", record.urcall);
EXPECT_INT("serialize DV ME", RIG_OK,
thd7x_serialize_me(&record, output, sizeof(output), &output_len));
expect_string("DV ME strips terminal CR", canonical, output);
EXPECT_TRUE("DV ME canonical length", output_len == sizeof(canonical) - 1);
EXPECT_INT("reparse DV ME", RIG_OK,
thd7x_parse_me(output, output_len, &reparsed));
EXPECT_INT("reparsed DV ME channel", record.channel, reparsed.channel);
expect_string("reparsed DV ME URCALL", record.urcall, reparsed.urcall);
}
static void test_empty_and_max_urcall(void)
{
static const char empty_command[] =
"ME 999,9999999999,9999999999,B,A,9,1,3,1,1,1,1,1,1,3,41,41,103,3,,2,99,1";
static const char max_command[] =
"ME 999,9999999999,9999999999,B,A,9,1,3,1,1,1,1,1,1,3,41,41,103,3,ABCDEFGH,2,99,1";
struct thd7x_me_record record;
char output[THD7X_COMMAND_BUFSIZE];
size_t output_len = 0;
EXPECT_INT("parse empty URCALL", RIG_OK,
thd7x_parse_me(empty_command, sizeof(empty_command) - 1,
&record));
expect_string("preserve empty URCALL", "", record.urcall);
EXPECT_INT("serialize empty URCALL", RIG_OK,
thd7x_serialize_me(&record, output, sizeof(output), NULL));
expect_string("empty URCALL round trip", empty_command, output);
EXPECT_INT("parse max URCALL", RIG_OK,
thd7x_parse_me(max_command, sizeof(max_command) - 1, &record));
EXPECT_INT("serialize max command", RIG_OK,
thd7x_serialize_me(&record, output, sizeof(output), &output_len));
EXPECT_TRUE("maximum command length",
output_len == THD7X_MAX_COMMAND_LENGTH);
expect_string("max URCALL round trip", max_command, output);
}
static void test_malformed_fo(void)
{
static const char *const malformed[] = {
"FX 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00",
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,",
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00,0",
"FO 0,,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00",
"FO 0,X146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00",
"FO 0,00146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00",
"FO 0,0146520000,0000600000,C,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00",
"FO 2,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00",
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,42,08,000,0,,0,00",
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,104,0,,0,00",
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,123456789,0,00",
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00\n",
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00\rX"
};
struct thd7x_fo_record record;
for (size_t i = 0; i < sizeof(malformed) / sizeof(malformed[0]); i++)
{
EXPECT_INT("reject malformed FO", -RIG_EPROTO,
thd7x_parse_fo(malformed[i], strlen(malformed[i]), &record));
}
EXPECT_INT("reject null FO input", -RIG_EINVAL,
thd7x_parse_fo(NULL, 0, &record));
}
static void test_malformed_me(void)
{
static const char *const malformed[] = {
"ME 1000,0145370000,0000600000,0,0,0,0,1,1,0,0,0,0,0,2,12,12,000,0,,0,00,0",
"ME 010,0145370000,0000600000,0,0,0,0,1,1,0,0,0,0,0,2,12,12,000,0,,0,00,0,1",
"ME 010,0145370000,0000600000,0,0,A,0,1,1,0,0,0,0,0,2,12,12,000,0,,0,00,0",
"ME 010,0145370000,0000600000,0,0,0,0,1,1,0,0,0,0,0,2,12,12,000,0,,0,00,"
};
struct thd7x_me_record record;
for (size_t i = 0; i < sizeof(malformed) / sizeof(malformed[0]); i++)
{
EXPECT_INT("reject malformed ME", -RIG_EPROTO,
thd7x_parse_me(malformed[i], strlen(malformed[i]), &record));
}
}
static void test_length_delimited_input(void)
{
static const char command[] =
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00";
char unterminated[sizeof(command) - 1];
struct thd7x_fo_record record;
memcpy(unterminated, command, sizeof(unterminated));
EXPECT_INT("parse unterminated buffer", RIG_OK,
thd7x_parse_fo(unterminated, sizeof(unterminated), &record));
for (size_t i = 0; i < sizeof(unterminated); i++)
{
EXPECT_INT("reject every truncated prefix", -RIG_EPROTO,
thd7x_parse_fo(unterminated, i, &record));
}
}
static void test_serializer_errors(void)
{
static const char command[] =
"FO 0,0146520000,0000600000,0,0,0,0,0,1,0,0,0,0,2,08,08,000,0,,0,00";
struct thd7x_fo_record record;
char output[8];
size_t required = 0;
EXPECT_INT("parse serializer fixture", RIG_OK,
thd7x_parse_fo(command, sizeof(command) - 1, &record));
EXPECT_INT("report truncated output", -RIG_ETRUNC,
thd7x_serialize_fo(&record, output, sizeof(output), &required));
EXPECT_TRUE("report required size", required == strlen(command));
EXPECT_TRUE("truncated output terminated", output[sizeof(output) - 1] == '\0');
record.mode = 10;
EXPECT_INT("reject invalid serialized value", -RIG_EINVAL,
thd7x_serialize_fo(&record, output, sizeof(output), NULL));
EXPECT_INT("reject null output", -RIG_EINVAL,
thd7x_serialize_fo(&record, NULL, 0, NULL));
}
int main(void)
{
test_analog_fo();
test_dv_fo();
test_analog_me();
test_79_byte_dv_me();
test_empty_and_max_urcall();
test_malformed_fo();
test_malformed_me();
test_length_delimited_input();
test_serializer_errors();
if (failures != 0)
{
fprintf(stderr, "%d TH-D7x record codec test(s) failed\n", failures);
return 1;
}
return 0;
}