From 07d6a7ff71837beebe31e73232ad9b6675889bdc Mon Sep 17 00:00:00 2001 From: David Christle Date: Wed, 29 Jul 2026 17:04:26 -0500 Subject: [PATCH] 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. --- bindings/python/test_Hamlib_class.py | 1 + include/hamlib/riglist.h | 1 + rigs/kenwood/Makefile.am | 2 +- rigs/kenwood/flex6xxx.c | 4 +- rigs/kenwood/k3.c | 2 +- rigs/kenwood/kenwood.c | 15 +- rigs/kenwood/kenwood.h | 4 +- rigs/kenwood/pihpsdr.c | 2 +- rigs/kenwood/th.c | 9 +- rigs/kenwood/thd72.c | 2 +- rigs/kenwood/thd74.c | 1030 +++++++++++++++++--------- rigs/kenwood/thd7x.c | 550 ++++++++++++++ rigs/kenwood/thd7x.h | 82 ++ rigs/kenwood/ts2000.c | 2 +- rigs/kenwood/ts590.c | 2 +- rigs/kenwood/tx500.c | 2 +- tests/Makefile.am | 3 +- tests/testthd7x.c | 282 +++++++ 18 files changed, 1616 insertions(+), 379 deletions(-) create mode 100644 rigs/kenwood/thd7x.c create mode 100644 rigs/kenwood/thd7x.h create mode 100644 tests/testthd7x.c diff --git a/bindings/python/test_Hamlib_class.py b/bindings/python/test_Hamlib_class.py index abcd09c3a..9a2bd7aa8 100755 --- a/bindings/python/test_Hamlib_class.py +++ b/bindings/python/test_Hamlib_class.py @@ -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', diff --git a/include/hamlib/riglist.h b/include/hamlib/riglist.h index 5618d340c..6329a487a 100644 --- a/include/hamlib/riglist.h +++ b/include/hamlib/riglist.h @@ -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) ///@} /* diff --git a/rigs/kenwood/Makefile.am b/rigs/kenwood/Makefile.am index f0287caa4..6d1f64651 100644 --- a/rigs/kenwood/Makefile.am +++ b/rigs/kenwood/Makefile.am @@ -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 diff --git a/rigs/kenwood/flex6xxx.c b/rigs/kenwood/flex6xxx.c index fcb587196..e0d640454 100644 --- a/rigs/kenwood/flex6xxx.c +++ b/rigs/kenwood/flex6xxx.c @@ -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; diff --git a/rigs/kenwood/k3.c b/rigs/kenwood/k3.c index 5aab5c69b..66cdaf5e3 100644 --- a/rigs/kenwood/k3.c +++ b/rigs/kenwood/k3.c @@ -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; diff --git a/rigs/kenwood/kenwood.c b/rigs/kenwood/kenwood.c index ed56c60a2..5972b3119 100644 --- a/rigs/kenwood/kenwood.c +++ b/rigs/kenwood/kenwood.c @@ -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); diff --git a/rigs/kenwood/kenwood.h b/rigs/kenwood/kenwood.h index 0af672a16..89713343b 100644 --- a/rigs/kenwood/kenwood.h +++ b/rigs/kenwood/kenwood.h @@ -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; diff --git a/rigs/kenwood/pihpsdr.c b/rigs/kenwood/pihpsdr.c index 1a52c9069..b57e1f2dc 100644 --- a/rigs/kenwood/pihpsdr.c +++ b/rigs/kenwood/pihpsdr.c @@ -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; diff --git a/rigs/kenwood/th.c b/rigs/kenwood/th.c index 91d53b521..76c1420e1 100644 --- a/rigs/kenwood/th.c +++ b/rigs/kenwood/th.c @@ -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 */ diff --git a/rigs/kenwood/thd72.c b/rigs/kenwood/thd72.c index 4f40a4554..6ca78ff13 100644 --- a/rigs/kenwood/thd72.c +++ b/rigs/kenwood/thd72.c @@ -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 */ diff --git a/rigs/kenwood/thd74.c b/rigs/kenwood/thd74.c index 8eae9c4bb..161b92222 100644 --- a/rigs/kenwood/thd74.c +++ b/rigs/kenwood/thd74.c @@ -20,6 +20,7 @@ */ #include +#include #include #include #include @@ -28,11 +29,16 @@ #include "hamlib/rig_state.h" #include "kenwood.h" #include "th.h" +#include "thd7x.h" #include "misc.h" -#define THD74_MODES (RIG_MODE_FM|RIG_MODE_AM|RIG_MODE_LSB|RIG_MODE_USB|RIG_MODE_CW|RIG_MODE_FMN|RIG_MODE_WFM|RIG_MODE_CWR) +#define THD74_MODES (RIG_MODE_FM|RIG_MODE_DSTAR|RIG_MODE_AM|RIG_MODE_LSB|RIG_MODE_USB|RIG_MODE_CW|RIG_MODE_FMN|RIG_MODE_WFM|RIG_MODE_CWR) #define THD74_MODES_TX (RIG_MODE_FM) +#define THD75_BAND_A_MODES (RIG_MODE_FM|RIG_MODE_FMN|RIG_MODE_DSTAR) +#define THD75_BAND_B_MODES (THD74_MODES) +#define THD75_MODES_TX (RIG_MODE_FM|RIG_MODE_FMN|RIG_MODE_DSTAR) + #define THD74_FUNC_ALL (RIG_FUNC_TSQL| \ RIG_FUNC_TONE) @@ -42,38 +48,56 @@ RIG_LEVEL_VOXGAIN|\ RIG_LEVEL_VOXDELAY) +#define THD75_LEVEL_ALL (RIG_LEVEL_RFPOWER|RIG_LEVEL_SQL) + #define THD74_PARMS (RIG_PARM_TIME) #define THD74_VFO_OP (RIG_OP_NONE) #define THD74_VFO (RIG_VFO_A|RIG_VFO_B) -static rmode_t thd74_mode_table[10] = +#define THD75_CHANNEL_CAPS \ + .freq = 1, \ + .tx_freq = 1, \ + .split = 1, \ + .mode = 1, \ + .width = 1, \ + .tuning_step = 1, \ + .rptr_shift = 1, \ + .rptr_offs = 1, \ + .funcs = RIG_FUNC_REV, \ + .ctcss_tone = 1, \ + .ctcss_sql = 1, \ + .dcs_code = 1, \ + .dcs_sql = 1, \ + .flags = 1 + +static rmode_t thd74_mode_table[KENWOOD_MODE_TABLE_MAX] = { [0] = RIG_MODE_FM, /* normal, but narrow compared to broadcast */ -// [1] = RIG_MODE_DV, + [1] = RIG_MODE_DSTAR, [2] = RIG_MODE_AM, [3] = RIG_MODE_LSB, [4] = RIG_MODE_USB, [5] = RIG_MODE_CW, [6] = RIG_MODE_FMN, /* what kenwood calls narrow */ -// [7] = RIG_MODE_DR, + [7] = RIG_MODE_DSTAR, [8] = RIG_MODE_WFM, [9] = RIG_MODE_CWR, }; static pbwidth_t thd74_width_table[10] = { - [0] = 10000, // +-5 kHz - [1] = 5000, // +-2.5 kHz - [2] = 10000, // FIXME: what should this be? - [3] = 10000, // FIXME: what should this be? - [4] = 10000, // FIXME: what should this be? - [5] = 10000, // FIXME: what should this be? - [6] = 10000, // FIXME: what should this be? - [7] = 10000, // FIXME: what should this be? - [8] = 10000, // FIXME: what should this be? - [9] = 10000, // FIXME: what should this be? + [0] = 14000, + [1] = 6000, + [2] = 9000, + [3] = 2700, + [4] = 2700, + [5] = 500, + [6] = 7000, + [7] = 6000, + [8] = 150000, + [9] = 500, }; static rptr_shift_t thd74_rshf_table[3] = @@ -91,19 +115,20 @@ static int thd74tuningstep_fine[4] = [3] = 1000, }; -static int thd74tuningstep[11] = +static int thd74tuningstep[12] = { [0] = 5000, [1] = 6250, [2] = 8330, [3] = 9000, [4] = 10000, - [5] = 15000, - [6] = 20000, - [7] = 25000, - [8] = 30000, - [9] = 50000, - [10] = 100000, + [5] = 12500, + [6] = 15000, + [7] = 20000, + [8] = 25000, + [9] = 30000, + [10] = 50000, + [11] = 100000, }; static int thd74voxdelay[7] = @@ -257,148 +282,113 @@ static int thd74_vfoc(RIG *rig, vfo_t vfo, char *vfoc) return RIG_OK; } -static int thd74_get_freq_info(RIG *rig, vfo_t vfo, char *buf) +static int thd74_pull_fo(RIG *rig, vfo_t vfo, + struct thd7x_fo_record *record) { + char band, cmd[8], reply[THD7X_COMMAND_BUFSIZE]; int retval; - char c, cmd[8]; - rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_vfoc(rig, vfo, &c); + retval = thd74_vfoc(rig, vfo, &band); if (retval != RIG_OK) { return retval; } - SNPRINTF(cmd, sizeof(cmd), "FO %c", c); - retval = kenwood_transaction(rig, cmd, buf, 73); - return retval; -} - -/* item is an offset into reply buf that is a single char */ -static int thd74_get_freq_item(RIG *rig, vfo_t vfo, int item, int hi, int *val) -{ - int retval, lval; - char c, buf[128]; - - retval = thd74_get_freq_info(rig, vfo, buf); + SNPRINTF(cmd, sizeof(cmd), "FO %c", band); + retval = kenwood_transaction(rig, cmd, reply, sizeof(reply)); if (retval != RIG_OK) { return retval; } - c = buf[item]; - rig_debug(RIG_DEBUG_TRACE, "%s: c:%c\n", __func__, c); + retval = thd7x_parse_fo(reply, strlen(reply), record); - if (c < '0' || c > '9') + if (retval != RIG_OK || record->band != (uint8_t)(band - '0')) { + rig_debug(RIG_DEBUG_ERR, "%s: Unexpected reply '%s'\n", __func__, reply); return -RIG_EPROTO; } - lval = c - '0'; - - if (lval > hi) - { - return -RIG_EPROTO; - } - - *val = lval; return RIG_OK; } -static int thd74_set_freq_item(RIG *rig, vfo_t vfo, int item, int val) +static int thd74_push_fo(RIG *rig, struct thd7x_fo_record *record) { + struct thd7x_fo_record acknowledged; + char command[THD7X_COMMAND_BUFSIZE], reply[THD7X_COMMAND_BUFSIZE]; int retval; - char buf[128]; - rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd7x_serialize_fo(record, command, sizeof(command), NULL); if (retval != RIG_OK) { return retval; } - buf[item] = val + '0'; - return kenwood_simple_transaction(rig, buf, 72); + retval = kenwood_transaction(rig, command, reply, sizeof(reply)); + + if (retval != RIG_OK) + { + return retval; + } + + retval = thd7x_parse_fo(reply, strlen(reply), &acknowledged); + + if (retval != RIG_OK || acknowledged.band != record->band) + { + rig_debug(RIG_DEBUG_ERR, "%s: Unexpected reply '%s'\n", __func__, reply); + return -RIG_EPROTO; + } + + *record = acknowledged; + return RIG_OK; +} + +static int thd74_record_ts(const struct thd7x_fo_record *record, + shortfreq_t *ts) +{ + if (record->fine_enabled) + { + *ts = thd74tuningstep_fine[record->fine_step]; + } + else + { + *ts = thd74tuningstep[record->rx_step]; + } + + return RIG_OK; } static int thd74_get_ts(RIG *rig, vfo_t vfo, shortfreq_t *ts) { - int retval, tsinx, fine, fine_ts; + struct thd7x_fo_record record; + int retval; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_get_freq_item(rig, vfo, 16, 9, &tsinx); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { - rig_debug(RIG_DEBUG_TRACE, "%s: fail1\n", __func__); return retval; } - retval = thd74_get_freq_item(rig, vfo, 33, 1, &fine); - - if (retval != RIG_OK) - { - rig_debug(RIG_DEBUG_TRACE, "%s: fail1\n", __func__); - return retval; - } - - retval = thd74_get_freq_item(rig, vfo, 35, 3, &fine_ts); - - if (retval != RIG_OK) - { - rig_debug(RIG_DEBUG_TRACE, "%s: fail1\n", __func__); - return retval; - } - - rig_debug(RIG_DEBUG_TRACE, "%s: tsinx is %d\n", __func__, tsinx); - rig_debug(RIG_DEBUG_TRACE, "%s: fine is %d\n", __func__, fine); - rig_debug(RIG_DEBUG_TRACE, "%s: fine_ts is %d\n", __func__, fine_ts); - - if (fine > 0) - { - *ts = thd74tuningstep_fine[fine_ts]; - } - else - { - *ts = thd74tuningstep[tsinx]; - } - - rig_debug(RIG_DEBUG_TRACE, "%s: stepsize is %d\n", __func__, (int)*ts); - return RIG_OK; + return thd74_record_ts(&record, ts); } -// needs rig and vfo to get correct stepsize -static int thd74_round_freq(RIG *rig, vfo_t vfo, freq_t freq) +static freq_t thd74_round_freq(freq_t freq, shortfreq_t ts) { - int64_t f; - long double r; - shortfreq_t ts; - // cppcheck-suppress * - char *fmt = "%s: rounded %"PRIll" to %"PRIll" because stepsize:%d\n"; - - rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - - thd74_get_ts(rig, vfo, &ts); - - f = (int64_t)freq; - r = round((double)f / (double)ts); - r = ts * r; - - rig_debug(RIG_DEBUG_TRACE, fmt, __func__, f, (int64_t)r, (int)ts); - - return (freq_t)r; + return (freq_t)(round((double)freq / (double)ts) * ts); } static int thd74_set_freq(RIG *rig, vfo_t vfo, freq_t freq) { struct kenwood_priv_data *priv = STATE(rig)->priv; + struct thd7x_fo_record record; + shortfreq_t ts; int retval; - char buf[128], fbuf[12]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); @@ -407,25 +397,30 @@ static int thd74_set_freq(RIG *rig, vfo_t vfo, freq_t freq) vfo = RIG_VFO_B; } - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - freq = thd74_round_freq(rig, vfo, freq); - SNPRINTF(fbuf, sizeof(fbuf), "%010"PRIll, (int64_t)freq); - memcpy(buf + 5, fbuf, 10); - retval = kenwood_simple_transaction(rig, buf, 72); - return retval; + thd74_record_ts(&record, &ts); + freq = thd74_round_freq(freq, ts); + + if (freq < 0.0 || freq > 9999999999.0) + { + return -RIG_EINVAL; + } + + record.frequency_hz = (uint64_t)llround(freq); + return thd74_push_fo(rig, &record); } static int thd74_get_freq(RIG *rig, vfo_t vfo, freq_t *freq) { struct kenwood_priv_data *priv = STATE(rig)->priv; + struct thd7x_fo_record record; int retval; - char buf[128]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); @@ -434,22 +429,22 @@ static int thd74_get_freq(RIG *rig, vfo_t vfo, freq_t *freq) vfo = RIG_VFO_B; } - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - sscanf(buf + 5, "%"SCNfreq, freq); + *freq = (freq_t)record.frequency_hz; return RIG_OK; } // setting the mode via FO leads to response 'N.' from the handset int thd74_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width) { - char kmode, mdbuf[8], replybuf[8], v; - int retval; + char mdbuf[8], replybuf[8], v; + int kmode, retval; const struct kenwood_priv_caps *priv = (const struct kenwood_priv_caps *) rig->caps->priv; @@ -507,10 +502,10 @@ int thd74_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width) } SNPRINTF(mdbuf, sizeof(mdbuf), "MD %c,%c", v, kmode); - rig_debug(RIG_DEBUG_ERR, "%s: mdbuf: %s\n", __func__, mdbuf); + rig_debug(RIG_DEBUG_TRACE, "%s: mdbuf: %s\n", __func__, mdbuf); retval = kenwood_transaction(rig, mdbuf, replybuf, 7); - rig_debug(RIG_DEBUG_ERR, "%s: retval: %d\n", __func__, retval); + rig_debug(RIG_DEBUG_TRACE, "%s: retval: %d\n", __func__, retval); if (retval != RIG_OK) { @@ -522,135 +517,176 @@ int thd74_set_mode(RIG *rig, vfo_t vfo, rmode_t mode, pbwidth_t width) static int thd74_get_mode(RIG *rig, vfo_t vfo, rmode_t *mode, pbwidth_t *width) { - int retval; - char modec, buf[128]; + char band, command[8], reply[8]; + int consumed, parsed_band, parsed_mode, retval; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_vfoc(rig, vfo, &band); if (retval != RIG_OK) { return retval; } - modec = buf[31]; + SNPRINTF(command, sizeof(command), "MD %c", band); + retval = kenwood_transaction(rig, command, reply, sizeof(reply)); - if (modec >= '0' && modec <= '9') + if (retval != RIG_OK) { - *mode = thd74_mode_table[modec - '0']; - *width = thd74_width_table[modec - '0']; - } - else - { - return -RIG_EINVAL; + return retval; } + consumed = 0; + retval = sscanf(reply, "MD %d,%d%n", &parsed_band, &parsed_mode, &consumed); + + if (retval != 2 || reply[consumed] != '\0' || parsed_band != band - '0' + || parsed_mode < 0 || parsed_mode >= 10 + || thd74_mode_table[parsed_mode] == RIG_MODE_NONE) + { + rig_debug(RIG_DEBUG_ERR, "%s: Unexpected reply '%s'\n", __func__, reply); + return -RIG_EPROTO; + } + + *mode = thd74_mode_table[parsed_mode]; + *width = thd74_width_table[parsed_mode]; return RIG_OK; } static int thd74_set_rptr_shft(RIG *rig, vfo_t vfo, rptr_shift_t rptr_shift) { - int rsinx; + struct thd7x_fo_record record; + int retval; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - switch (rptr_shift) - { - case RIG_RPT_SHIFT_NONE: rsinx = 0; break; - - case RIG_RPT_SHIFT_PLUS: rsinx = 1; break; - - case RIG_RPT_SHIFT_MINUS: rsinx = 2; break; - - default: - return -RIG_EINVAL; - } - - return thd74_set_freq_item(rig, vfo, 47, rsinx); -} - -static int thd74_get_rptr_shft(RIG *rig, vfo_t vfo, rptr_shift_t *rptr_shift) -{ - int retval, rsinx; - - rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - - retval = thd74_get_freq_item(rig, vfo, 47, 3, &rsinx); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - /* rsinx == 3 indicates split mode? */ - *rptr_shift = (rsinx == 3) ? RIG_RPT_SHIFT_NONE : thd74_rshf_table[rsinx]; + switch (rptr_shift) + { + case RIG_RPT_SHIFT_NONE: record.shift = 0; break; + + case RIG_RPT_SHIFT_PLUS: record.shift = 1; break; + + case RIG_RPT_SHIFT_MINUS: record.shift = 2; break; + + default: + return -RIG_EINVAL; + } + + return thd74_push_fo(rig, &record); +} + +static int thd74_get_rptr_shft(RIG *rig, vfo_t vfo, rptr_shift_t *rptr_shift) +{ + struct thd7x_fo_record record; + int retval; + + rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); + + retval = thd74_pull_fo(rig, vfo, &record); + + if (retval != RIG_OK) + { + return retval; + } + + *rptr_shift = record.shift == 3 ? RIG_RPT_SHIFT_NONE : + thd74_rshf_table[record.shift]; return RIG_OK; } static int thd74_set_rptr_offs(RIG *rig, vfo_t vfo, shortfreq_t offs) { + struct thd7x_fo_record record; int retval; - char boff[11], buf[128]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_get_freq_info(rig, vfo, buf); + if (offs < 0) + { + return -RIG_EINVAL; + } + + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - SNPRINTF(boff, sizeof(boff), "%010ld", offs); - memcpy(buf + 16, boff, 10); - retval = kenwood_simple_transaction(rig, buf, 72); - return retval; + record.offset_hz = (uint64_t)offs; + return thd74_push_fo(rig, &record); } static int thd74_get_rptr_offs(RIG *rig, vfo_t vfo, shortfreq_t *offs) { + struct thd7x_fo_record record; int retval; - char buf[128]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - sscanf(buf + 16, "%ld", offs); + if (record.offset_hz > (uint64_t)LONG_MAX) + { + return -RIG_EPROTO; + } + + *offs = (shortfreq_t)record.offset_hz; return RIG_OK; } static int thd74_set_ts(RIG *rig, vfo_t vfo, shortfreq_t ts) { + struct thd7x_fo_record record; + int retval; int tsinx; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); for (tsinx = 0; tsinx < 4; tsinx++) { - if (thd74tuningstep_fine[tsinx] >= ts) + if (thd74tuningstep_fine[tsinx] == ts) { - thd74_set_freq_item(rig, vfo, 33, 1); // Turn fine mode on - thd74_set_freq_item(rig, vfo, 35, tsinx); - return RIG_OK; + retval = thd74_pull_fo(rig, vfo, &record); + + if (retval != RIG_OK) + { + return retval; + } + + record.fine_enabled = 1; + record.fine_step = (uint8_t)tsinx; + return thd74_push_fo(rig, &record); } } - for (tsinx = 0; tsinx < 10; tsinx++) + for (tsinx = 0; tsinx < 12; tsinx++) { - - if (thd74tuningstep[tsinx] >= ts) + if (thd74tuningstep[tsinx] == ts) { - thd74_set_freq_item(rig, vfo, 33, 0); //Turn fine mode off - thd74_set_freq_item(rig, vfo, 27, tsinx); - return RIG_OK; + retval = thd74_pull_fo(rig, vfo, &record); + + if (retval != RIG_OK) + { + return retval; + } + + record.fine_enabled = 0; + record.rx_step = (uint8_t)tsinx; + return thd74_push_fo(rig, &record); } } @@ -659,8 +695,8 @@ static int thd74_set_ts(RIG *rig, vfo_t vfo, shortfreq_t ts) static int thd74_set_ctcss_tone(RIG *rig, vfo_t vfo, tone_t tone) { + struct thd7x_fo_record record; int retval, tinx; - char buf[64], tmp[4]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); @@ -682,49 +718,44 @@ static int thd74_set_ctcss_tone(RIG *rig, vfo_t vfo, tone_t tone) } } - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - buf[22] = (tone == 0) ? '0' : '1'; - SNPRINTF(tmp, sizeof(tmp), "%02d", tinx); - memcpy(buf + 30, tmp, 2); - return kenwood_simple_transaction(rig, buf, 52); + record.tone_enabled = tone != 0; + + if (tone != 0) + { + record.tone_index = (uint8_t)tinx; + } + + return thd74_push_fo(rig, &record); } static int thd74_get_ctcss_tone(RIG *rig, vfo_t vfo, tone_t *tone) { - int retval, tinx; - char buf[64]; + struct thd7x_fo_record record; + int retval; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - if (buf[22] == '0') /* no tone */ + if (!record.tone_enabled) { *tone = 0; } else { - sscanf(buf + 30, "%d", &tinx); - - if (tinx >= 0 && tinx <= 41) - { - *tone = kenwood42_ctcss_list[tinx]; - } - else - { - return -RIG_EINVAL; - } + *tone = kenwood42_ctcss_list[record.tone_index]; } return RIG_OK; @@ -732,8 +763,8 @@ static int thd74_get_ctcss_tone(RIG *rig, vfo_t vfo, tone_t *tone) static int thd74_set_dcs_code(RIG *rig, vfo_t vfo, tone_t code) { + struct thd7x_fo_record record; int retval, cinx; - char buf[64], tmp[4]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); @@ -755,41 +786,44 @@ static int thd74_set_dcs_code(RIG *rig, vfo_t vfo, tone_t code) } } - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - buf[26] = (code == 0) ? '0' : '1'; - SNPRINTF(tmp, sizeof(tmp), "%03d", cinx); - memcpy(buf + 36, tmp, 3); - return kenwood_simple_transaction(rig, buf, 52); + record.dcs_enabled = code != 0; + + if (code != 0) + { + record.dcs_index = (uint8_t)cinx; + } + + return thd74_push_fo(rig, &record); } static int thd74_get_dcs_code(RIG *rig, vfo_t vfo, tone_t *code) { - int retval, cinx; - char buf[64]; + struct thd7x_fo_record record; + int retval; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - if (buf[26] == '0') /* no tone */ + if (!record.dcs_enabled) { *code = 0; } else { - sscanf(buf + 36, "%d", &cinx); - *code = thd74dcs_list[cinx]; + *code = thd74dcs_list[record.dcs_index]; } return RIG_OK; @@ -797,8 +831,8 @@ static int thd74_get_dcs_code(RIG *rig, vfo_t vfo, tone_t *code) static int thd74_set_ctcss_sql(RIG *rig, vfo_t vfo, tone_t tone) { + struct thd7x_fo_record record; int retval, tinx; - char buf[64], tmp[4]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); @@ -820,49 +854,44 @@ static int thd74_set_ctcss_sql(RIG *rig, vfo_t vfo, tone_t tone) } } - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - buf[24] = (tone == 0) ? '0' : '1'; - SNPRINTF(tmp, sizeof(tmp), "%02d", tinx); - memcpy(buf + 33, tmp, 2); - return kenwood_simple_transaction(rig, buf, 52); + record.ctcss_enabled = tone != 0; + + if (tone != 0) + { + record.ctcss_index = (uint8_t)tinx; + } + + return thd74_push_fo(rig, &record); } static int thd74_get_ctcss_sql(RIG *rig, vfo_t vfo, tone_t *tone) { - int retval, tinx; - char buf[64]; + struct thd7x_fo_record record; + int retval; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - if (buf[24] == '0') /* no tsql */ + if (!record.ctcss_enabled) { *tone = 0; } else { - sscanf(buf + 33, "%d", &tinx); - - if (tinx >= 0 && tinx <= 41) - { - *tone = kenwood42_ctcss_list[tinx]; - } - else - { - return -RIG_EINVAL; - } + *tone = kenwood42_ctcss_list[record.ctcss_index]; } return RIG_OK; @@ -931,11 +960,17 @@ static int thd74_set_level(RIG *rig, vfo_t vfo, setting_t level, value_t val) return kenwood_simple_transaction(rig, cmd, 4); case RIG_LEVEL_SQL: - SNPRINTF(cmd, sizeof(cmd), "SQ %c,%d", c, (int)val.f); + if (val.f < 0.0 || val.f > 1.0) + { + return -RIG_EINVAL; + } + + lvl = (int)round(val.f * 5.0); + SNPRINTF(cmd, sizeof(cmd), "SQ %c,%d", c, lvl); return kenwood_simple_transaction(rig, cmd, 6); - case RIG_LEVEL_ATT: // no value provided to distinguish between on/off?? - SNPRINTF(cmd, sizeof(cmd), "RA %c,%d", c, (int)val.f); + case RIG_LEVEL_ATT: + SNPRINTF(cmd, sizeof(cmd), "RA %c,%d", c, val.i ? 1 : 0); return kenwood_simple_transaction(rig, cmd, 6); default: @@ -1043,14 +1078,22 @@ static int thd74_get_level(RIG *rig, vfo_t vfo, setting_t level, value_t *val) case RIG_LEVEL_ATT: SNPRINTF(cmd, sizeof(cmd), "RA %c", c); - retval = kenwood_transaction(rig, cmd, buf, 7); + retval = kenwood_transaction(rig, cmd, buf, sizeof(buf)); if (retval != RIG_OK) { return retval; } - sscanf(buf + 5, "%d", &val->i); + retval = sscanf(buf, "RA %d,%d", &v, &l); + + if (retval != 2 || l < 0 || l > 1) + { + rig_debug(RIG_DEBUG_ERR, "%s: Unexpected reply '%s'\n", __func__, buf); + return -RIG_ERJCTED; + } + + val->i = l; break; default: @@ -1064,49 +1107,68 @@ static int thd74_get_level(RIG *rig, vfo_t vfo, setting_t level, value_t *val) static int thd74_set_func(RIG *rig, vfo_t vfo, setting_t func, int status) { - rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - - switch (func) - { - case RIG_FUNC_TONE: - return thd74_set_freq_item(rig, vfo, 37, status); - - case RIG_FUNC_TSQL: - return thd74_set_freq_item(rig, vfo, 39, status); - - default: - return -RIG_EINVAL; - } - - return RIG_OK; -} - -static int thd74_get_func(RIG *rig, vfo_t vfo, setting_t func, int *status) -{ - int retval, f; + struct thd7x_fo_record record; + int retval; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); - switch (func) + if (status != 0 && status != 1) { - case RIG_FUNC_TONE: - retval = thd74_get_freq_item(rig, vfo, 37, 1, &f); - break; - - case RIG_FUNC_TSQL: - retval = thd74_get_freq_item(rig, vfo, 39, 1, &f); - break; - - default: return -RIG_EINVAL; } + retval = thd74_pull_fo(rig, vfo, &record); + if (retval != RIG_OK) { return retval; } - *status = f; + switch (func) + { + case RIG_FUNC_TONE: + record.tone_enabled = (uint8_t)status; + break; + + case RIG_FUNC_TSQL: + record.ctcss_enabled = (uint8_t)status; + break; + + default: + return -RIG_EINVAL; + } + + return thd74_push_fo(rig, &record); +} + +static int thd74_get_func(RIG *rig, vfo_t vfo, setting_t func, int *status) +{ + struct thd7x_fo_record record; + int retval; + + rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); + + retval = thd74_pull_fo(rig, vfo, &record); + + if (retval != RIG_OK) + { + return retval; + } + + switch (func) + { + case RIG_FUNC_TONE: + *status = record.tone_enabled; + break; + + case RIG_FUNC_TSQL: + *status = record.ctcss_enabled; + break; + + default: + return -RIG_EINVAL; + } + return RIG_OK; } @@ -1159,6 +1221,11 @@ static int thd74_set_mem(RIG *rig, vfo_t vfo, int ch) rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); + if (ch < 0 || ch > 999) + { + return -RIG_EINVAL; + } + retval = thd74_vfoc(rig, vfo, &c); if (retval != RIG_OK) @@ -1172,7 +1239,7 @@ static int thd74_set_mem(RIG *rig, vfo_t vfo, int ch) static int thd74_get_mem(RIG *rig, vfo_t vfo, int *ch) { - int retval; + int consumed, parsed_band, retval; char c, cmd[10], buf[10]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); @@ -1192,7 +1259,16 @@ static int thd74_get_mem(RIG *rig, vfo_t vfo, int *ch) return retval; } - sscanf(buf + 3, "%d", ch); + consumed = 0; + retval = sscanf(buf, "MR %d,%d%n", &parsed_band, ch, &consumed); + + if (retval != 2 || buf[consumed] != '\0' || parsed_band != c - '0' + || *ch < 0 || *ch > 999) + { + rig_debug(RIG_DEBUG_ERR, "%s: Unexpected reply '%s'\n", __func__, buf); + return -RIG_EPROTO; + } + return RIG_OK; } @@ -1203,97 +1279,172 @@ static int thd74_set_channel(RIG *rig, vfo_t vfo, const channel_t *chan) return -RIG_EINVAL; } -static int thd74_parse_channel(int kind, const char *buf, channel_t *chan) +static void thd74_channel_tones(channel_t *chan, uint8_t tone_enabled, + uint8_t ctcss_enabled, uint8_t dcs_enabled, + uint8_t cross_enabled, uint8_t tone_index, + uint8_t ctcss_index, uint8_t dcs_index, + uint8_t cross_selector) { - int tmp; - char c; - const char *data; + chan->ctcss_tone = 0; + chan->ctcss_sql = 0; + chan->dcs_code = 0; + chan->dcs_sql = 0; - if (kind == 0) { data = buf + 5; } - else { data = buf + 7; } - - sscanf(data, "%"SCNfreq, &chan->freq); - c = data[46]; // mode - - if (c >= '0' && c <= '2') + if (cross_enabled) { - chan->mode = thd74_mode_table[c - '0']; - chan->width = thd74_width_table[c - '0']; - } - - c = data[11]; // tuning step - - if (c >= '0' && c <= '9') - { - chan->tuning_step = thd74tuningstep[c - '0']; - } - - c = data[13]; // repeater shift - - if (c >= '0' && c <= '2') - { - chan->rptr_shift = thd74_rshf_table[c - '0']; - } - - sscanf(data + 37, "%ld", &chan->rptr_offs); - c = data[17]; // Tone status - - if (c != '0') - { - sscanf(data + 25, "%d", &tmp); - - if (tmp > 0 && tmp < 42) + switch (cross_selector) { - chan->ctcss_tone = kenwood42_ctcss_list[tmp]; + case 0: + chan->dcs_code = thd74dcs_list[dcs_index]; + break; + + case 1: + chan->ctcss_tone = kenwood42_ctcss_list[tone_index]; + chan->dcs_sql = thd74dcs_list[dcs_index]; + break; + + case 2: + chan->dcs_code = thd74dcs_list[dcs_index]; + chan->ctcss_sql = kenwood42_ctcss_list[ctcss_index]; + break; + + case 3: + chan->ctcss_tone = kenwood42_ctcss_list[tone_index]; + chan->ctcss_sql = kenwood42_ctcss_list[ctcss_index]; + break; } + + return; + } + + if (tone_enabled) + { + chan->ctcss_tone = kenwood42_ctcss_list[tone_index]; + } + + if (ctcss_enabled) + { + chan->ctcss_sql = kenwood42_ctcss_list[ctcss_index]; + } + + if (dcs_enabled) + { + chan->dcs_code = thd74dcs_list[dcs_index]; + chan->dcs_sql = thd74dcs_list[dcs_index]; + } +} + +static int thd74_channel_mode(channel_t *chan, uint8_t mode) +{ + if (mode >= 10 || thd74_mode_table[mode] == RIG_MODE_NONE) + { + return -RIG_EPROTO; + } + + chan->mode = thd74_mode_table[mode]; + chan->width = thd74_width_table[mode]; + chan->tx_mode = chan->mode; + chan->tx_width = chan->width; + return RIG_OK; +} + +static int thd74_channel_from_fo(const struct thd7x_fo_record *record, + channel_t *chan) +{ + int retval; + + if (record->offset_hz > (uint64_t)LONG_MAX) + { + return -RIG_EPROTO; + } + + retval = thd74_channel_mode(chan, record->mode); + + if (retval != RIG_OK) + { + return retval; + } + + chan->freq = (freq_t)record->frequency_hz; + chan->tx_freq = 0; + chan->split = RIG_SPLIT_OFF; + chan->rptr_shift = record->shift == 3 ? RIG_RPT_SHIFT_NONE : + thd74_rshf_table[record->shift]; + chan->rptr_offs = (shortfreq_t)record->offset_hz; + chan->tuning_step = record->fine_enabled ? + thd74tuningstep_fine[record->fine_step] : + thd74tuningstep[record->rx_step]; + chan->funcs = record->reverse_enabled ? RIG_FUNC_REV : 0; + chan->flags = RIG_CHFLAG_NONE; + chan->channel_desc[0] = '\0'; + thd74_channel_tones(chan, record->tone_enabled, record->ctcss_enabled, + record->dcs_enabled, record->cross_enabled, + record->tone_index, record->ctcss_index, + record->dcs_index, record->cross_selector); + return RIG_OK; +} + +static int thd74_channel_from_me(const struct thd7x_me_record *record, + channel_t *chan) +{ + int retval; + + if (record->offset_hz > (uint64_t)LONG_MAX) + { + return -RIG_EPROTO; + } + + retval = thd74_channel_mode(chan, record->mode); + + if (retval != RIG_OK) + { + return retval; + } + + chan->channel_num = record->channel; + chan->freq = (freq_t)record->frequency_hz; + chan->tuning_step = record->fine_enabled ? + thd74tuningstep_fine[record->fine_step] : + thd74tuningstep[record->rx_step]; + chan->funcs = record->reverse_enabled ? RIG_FUNC_REV : 0; + chan->flags = record->lockout_enabled ? RIG_CHFLAG_SKIP : RIG_CHFLAG_NONE; + chan->channel_desc[0] = '\0'; + + if (record->odd_split_enabled) + { + chan->split = RIG_SPLIT_ON; + chan->tx_freq = (freq_t)record->offset_hz; + chan->rptr_shift = RIG_RPT_SHIFT_NONE; + chan->rptr_offs = 0; } else { - chan->ctcss_tone = 0; - } - - c = data[19]; // TSQL status - - if (c != '0') - { - sscanf(data + 28, "%d", &tmp); - - if (tmp > 0 && tmp < 42) - { - chan->ctcss_sql = kenwood42_ctcss_list[tmp]; - } - } - else - { - chan->ctcss_sql = 0; - } - - c = data[21]; // DCS status - - if (c != '0') - { - sscanf(data + 31, "%d", &tmp); - chan->dcs_code = tmp; - } - else - { - chan->dcs_code = 0; + chan->split = RIG_SPLIT_OFF; + chan->tx_freq = 0; + chan->rptr_shift = record->shift == 3 ? RIG_RPT_SHIFT_NONE : + thd74_rshf_table[record->shift]; + chan->rptr_offs = (shortfreq_t)record->offset_hz; } + thd74_channel_tones(chan, record->tone_enabled, record->ctcss_enabled, + record->dcs_enabled, record->cross_enabled, + record->tone_index, record->ctcss_index, + record->dcs_index, record->cross_selector); return RIG_OK; } static int thd74_get_channel(RIG *rig, vfo_t vfo, channel_t *chan, int read_only) { + struct thd7x_fo_record fo_record; + struct thd7x_me_record me_record; int retval; - char buf[72]; + char buf[THD7X_COMMAND_BUFSIZE]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); if (chan->vfo == RIG_VFO_MEM) /* memory channel */ { - int len; char cmd[16]; SNPRINTF(cmd, sizeof(cmd), "ME %03d", chan->channel_num); retval = kenwood_transaction(rig, cmd, buf, sizeof(buf)); @@ -1303,34 +1454,31 @@ static int thd74_get_channel(RIG *rig, vfo_t vfo, channel_t *chan, return retval; } - retval = thd74_parse_channel(1, buf, chan); + retval = thd7x_parse_me(buf, strlen(buf), &me_record); + + if (retval != RIG_OK || me_record.channel != chan->channel_num) + { + rig_debug(RIG_DEBUG_ERR, "%s: Unexpected reply '%s'\n", __func__, buf); + return -RIG_EPROTO; + } + + retval = thd74_channel_from_me(&me_record, chan); if (retval != RIG_OK) { return retval; } - - cmd[1] = 'N'; /* change ME to MN */ - retval = kenwood_transaction(rig, cmd, buf, sizeof(buf)); - - if (retval != RIG_OK) - { - return retval; - } - - len = strlen(buf); - memcpy(chan->channel_desc, buf + 7, len - 7); } else /* current channel */ { - retval = thd74_get_freq_info(rig, chan->vfo, buf); + retval = thd74_pull_fo(rig, chan->vfo, &fo_record); if (retval != RIG_OK) { return retval; } - return thd74_parse_channel(0, buf, chan); + return thd74_channel_from_fo(&fo_record, chan); } if (!read_only) @@ -1387,8 +1535,8 @@ otherwise return -RIG_EPROTO int thd74_get_split_freq(RIG *rig, vfo_t vfo, freq_t *tx_freq) { struct kenwood_priv_data *priv = STATE(rig)->priv; + struct thd7x_fo_record record; int retval; - char buf[128]; rig_debug(RIG_DEBUG_TRACE, "%s: called\n", __func__); @@ -1401,14 +1549,14 @@ int thd74_get_split_freq(RIG *rig, vfo_t vfo, freq_t *tx_freq) return -RIG_EINVAL; } - retval = thd74_get_freq_info(rig, vfo, buf); + retval = thd74_pull_fo(rig, vfo, &record); if (retval != RIG_OK) { return retval; } - sscanf(buf + 5, "%"SCNfreq, tx_freq); + *tx_freq = (freq_t)record.frequency_hz; return RIG_OK; } @@ -1424,18 +1572,25 @@ int thd74_set_split_freq(RIG *rig, vfo_t vfo, freq_t tx_freq) if (priv->split == RIG_SPLIT_ON) { - char fbuf[12], buf[128]; - int retval = thd74_get_freq_info(rig, RIG_VFO_A, buf); + struct thd7x_fo_record record; + shortfreq_t ts; + int retval = thd74_pull_fo(rig, RIG_VFO_A, &record); if (retval != RIG_OK) { return retval; } - tx_freq = thd74_round_freq(rig, RIG_VFO_A, tx_freq); - SNPRINTF(fbuf, sizeof(fbuf), "%010"PRIll, (int64_t)tx_freq); - memcpy(buf + 5, fbuf, 10); - return kenwood_simple_transaction(rig, buf, 72); + thd74_record_ts(&record, &ts); + tx_freq = thd74_round_freq(tx_freq, ts); + + if (tx_freq < 0.0 || tx_freq > 9999999999.0) + { + return -RIG_EINVAL; + } + + record.frequency_hz = (uint64_t)llround(tx_freq); + return thd74_push_fo(rig, &record); } return -RIG_EPROTO; @@ -1776,3 +1931,160 @@ struct rig_caps thd74_caps = .get_info = th_get_info, .hamlib_check_rig_caps = HAMLIB_CHECK_RIG_CAPS }; + +struct rig_caps thd75_caps = +{ + RIG_MODEL(RIG_MODEL_THD75), + .model_name = "TH-D75", + .mfg_name = "Kenwood", + .version = BACKEND_VER ".1", + .copyright = "LGPL", + .status = RIG_STATUS_BETA, + .rig_type = RIG_TYPE_HANDHELD | RIG_FLAG_APRS | RIG_FLAG_TNC | RIG_FLAG_DXCLUSTER, + .ptt_type = RIG_PTT_RIG, + .dcd_type = RIG_DCD_NONE, + .port_type = RIG_PORT_SERIAL, + .serial_rate_min = 9600, + .serial_rate_max = 9600, + .serial_data_bits = 8, + .serial_stop_bits = 1, + .serial_parity = RIG_PARITY_NONE, + .serial_handshake = RIG_HANDSHAKE_NONE, + .write_delay = 0, + .post_write_delay = 0, + .timeout = 500, + .retry = 3, + + .has_get_func = THD74_FUNC_ALL, + .has_set_func = THD74_FUNC_ALL, + .has_get_level = THD75_LEVEL_ALL, + .has_set_level = RIG_LEVEL_SET(THD75_LEVEL_ALL), + .level_gran = + { + [LVL_SQL] = { + .min = { .f = 0.0f }, + .max = { .f = 1.0f }, + .step = { .f = 0.2f }, + }, + [LVL_RFPOWER] = { + .min = { .f = 0.01f }, + .max = { .f = 1.0f }, + .step = { .f = 0.0f }, + }, + }, + + .ctcss_list = kenwood42_ctcss_list, + .dcs_list = thd74dcs_list, + .preamp = { RIG_DBLST_END, }, + .attenuator = { RIG_DBLST_END, }, + .max_rit = Hz(0), + .max_xit = Hz(0), + .max_ifshift = Hz(0), + .vfo_ops = THD74_VFO_OP, + .targetable_vfo = RIG_TARGETABLE_FREQ, + .transceive = RIG_TRN_OFF, + .bank_qty = 0, + .chan_desc_sz = 0, + .chan_list = + { + { 0, 999, RIG_MTYPE_MEM, {THD75_CHANNEL_CAPS}}, + RIG_CHAN_END, + }, + .rx_range_list1 = + { + {MHz(136), MHz(174), THD75_BAND_A_MODES, -1, -1, RIG_VFO_A}, + {MHz(410), MHz(470), THD75_BAND_A_MODES, -1, -1, RIG_VFO_A}, + {kHz(100), MHz(76), THD75_BAND_B_MODES, -1, -1, RIG_VFO_B}, + {MHz(76), MHz(108), RIG_MODE_WFM, -1, -1, RIG_VFO_B}, + {MHz(108), MHz(524), THD75_BAND_B_MODES, -1, -1, RIG_VFO_B}, + RIG_FRNG_END, + }, + .tx_range_list1 = + { + {MHz(144), MHz(146), THD75_MODES_TX, W(0.05), W(5), RIG_VFO_A}, + {MHz(430), MHz(440), THD75_MODES_TX, W(0.05), W(5), RIG_VFO_A}, + RIG_FRNG_END, + }, + .rx_range_list2 = + { + {MHz(136), MHz(174), THD75_BAND_A_MODES, -1, -1, RIG_VFO_A}, + {MHz(216), MHz(260), THD75_BAND_A_MODES, -1, -1, RIG_VFO_A}, + {MHz(410), MHz(470), THD75_BAND_A_MODES, -1, -1, RIG_VFO_A}, + {kHz(100), MHz(76), THD75_BAND_B_MODES, -1, -1, RIG_VFO_B}, + {MHz(76), MHz(108), RIG_MODE_WFM, -1, -1, RIG_VFO_B}, + {MHz(108), MHz(524), THD75_BAND_B_MODES, -1, -1, RIG_VFO_B}, + RIG_FRNG_END, + }, + .tx_range_list2 = + { + {MHz(144), MHz(148), THD75_MODES_TX, W(0.05), W(5), RIG_VFO_A}, + {MHz(222), MHz(225), THD75_MODES_TX, W(0.05), W(5), RIG_VFO_A}, + {MHz(430), MHz(450), THD75_MODES_TX, W(0.05), W(5), RIG_VFO_A}, + RIG_FRNG_END, + }, + + .tuning_steps = + { + {THD75_BAND_B_MODES, Hz(20)}, + {THD75_BAND_B_MODES, Hz(100)}, + {THD75_BAND_B_MODES, Hz(500)}, + {THD75_BAND_B_MODES, kHz(1)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(5)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(6.25)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, Hz(8330)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(9)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(10)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(12.5)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(15)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(20)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(25)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(30)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(50)}, + {THD75_BAND_A_MODES | THD75_BAND_B_MODES, kHz(100)}, + RIG_TS_END, + }, + .filters = + { + {RIG_MODE_FM, kHz(14)}, + {RIG_MODE_FMN, kHz(7)}, + {RIG_MODE_DSTAR, kHz(6)}, + {RIG_MODE_AM, kHz(9)}, + {RIG_MODE_LSB | RIG_MODE_USB, Hz(2700)}, + {RIG_MODE_CW | RIG_MODE_CWR, Hz(500)}, + {RIG_MODE_WFM, kHz(150)}, + RIG_FLT_END, + }, + .priv = (void *)&thd74_priv_caps, + + .rig_init = kenwood_init, + .rig_cleanup = kenwood_cleanup, + .rig_open = kenwood_open, + .set_freq = thd74_set_freq, + .get_freq = thd74_get_freq, + .set_mode = thd74_set_mode, + .get_mode = thd74_get_mode, + .set_vfo = thd74_set_vfo, + .get_vfo = thd74_get_vfo, + .set_ptt = thd74_set_ptt, + .set_rptr_shift = thd74_set_rptr_shft, + .get_rptr_shift = thd74_get_rptr_shft, + .set_rptr_offs = thd74_set_rptr_offs, + .get_rptr_offs = thd74_get_rptr_offs, + .set_ts = thd74_set_ts, + .get_ts = thd74_get_ts, + .set_ctcss_tone = thd74_set_ctcss_tone, + .get_ctcss_tone = thd74_get_ctcss_tone, + .set_dcs_code = thd74_set_dcs_code, + .get_dcs_code = thd74_get_dcs_code, + .set_ctcss_sql = thd74_set_ctcss_sql, + .get_ctcss_sql = thd74_get_ctcss_sql, + .set_level = thd74_set_level, + .get_level = thd74_get_level, + .set_func = thd74_set_func, + .get_func = thd74_get_func, + .set_mem = thd74_set_mem, + .get_mem = thd74_get_mem, + .get_channel = thd74_get_channel, + .get_info = th_get_info, + .hamlib_check_rig_caps = HAMLIB_CHECK_RIG_CAPS +}; diff --git a/rigs/kenwood/thd7x.c b/rigs/kenwood/thd7x.c new file mode 100644 index 000000000..b7adb3bf4 --- /dev/null +++ b/rigs/kenwood/thd7x.c @@ -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 +#include +#include + +#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; +} diff --git a/rigs/kenwood/thd7x.h b/rigs/kenwood/thd7x.h new file mode 100644 index 000000000..1c0796c26 --- /dev/null +++ b/rigs/kenwood/thd7x.h @@ -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 +#include + +#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 diff --git a/rigs/kenwood/ts2000.c b/rigs/kenwood/ts2000.c index 68ac93dee..32ce19df5 100644 --- a/rigs/kenwood/ts2000.c +++ b/rigs/kenwood/ts2000.c @@ -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; diff --git a/rigs/kenwood/ts590.c b/rigs/kenwood/ts590.c index c8ffe3634..b7fa51bd8 100644 --- a/rigs/kenwood/ts590.c +++ b/rigs/kenwood/ts590.c @@ -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; diff --git a/rigs/kenwood/tx500.c b/rigs/kenwood/tx500.c index cb3dc7450..74c3e8601 100644 --- a/rigs/kenwood/tx500.c +++ b/rigs/kenwood/tx500.c @@ -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; diff --git a/tests/Makefile.am b/tests/Makefile.am index 647af4d0e..db6d927e1 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -24,7 +24,8 @@ DIRECT_TESTS = \ testftx1parsers \ testgeministatus \ testgs100 \ - testicomts + testicomts \ + testthd7x GENERATED_TEST_WRAPPERS = \ test2038.sh \ diff --git a/tests/testthd7x.c b/tests/testthd7x.c new file mode 100644 index 000000000..d81e961de --- /dev/null +++ b/tests/testthd7x.c @@ -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 +#include +#include + +#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; +}