Merge pull request #4 from F4JNT-AF/master

Add basic CM108 PTT via USB support
This commit is contained in:
Zenith 2026-01-15 16:54:14 -05:00 committed by GitHub
commit a40010c6bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 268 additions and 35 deletions

View file

@ -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 ""

View file

@ -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

65
cm108_ptt.hh Normal file
View file

@ -0,0 +1,65 @@
#pragma once
#include <string>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <hidapi/hidapi.h>
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;
};

View file

@ -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<CM108PTT>();
cm108_ptt_->open(config_.cm108_gpio);
#endif
} else {
dummy_ptt_ = std::make_unique<DummyPTT>();
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<MiniAudio> audio_;
std::unique_ptr<RigctlPTT> rigctl_;
std::unique_ptr<SerialPTT> serial_ptt_;
#ifdef WITH_CM108
std::unique_ptr<CM108PTT> cm108_ptt_;
#endif
std::unique_ptr<DummyPTT> 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) {

View file

@ -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";
}

1
misc/50-cm108-ptt.rules Normal file
View file

@ -0,0 +1 @@
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", MODE="0660", TAG+="uaccess"

View file

@ -36,6 +36,9 @@ const std::vector<std::string> CODE_RATE_OPTIONS = {
const std::vector<std::string> PTT_TYPE_OPTIONS = {
"NONE", "RIGCTL", "VOX", "COM"
#ifdef WITH_CM108
, "CM108"
#endif
};
const std::vector<std::string> 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