#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Configuration structure typedef struct { char device[256]; float threshold; int hang_ms; int window_width; int window_height; char window_title[256]; char ptt_method[32]; // "keyboard" or "serial" char serial_port[256]; // Port for VOX to control (e.g., /dev/tnt0) } Config; // Defaults Config config = { .device = "pipewire:NODE=FDV_VOX.monitor_FL", .threshold = 0.04f, .hang_ms = 1200, .window_width = 180, .window_height = 50, .window_title = "VOX", .ptt_method = "serial", .serial_port = "/dev/tnt0" }; static bool running = false; static pthread_t vox_thread; static int lockfd = -1; static int serial_fd = -1; // X11 globals Display *dpy; int keycode_space; // Trim whitespace from string char* trim(char* str) { char* end; while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r') str++; if (*str == 0) return str; end = str + strlen(str) - 1; while (end > str && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--; *(end + 1) = 0; return str; } // Create lock file to prevent multiple instances int create_lock_file() { const char* home = getenv("HOME"); char lock_path[512]; snprintf(lock_path, sizeof(lock_path), "%s/.config/freedv-vox.lock", home ? home : "."); lockfd = open(lock_path, O_CREAT | O_RDWR, 0600); if (lockfd < 0) { fprintf(stderr, "Error: Cannot create lock file %s: %s\n", lock_path, strerror(errno)); return -1; } // Try to acquire exclusive lock (non-blocking) if (flock(lockfd, LOCK_EX | LOCK_NB) < 0) { if (errno == EWOULDBLOCK) { fprintf(stderr, "Error: Another instance of FreeDV VOX is already running.\n"); close(lockfd); lockfd = -1; return -1; } fprintf(stderr, "Error: Cannot lock file: %s\n", strerror(errno)); close(lockfd); lockfd = -1; return -1; } // Write PID to lock file char pid_str[32]; snprintf(pid_str, sizeof(pid_str), "%d\n", getpid()); if (write(lockfd, pid_str, strlen(pid_str)) < 0) { fprintf(stderr, "Warning: Cannot write PID to lock file\n"); } printf("Lock file created: %s\n", lock_path); return 0; } // Release lock file void release_lock_file() { if (lockfd >= 0) { flock(lockfd, LOCK_UN); close(lockfd); lockfd = -1; // Remove lock file const char* home = getenv("HOME"); char lock_path[512]; snprintf(lock_path, sizeof(lock_path), "%s/.config/freedv-vox.lock", home ? home : "."); unlink(lock_path); } } // Open serial port for PTT control int open_serial_port() { // Open with O_NONBLOCK to prevent blocking on carrier detect serial_fd = open(config.serial_port, O_RDWR | O_NOCTTY | O_NONBLOCK); if (serial_fd < 0) { fprintf(stderr, "Error: Cannot open serial port %s: %s\n", config.serial_port, strerror(errno)); return -1; } // Configure serial port struct termios tio; tcgetattr(serial_fd, &tio); cfmakeraw(&tio); // Critical flags for tty0tty compatibility tio.c_cflag |= CLOCAL; // Ignore modem control lines for opening tio.c_cflag &= ~CRTSCTS; // Disable hardware flow control cfsetispeed(&tio, B9600); cfsetospeed(&tio, B9600); tcsetattr(serial_fd, TCSANOW, &tio); // Get the paired port number char paired_port[256]; int port_num = -1; sscanf(config.serial_port, "/dev/tnt%d", &port_num); if (port_num >= 0) { int paired_num = (port_num % 2 == 0) ? port_num + 1 : port_num - 1; snprintf(paired_port, sizeof(paired_port), "/dev/tnt%d", paired_num); printf("Serial port opened: %s (non-blocking, no HW flow control)\n", config.serial_port); printf("Configure FreeDV to use: %s with CTS for PTT input\n", paired_port); } else { printf("Serial port opened: %s (non-blocking, no HW flow control)\n", config.serial_port); } return 0; } // Close serial port void close_serial_port() { if (serial_fd >= 0) { // Make sure RTS is off before closing int status; if (ioctl(serial_fd, TIOCMGET, &status) == 0) { status &= ~TIOCM_RTS; ioctl(serial_fd, TIOCMSET, &status); } close(serial_fd); serial_fd = -1; printf("Serial port closed\n"); } } // Set serial PTT state (controls RTS, which appears as CTS on the other end) void set_serial_ptt(bool on) { if (serial_fd < 0) return; int status; if (ioctl(serial_fd, TIOCMGET, &status) < 0) { fprintf(stderr, "Error: Cannot get serial port status: %s\n", strerror(errno)); return; } if (on) { status |= TIOCM_RTS; } else { status &= ~TIOCM_RTS; } if (ioctl(serial_fd, TIOCMSET, &status) < 0) { fprintf(stderr, "Error: Cannot set serial port status: %s\n", strerror(errno)); return; } printf("Serial PTT RTS: %s (FreeDV sees CTS %s)\n", on ? "ON" : "OFF", on ? "HIGH" : "LOW"); } // Load configuration from file void load_config(const char* filename) { FILE* file = fopen(filename, "r"); if (!file) { printf("Config file not found, using defaults. Will create %s\n", filename); return; } char line[512]; while (fgets(line, sizeof(line), file)) { char* trimmed = trim(line); if (trimmed[0] == '#' || trimmed[0] == '\0') continue; char key[256], value[256]; if (sscanf(trimmed, "%255[^=]=%255[^\n]", key, value) == 2) { char* k = trim(key); char* v = trim(value); if (strcmp(k, "device") == 0) { strncpy(config.device, v, sizeof(config.device) - 1); } else if (strcmp(k, "threshold") == 0) { config.threshold = atof(v); } else if (strcmp(k, "hang_ms") == 0) { config.hang_ms = atoi(v); } else if (strcmp(k, "window_width") == 0) { config.window_width = atoi(v); } else if (strcmp(k, "window_height") == 0) { config.window_height = atoi(v); } else if (strcmp(k, "window_title") == 0) { strncpy(config.window_title, v, sizeof(config.window_title) - 1); } else if (strcmp(k, "ptt_method") == 0) { strncpy(config.ptt_method, v, sizeof(config.ptt_method) - 1); } else if (strcmp(k, "serial_port") == 0) { strncpy(config.serial_port, v, sizeof(config.serial_port) - 1); } } } fclose(file); printf("Configuration loaded from %s\n", filename); } // Save configuration to file void save_config(const char* filename) { FILE* file = fopen(filename, "w"); if (!file) { fprintf(stderr, "Error: Cannot create config file %s\n", filename); return; } fprintf(file, "# FreeDV VOX Configuration\n\n"); fprintf(file, "# Audio device (PipeWire/ALSA)\n"); fprintf(file, "device=%s\n\n", config.device); fprintf(file, "# Audio threshold (0.0-1.0, typical: 0.01-0.05)\n"); fprintf(file, "threshold=%.4f\n\n", config.threshold); fprintf(file, "# Hang time in milliseconds (typical: 300-1000)\n"); fprintf(file, "hang_ms=%d\n\n", config.hang_ms); fprintf(file, "# Window size\n"); fprintf(file, "window_width=%d\n", config.window_width); fprintf(file, "window_height=%d\n\n", config.window_height); fprintf(file, "# Window title\n"); fprintf(file, "window_title=%s\n\n", config.window_title); fprintf(file, "# PTT method: keyboard or serial\n"); fprintf(file, "ptt_method=%s\n\n", config.ptt_method); fprintf(file, "# Serial port for VOX to control (e.g., /dev/tnt0)\n"); fprintf(file, "# FreeDV should use the paired port (e.g., /dev/tnt1) with CTS for PTT\n"); fprintf(file, "# Pairs: tnt0<->tnt1, tnt2<->tnt3, tnt4<->tnt5, tnt6<->tnt7\n"); fprintf(file, "serial_port=%s\n", config.serial_port); fclose(file); printf("Configuration saved to %s\n", filename); } // Send PTT command (keyboard toggle or serial state) void ptt_command(bool on) { if (strcmp(config.ptt_method, "serial") == 0) { set_serial_ptt(on); } else { // Keyboard PTT - toggle printf("PTT: TOGGLE (keycode=%d)\n", keycode_space); XSync(dpy, False); XTestFakeKeyEvent(dpy, keycode_space, True, CurrentTime); XFlush(dpy); usleep(30000); XTestFakeKeyEvent(dpy, keycode_space, False, CurrentTime); XFlush(dpy); XSync(dpy, False); printf("PTT toggle complete\n"); } } // audio thread void* vox_loop(void *arg) { snd_pcm_t *handle; int err; printf("VOX thread starting...\n"); err = snd_pcm_open(&handle, config.device, SND_PCM_STREAM_CAPTURE, 0); if (err < 0) { fprintf(stderr, "Cannot open audio device %s (%s)\n", config.device, snd_strerror(err)); return NULL; } err = snd_pcm_set_params(handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, 1, 48000, 1, 20000); if (err < 0) { fprintf(stderr, "Cannot set audio parameters (%s)\n", snd_strerror(err)); snd_pcm_close(handle); return NULL; } printf("Audio device configured successfully\n"); printf("PTT method: %s\n", config.ptt_method); printf("Threshold: %.4f\n", config.threshold); printf("Hang time: %d ms\n", config.hang_ms); printf("Monitoring audio levels...\n"); const int frames = 960; int16_t buf[frames]; bool tx = false; uint64_t last_peak = 0; int frame_count = 0; while (running) { int r = snd_pcm_readi(handle, buf, frames); if (r > 0) { float sum = 0; for (int i = 0; i < r; i++) { float sample = buf[i] / 32768.0f; sum += sample * sample; } float rms = sqrt(sum / r); if (frame_count % 50 == 0) { printf("RMS: %.6f %s (tx=%d)\n", rms, rms > config.threshold ? "*** ABOVE THRESHOLD ***" : "", tx); } frame_count++; uint64_t now = g_get_monotonic_time() / 1000; if (rms > config.threshold) { last_peak = now; if (!tx) { tx = true; printf("\n=== ACTIVATING TX (RMS=%.6f) ===\n", rms); ptt_command(true); } } if (tx && (now - last_peak > config.hang_ms)) { tx = false; printf("\n=== DEACTIVATING TX (hang timeout: %llu ms) ===\n", now - last_peak); ptt_command(false); } } else if (r < 0) { fprintf(stderr, "Read error: %s\n", snd_strerror(r)); } } if (tx) { printf("Cleaning up: releasing PTT\n"); ptt_command(false); } snd_pcm_close(handle); printf("VOX thread exiting\n"); return NULL; } // GTK button toggles VOX //void on_button_clicked(GtkButton *btn, gpointer data) { // running = !running; // printf("\n=== Button clicked: running=%d ===\n", running); // if (running) { // gtk_button_set_label(btn, "VOX ON"); // pthread_create(&vox_thread, NULL, vox_loop, NULL); // } else { // gtk_button_set_label(btn, "VOX OFF"); // pthread_join(vox_thread, NULL); // } //} // In the on_button_clicked function, add color styling: void on_button_clicked(GtkButton *btn, gpointer data) { running = !running; printf("\n=== Button clicked: running=%d ===\n", running); if (running) { gtk_button_set_label(btn, "ON"); // Set amber/orange background color GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(btn)); GtkCssProvider *provider = gtk_css_provider_new(); gtk_css_provider_load_from_data(provider, "button { background-image: none; background-color: #FF9800; color: white; font-weight: bold; }", -1, NULL); gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref(provider); pthread_create(&vox_thread, NULL, vox_loop, NULL); } else { gtk_button_set_label(btn, "OFF"); // Reset to default styling GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(btn)); GtkCssProvider *provider = gtk_css_provider_new(); gtk_css_provider_load_from_data(provider, "button { background-image: none; background-color: #353535; color: white; font-weight: normal; }", -1, NULL); gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref(provider); pthread_join(vox_thread, NULL); } } // Cleanup on exit void cleanup_on_exit() { running = false; close_serial_port(); release_lock_file(); } int main(int argc, char **argv) { if (create_lock_file() < 0) { return 1; } atexit(cleanup_on_exit); const char* home = getenv("HOME"); char config_path[512]; snprintf(config_path, sizeof(config_path), "%s/.config/freedv-vox.cfg", home ? home : "."); load_config(config_path); if (access(config_path, F_OK) != 0) { save_config(config_path); } gtk_init(&argc, &argv); if (strcmp(config.ptt_method, "serial") == 0) { if (open_serial_port() < 0) { fprintf(stderr, "Error: Failed to open serial port\n"); return 1; } } else { dpy = XOpenDisplay(NULL); if (!dpy) { fprintf(stderr, "Cannot open X display\n"); return 1; } int event_base, error_base, major, minor; if (!XTestQueryExtension(dpy, &event_base, &error_base, &major, &minor)) { fprintf(stderr, "XTest extension not available\n"); return 1; } printf("XTest extension available: v%d.%d\n", major, minor); keycode_space = XKeysymToKeycode(dpy, XK_space); printf("X11 initialized: keycode for space = %d\n", keycode_space); if (keycode_space == 0) { fprintf(stderr, "Failed to get keycode for space!\n"); return 1; } } // Create main horizontal box with padding GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); gtk_container_set_border_width(GTK_CONTAINER(hbox), 10); // Create the button GtkWidget *btn = gtk_button_new_with_label("VOX OFF"); g_signal_connect(btn, "clicked", G_CALLBACK(on_button_clicked), NULL); // Add button to box with expand/fill gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0); // Create window GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(win), config.window_title); gtk_window_set_default_size(GTK_WINDOW(win), config.window_width, config.window_height); gtk_window_set_position(GTK_WINDOW(win), GTK_WIN_POS_CENTER); gtk_window_set_resizable(GTK_WINDOW(win), FALSE); // Add box to window gtk_container_add(GTK_CONTAINER(win), hbox); g_signal_connect(win, "destroy", G_CALLBACK(gtk_main_quit), NULL); gtk_widget_show_all(win); gtk_main(); running = false; if (strcmp(config.ptt_method, "keyboard") == 0) { XCloseDisplay(dpy); } close_serial_port(); release_lock_file(); return 0; }