diff --git a/Makefile b/Makefile index 1a002ce..b10328f 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,20 @@ OBJS = miniaudio.o # defualt to build with UI, headless operations through --headless UI_FLAGS = -DWITH_UI +# Optional CM108 PTT support requires libhidapi-dev +HIDAPI_CFLAGS := $(shell pkg-config --cflags hidapi-hidraw 2>/dev/null || pkg-config --cflags hidapi-libusb 2>/dev/null || pkg-config --cflags hidapi 2>/dev/null) +HIDAPI_LIBS := $(shell pkg-config --libs hidapi-hidraw 2>/dev/null || pkg-config --libs hidapi-libusb 2>/dev/null || pkg-config --libs hidapi 2>/dev/null) + +ifneq ($(HIDAPI_LIBS),) + $(info CM108 PTT support: enabled (found hidapi)) + CM108_FLAGS = -DWITH_CM108 + CXXFLAGS += $(HIDAPI_CFLAGS) + LDFLAGS += $(HIDAPI_LIBS) +else + $(info CM108 PTT support: disabled (install libhidapi-dev to enable)) + CM108_FLAGS = +endif + .PHONY: all clean install debug help all: $(TARGET) @@ -27,13 +41,25 @@ miniaudio.o: miniaudio.c miniaudio.h $(CC) -c -O2 -o $@ miniaudio.c $(TARGET): $(SRCS) $(HDRS) $(OBJS) - $(CXX) $(CXXFLAGS) $(UI_FLAGS) $(INCLUDES) -o $@ $(SRCS) $(OBJS) $(LDFLAGS) + $(CXX) $(CXXFLAGS) $(UI_FLAGS) $(CM108_FLAGS) $(INCLUDES) -o $@ $(SRCS) $(OBJS) $(LDFLAGS) +ifneq ($(HIDAPI_LIBS),) + @echo "" + @echo "CM108 PTT support enabled. To allow non-root access, install udev rules:" + @echo " sudo cp misc/50-cm108-ptt.rules /etc/udev/rules.d/" + @echo " sudo udevadm control --reload-rules" +endif clean: rm -f $(TARGET) $(OBJS) install: $(TARGET) install -m 755 $(TARGET) /usr/local/bin/ +ifneq ($(HIDAPI_LIBS),) + @if [ -f misc/50-cm108-ptt.rules ]; then \ + cp misc/50-cm108-ptt.rules /etc/udev/rules.d/ 2>/dev/null || \ + echo "Note: Run 'sudo cp misc/50-cm108-ptt.rules /etc/udev/rules.d/' for CM108 udev rules"; \ + fi +endif # Debug build debug: CXXFLAGS = -std=c++17 -g -O0 -Wall -Wextra -DDEBUG @@ -54,6 +80,9 @@ help: @echo " AICODIX_CODE - Path to aicodix/code (default: ../code)" @echo " MODEM_SRC - Path to modem source (default: ../modem)" @echo "" + @echo "Optional features:" + @echo " CM108 PTT - Requires libhidapi-dev (auto-detected)" + @echo "" @echo "Example:" @echo " make AICODIX_DSP=~/aicodix/dsp AICODIX_CODE=~/aicodix/code" @echo "" diff --git a/README.md b/README.md index 2a75c23..bcd786a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,17 @@ MODEM73 is a [KISS](https://en.wikipedia.org/wiki/KISS_(amateur_radio_protocol)) sudo apt install git build-essential libncurses-dev g++ ``` +#### Optional Addons + + +##### CM108 USB PTT Support + +CM108-based USB audio interfaces have GPIO pins that can be used for PTT control. To enable CM108 support, install libhidapi-dev before building. The Makefile will auto-detect it and enable the feature. +``` +# Debian/Ubuntu/Pi - install before building +sudo apt install libhidapi-dev +``` +---- 2. Clone aiocdix DSP libraries and build. @@ -64,6 +75,7 @@ There are currently four PTT options: - Rigctl - VOX - Serial +- CM108 ``` @@ -97,6 +109,11 @@ while running `rigctld` ./modem73 --ptt com --com-port /dev/ttyUSB0 --com-line rts ``` +``` +# CM108 USB audio interface PTT (GPIO3 is default) +./modem73 --ptt cm108 --cm108-gpio 3 +``` + ## Updating diff --git a/cm108_ptt.hh b/cm108_ptt.hh new file mode 100644 index 0000000..9c65a0b --- /dev/null +++ b/cm108_ptt.hh @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +class CM108PTT { +public: + CM108PTT() = default; + + ~CM108PTT() { + close(); + } + + bool open(const int gpio){ + res_ = hid_init(); + gpio_ = gpio; + handle_ = hid_open(0x0D8C, 0x013C, NULL); + if (!handle_) { + std::cerr << "Failed to open CM108 PTT via USB" << std::endl; + hid_exit(); + return false; + } + return true; + } + + void close(){ + if (handle_) { + hid_close(handle_); + handle_ = nullptr; + } + hid_exit(); + } + + void set_ptt(bool on){ + if (!handle_) return; + + unsigned char buf[5]; + buf[0] = 0x00; + buf[1] = 0x00; + + if (on){ + buf[2] = cm108_on_[gpio_-1]; + buf[3] = cm108_on_[gpio_-1]; + } else { + buf[2] = 0x00; + buf[3] = 0x00; + } + buf[4] = 0x00; + + res_ = hid_write(handle_, buf, 5); + } + +private: + int res_ = 0; + int gpio_ = 3; // PTT control pin GPIOX, where X should be 1,2,3,4 - GPIO3 on most devices + const int cm108_on_[4] = {0x01, 0x02, 0x04, 0x08}; + hid_device *handle_ = nullptr; +}; diff --git a/kiss_tnc.cc b/kiss_tnc.cc index 411065f..a8b6ea9 100644 --- a/kiss_tnc.cc +++ b/kiss_tnc.cc @@ -30,6 +30,9 @@ #include "miniaudio_audio.hh" #include "rigctl_ptt.hh" #include "serial_ptt.hh" +#ifdef WITH_CM108 +#include "cm108_ptt.hh" +#endif #include "modem.hh" #ifdef WITH_UI @@ -184,6 +187,11 @@ public: config_.com_invert_rts)) { std::cerr << "Could not open COM port: " << serial_ptt_->last_error() << std::endl; } +#ifdef WITH_CM108 + } else if (config_.ptt_type == PTTType::CM108) { + cm108_ptt_ = std::make_unique(); + cm108_ptt_->open(config_.cm108_gpio); +#endif } else { dummy_ptt_ = std::make_unique(); dummy_ptt_->connect(); @@ -246,6 +254,11 @@ public: std::cerr << "PTT: COM " << config_.com_port << " (" << PTT_LINE_OPTIONS[config_.com_ptt_line] << ")" << std::endl; break; +#ifdef WITH_CM108 + case PTTType::CM108: + std::cerr << "PTT: CM108 (GPIO" << config_.cm108_gpio << ")" << std::endl; + break; +#endif } // Start threads @@ -592,7 +605,11 @@ private: std::to_string(duration) + " seconds"); // PTT on (for RIGCTL or COM mode) - if (config_.ptt_type == PTTType::RIGCTL || config_.ptt_type == PTTType::COM) { + if (config_.ptt_type == PTTType::RIGCTL || config_.ptt_type == PTTType::COM +#ifdef WITH_CM108 + || config_.ptt_type == PTTType::CM108 +#endif + ) { set_ptt(true); std::this_thread::sleep_for(std::chrono::milliseconds(config_.ptt_delay_ms)); } @@ -612,7 +629,11 @@ private: audio_->drain_playback(); // PTT off - if (config_.ptt_type == PTTType::RIGCTL || config_.ptt_type == PTTType::COM) { + if (config_.ptt_type == PTTType::RIGCTL || config_.ptt_type == PTTType::COM +#ifdef WITH_CM108 + || config_.ptt_type == PTTType::CM108 +#endif + ) { std::this_thread::sleep_for(std::chrono::milliseconds(config_.ptt_tail_ms)); set_ptt(false); } @@ -746,6 +767,10 @@ private: } else { serial_ptt_->ptt_off(); } +#ifdef WITH_CM108 + } else if (cm108_ptt_) { + cm108_ptt_->set_ptt(on); +#endif } else if (dummy_ptt_) { dummy_ptt_->set_ptt(on); } @@ -799,6 +824,9 @@ private: std::unique_ptr audio_; std::unique_ptr rigctl_; std::unique_ptr serial_ptt_; +#ifdef WITH_CM108 + std::unique_ptr cm108_ptt_; +#endif std::unique_ptr dummy_ptt_; int server_fd_ = -1; @@ -922,11 +950,20 @@ void print_help(const char* prog) { << " --short Use short frames\n" << " --normal Use normal frames (default)\n" << "\nPTT options:\n" - << " --ptt TYPE PTT type: none, rigctl, vox (default: rigctl)\n" + << " --ptt TYPE PTT type: none, rigctl, vox, com" +#ifdef WITH_CM108 + << ", cm108" +#endif + << " (default: rigctl)\n" << " --rigctl HOST:PORT Rigctl address (default: localhost:4532)\n" + << " --com-port PORT Serial port for COM PTT (default: /dev/ttyUSB0)\n" + << " --com-line LINE COM PTT line: dtr, rts, both (default: rts)\n" << " --vox-freq HZ VOX tone frequency (default: 1200)\n" << " --vox-lead MS VOX lead time in ms (default: 150)\n" << " --vox-tail MS VOX tail time in ms (default: 100)\n" +#ifdef WITH_CM108 + << " --cm108-gpio N CM108 GPIO pin for PTT (default: 3)\n" +#endif << " --ptt-delay MS PTT delay before TX (default: 50)\n" << " --ptt-tail MS PTT tail after TX (default: 50)\n" << "\nCSMA options:\n" @@ -1006,13 +1043,32 @@ int main(int argc, char** argv) { } else { config.rigctl_host = hostport; } + } else if (arg == "--com-port" && i + 1 < argc) { + config.com_port = argv[++i]; + } else if (arg == "--com-line" && i + 1 < argc) { + std::string line = argv[++i]; + if (line == "dtr") config.com_ptt_line = 0; + else if (line == "rts") config.com_ptt_line = 1; + else if (line == "both") config.com_ptt_line = 2; + else { + std::cerr << "Unknown COM PTT line: " << line << " (use dtr, rts, or both)\n"; + return 1; + } } else if (arg == "--ptt" && i + 1 < argc) { std::string ptt_type = argv[++i]; if (ptt_type == "none") config.ptt_type = PTTType::NONE; else if (ptt_type == "rigctl") config.ptt_type = PTTType::RIGCTL; else if (ptt_type == "vox") config.ptt_type = PTTType::VOX; + else if (ptt_type == "com") config.ptt_type = PTTType::COM; +#ifdef WITH_CM108 + else if (ptt_type == "cm108") config.ptt_type = PTTType::CM108; +#endif else { - std::cerr << "Unknown PTT type: " << ptt_type << " (use none, rigctl, or vox)\n"; + std::cerr << "Unknown PTT type: " << ptt_type << " (use none, rigctl, vox, com" +#ifdef WITH_CM108 + << ", cm108" +#endif + << ")\n"; return 1; } } else if (arg == "--vox-freq" && i + 1 < argc) { @@ -1021,6 +1077,10 @@ int main(int argc, char** argv) { config.vox_lead_ms = std::atoi(argv[++i]); } else if (arg == "--vox-tail" && i + 1 < argc) { config.vox_tail_ms = std::atoi(argv[++i]); +#ifdef WITH_CM108 + } else if (arg == "--cm108-gpio" && i + 1 < argc) { + config.cm108_gpio = std::atoi(argv[++i]); +#endif } else if (arg == "--ptt-delay" && i + 1 < argc) { config.ptt_delay_ms = std::atoi(argv[++i]); } else if (arg == "--ptt-tail" && i + 1 < argc) { diff --git a/kiss_tnc.hh b/kiss_tnc.hh index 8e02af1..945646a 100644 --- a/kiss_tnc.hh +++ b/kiss_tnc.hh @@ -36,7 +36,10 @@ enum class PTTType { NONE = 0, RIGCTL = 1, VOX = 2, - COM = 3 + COM = 3, +#ifdef WITH_CM108 + CM108 = 4 +#endif }; struct TNCConfig { @@ -73,6 +76,11 @@ struct TNCConfig { int com_ptt_line = 1; // 0=DTR, 1=RTS, 2=BOTH bool com_invert_dtr = false; bool com_invert_rts = false; + +#ifdef WITH_CM108 + // CM108 PTT settings + int cm108_gpio = 3; +#endif // PTT timing int ptt_delay_ms = 50; // Delay after PTT before TX @@ -250,44 +258,32 @@ inline std::string packet_visualize(const uint8_t* data, size_t len, bool is_tx, uint8_t flags = data[4]; oss << " │ FRAG HDR [5 bytes] Magic: 0xF3 │\n"; - oss << " │ Packet ID: " << std::setw(5) << pkt_id; - oss << " Seq: " << std::setw(3) << (int)seq; - oss << " Flags: "; - - std::string flag_str; - if (flags & 0x02) flag_str += "FIRST "; - if (flags & 0x01) flag_str += "MORE"; - if (flag_str.empty()) flag_str = "LAST"; - oss << std::left << std::setw(12) << flag_str << std::right << " │\n"; - + oss << " │ Packet ID: 0x" << std::hex << std::setfill('0') << std::setw(4) << pkt_id << std::dec; + oss << " Seq: " << std::setw(3) << (int)seq; + oss << " Flags: "; + if (flags & 0x02) oss << "FIRST "; + if (flags & 0x01) oss << "MORE"; + if (!(flags & 0x03)) oss << "LAST"; + oss << std::string(20, ' ') << "│\n"; offset = 5; - oss << " ├─────────────────────────────────────────────────────────────┤\n"; } - size_t payload_len = len - offset; - oss << " │ PAYLOAD [" << payload_len << " bytes]"; - oss << std::string(49 - std::to_string(payload_len).length(), ' ') << "│\n"; - - size_t preview_len = std::min(payload_len, (size_t)32); - if (preview_len > 0) { + if (offset < len) { + oss << " ├─────────────────────────────────────────────────────────────┤\n"; + size_t payload_len = len - offset; + oss << " │ PAYLOAD [" << payload_len << " bytes]"; + oss << std::string(49 - std::to_string(payload_len).length(), ' ') << "│\n"; + + size_t preview_len = std::min(payload_len, (size_t)24); oss << " │ "; for (size_t i = 0; i < preview_len; i++) { oss << std::hex << std::setfill('0') << std::setw(2) << (int)data[offset + i]; if (i < preview_len - 1) oss << " "; } - if (payload_len > 32) oss << "..."; - size_t used = preview_len * 3 - 1 + (payload_len > 32 ? 3 : 0); + if (payload_len > 24) oss << " ..."; + oss << std::dec; + size_t used = preview_len * 3 - 1 + (payload_len > 24 ? 4 : 0); if (used < 57) oss << std::string(57 - used, ' '); - oss << std::dec << " │\n"; - - oss << " │ "; - for (size_t i = 0; i < preview_len; i++) { - char c = data[offset + i]; - oss << (c >= 32 && c < 127 ? c : '.'); - } - if (payload_len > 32) oss << "..."; - size_t ascii_used = preview_len + (payload_len > 32 ? 3 : 0); - if (ascii_used < 57) oss << std::string(57 - ascii_used, ' '); oss << " │\n"; } diff --git a/misc/50-cm108-ptt.rules b/misc/50-cm108-ptt.rules new file mode 100644 index 0000000..b7fd429 --- /dev/null +++ b/misc/50-cm108-ptt.rules @@ -0,0 +1 @@ +SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", MODE="0660", TAG+="uaccess" diff --git a/tnc_ui.hh b/tnc_ui.hh index 0e25256..3ec96e4 100644 --- a/tnc_ui.hh +++ b/tnc_ui.hh @@ -36,6 +36,9 @@ const std::vector CODE_RATE_OPTIONS = { const std::vector PTT_TYPE_OPTIONS = { "NONE", "RIGCTL", "VOX", "COM" +#ifdef WITH_CM108 + , "CM108" +#endif }; const std::vector PTT_LINE_OPTIONS = { @@ -87,6 +90,10 @@ struct TNCUIState { bool com_invert_dtr = false; bool com_invert_rts = false; +#ifdef WITH_CM108 + // CM108 PTT settings (PTT type 4) + int cm108_gpio = 3; // GPIO pin to use for PTT, default 3 +#endif int mtu_bytes = 0; int bitrate_bps = 0; @@ -368,6 +375,10 @@ struct TNCUIState { fprintf(f, "com_ptt_line=%d\n", com_ptt_line); fprintf(f, "com_invert_dtr=%d\n", com_invert_dtr ? 1 : 0); fprintf(f, "com_invert_rts=%d\n", com_invert_rts ? 1 : 0); +#ifdef WITH_CM108 + fprintf(f, "# CM108 PTT\n"); + fprintf(f, "cm108_gpio=%d\n", cm108_gpio); +#endif fprintf(f, "# Network\n"); fprintf(f, "port=%d\n", port); fprintf(f, "# Utils\n"); @@ -414,6 +425,9 @@ struct TNCUIState { else if (strcmp(key, "com_ptt_line") == 0) com_ptt_line = atoi(value); else if (strcmp(key, "com_invert_dtr") == 0) com_invert_dtr = atoi(value) != 0; else if (strcmp(key, "com_invert_rts") == 0) com_invert_rts = atoi(value) != 0; +#ifdef WITH_CM108 + else if (strcmp(key, "cm108_gpio") == 0) cm108_gpio = atoi(value); +#endif else if (strcmp(key, "port") == 0) port = atoi(value); else if (strcmp(key, "random_data_size") == 0) random_data_size = atoi(value); } @@ -683,6 +697,9 @@ private: FIELD_COM_PORT, FIELD_COM_LINE, FIELD_COM_INVERT, +#ifdef WITH_CM108 + FIELD_CM108_GPIO, +#endif FIELD_NET_PORT, FIELD_PRESET, FIELD_COUNT @@ -835,6 +852,10 @@ private: edit_text_field(FIELD_COM_PORT); +#ifdef WITH_CM108 + } else if (current_field_ == FIELD_CM108_GPIO) { + edit_text_field(FIELD_CM108_GPIO); +#endif } else if (current_field_ == FIELD_AUDIO_INPUT) { @@ -1045,6 +1066,11 @@ private: } else if (field == FIELD_COM_PORT) { row = 20; max_len = 20; +#ifdef WITH_CM108 + } else if (field == FIELD_CM108_GPIO) { + row = 20; + max_len = 1; +#endif } else if (field == FIELD_NET_PORT) { if (state_.ptt_type_index == 2) { //2 extra rows row = 24; @@ -1086,6 +1112,16 @@ private: state_.com_port = buf; state_.add_log("(!) COM port changed, restart required"); apply_settings(); +#ifdef WITH_CM108 + } else if (field == FIELD_CM108_GPIO) { + try { + int gpio = std::stoi(buf); + if (gpio >= 1 && gpio <= 4) { + state_.cm108_gpio = gpio; + apply_settings(); + } + } catch (...) {} +#endif } else if (field == FIELD_NET_PORT) { try { int port = std::stoi(buf); @@ -1114,6 +1150,13 @@ private: return true; } } +#ifdef WITH_CM108 + if (state_.ptt_type_index != 4) { // not CM108 + if (field == FIELD_CM108_GPIO) { + return true; + } + } +#endif return false; } @@ -1149,7 +1192,11 @@ private: case FIELD_AUDIO_OUTPUT: break; case FIELD_PTT_TYPE: +#ifdef WITH_CM108 + state_.ptt_type_index = (state_.ptt_type_index + delta + 5) % 5; +#else state_.ptt_type_index = (state_.ptt_type_index + delta + 4) % 4; +#endif break; case FIELD_VOX_FREQ: state_.vox_tone_freq += delta * 100; @@ -2022,6 +2069,13 @@ private: if (field == FIELD_COM_INVERT) return row; row++; } +#ifdef WITH_CM108 + // CM108 field, only when CM108 selected as PTT + if (state_.ptt_type_index == 4) { + if (field == FIELD_CM108_GPIO) return row; + row++; + } +#endif row++; // NETWORK section row++; // header @@ -2252,6 +2306,17 @@ private: } row++; } +#ifdef WITH_CM108 + if (state_.ptt_type_index == 4) { // CM108 + dy = visible_y(row); + if (dy >= 0) { + char cm108_gpio_buf[32]; + snprintf(cm108_gpio_buf, sizeof(cm108_gpio_buf), "%d", state_.cm108_gpio); + draw_field(dy, c1, c2, "GPIO Pin", FIELD_CM108_GPIO, cm108_gpio_buf, true); + } + row++; + } +#endif row++; // Network section