/* * BlueALSA - rfcomm.c * SPDX-FileCopyrightText: 2016-2025 BlueALSA developers * SPDX-License-Identifier: MIT */ #if HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shared/dbus-client.h" #include "shared/dbus-client-rfcomm.h" #include "shared/log.h" static int rfcomm_fd = -1; static bool main_loop_on = true; static bool input_is_tty = true; static bool output_is_tty = true; static bool properties = false; static int path2hci(const char *path) { int id; if ((path = strstr(path, "/hci")) == NULL || sscanf(path, "/hci%d/", &id) != 1) return -1; return id; } static int path2ba(const char *path, bdaddr_t *ba) { unsigned int x[6]; if ((path = strstr(path, "/dev_")) == NULL || sscanf(&path[5], "%x_%x_%x_%x_%x_%x", &x[5], &x[4], &x[3], &x[2], &x[1], &x[0]) != 6) return -1; for (size_t i = 0; i < 6; i++) ba->b[i] = x[i]; return 0; } static char *strtrim(char *str) { while (isspace(*str)) str++; if (*str == '\0') return str; char *end = &str[strlen(str) - 1]; while (end > str && isspace(*end)) end--; end[1] = '\0'; return str; } static bool print_properties(struct ba_dbus_ctx *dbus_ctx, const char* path, DBusError *err) { struct ba_rfcomm_props props = { 0 }; if (!ba_dbus_rfcomm_props_get(dbus_ctx, path, &props, err)) { ba_dbus_rfcomm_props_free(&props); return false; } printf("Transport: %s\n", props.transport); printf("Features:"); for (size_t i = 0; i < props.features_len; i++) printf(" %s", props.features[i]); printf("\n"); printf("Battery: %d\n", props.battery); ba_dbus_rfcomm_props_free(&props); return true; } static char *build_rfcomm_command(char *buffer, size_t size, const char *cmd) { bool at = strncmp(cmd, "AT", 2) == 0; snprintf(buffer, size, "%s%s%s", at ? "" : "\r\n", cmd, at ? "\r" : "\r\n"); return buffer; } static void rl_callback_handler(char *line) { if (line == NULL) { rl_callback_handler_remove(); main_loop_on = false; return; } line = strtrim(line); if (strlen(line) == 0) return; char cmd[512]; build_rfcomm_command(cmd, sizeof(cmd), line); if (write(rfcomm_fd, cmd, strlen(cmd)) == -1) warn("Couldn't send RFCOMM command: %s", strerror(errno)); if (input_is_tty) add_history(line); } int main(int argc, char *argv[]) { int opt; const char *opts = "hVB:p"; const struct option longopts[] = { { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { "dbus", required_argument, NULL, 'B' }, { "properties", no_argument, NULL, 'p' }, { 0, 0, 0, 0 }, }; char dbus_ba_service[32] = BLUEALSA_SERVICE; log_open(basename(argv[0]), false); while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1) switch (opt) { case 'h' /* --help */ : usage: printf("Usage:\n" " %s [OPTION]... \n" "\nOptions:\n" " -h, --help\t\tprint this help and exit\n" " -V, --version\t\tprint version and exit\n" " -B, --dbus=NAME\tBlueALSA D-Bus service name suffix\n" " -p, --properties\tprint device properties and exit\n", argv[0]); return EXIT_SUCCESS; case 'V' /* --version */ : printf("%s\n", PACKAGE_VERSION); return EXIT_SUCCESS; case 'B' /* --dbus=NAME */ : snprintf(dbus_ba_service, sizeof(dbus_ba_service), BLUEALSA_SERVICE ".%s", optarg); if (!dbus_validate_bus_name(dbus_ba_service, NULL)) { error("Invalid BlueALSA D-Bus service name: %s", dbus_ba_service); return EXIT_FAILURE; } break; case 'p' /* --properties */ : properties = true; break; default: fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]); return EXIT_FAILURE; } if (optind + 1 != argc) goto usage; int hci_dev_id; bdaddr_t addr; if ((hci_dev_id = path2hci(argv[optind])) == -1 || path2ba(argv[optind], &addr) == -1) { error("Invalid BT device path: %s", argv[optind]); return EXIT_FAILURE; } DBusError err = DBUS_ERROR_INIT; struct ba_dbus_ctx dbus_ctx; if (!ba_dbus_connection_ctx_init(&dbus_ctx, dbus_ba_service, &err)) { error("Couldn't initialize D-Bus context: %s", err.message); return EXIT_FAILURE; } char rfcomm_path[128]; sprintf(rfcomm_path, "/org/bluealsa/hci%d/dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X/rfcomm", hci_dev_id, addr.b[5], addr.b[4], addr.b[3], addr.b[2], addr.b[1], addr.b[0]); if (properties) { if (print_properties(&dbus_ctx, rfcomm_path, &err)) return EXIT_SUCCESS; error("D-Bus error: %s", err.message); return EXIT_FAILURE; } if (!ba_dbus_rfcomm_open(&dbus_ctx, rfcomm_path, &rfcomm_fd, &err)) { error("Couldn't open RFCOMM: %s", err.message); return EXIT_FAILURE; } input_is_tty = isatty(fileno(stdin)); output_is_tty = isatty(fileno(stdout)); const char *prefix = ""; char prompt[32] = ""; if (input_is_tty) { sprintf(prompt, "%.2X:%.2X:%.2X:%.2X:%.2X:%.2X> ", addr.b[5], addr.b[4], addr.b[3], addr.b[2], addr.b[1], addr.b[0]); rl_bind_key('\t', rl_insert); prefix = "> "; } else /* libreadline echos its input to stdout which is a problem when * stdin is not the terminal. The function rl_tty_set_echoing() is * broken when used with the callback API, so as a workaround we * redirect the (useless) output to /dev/null */ rl_outstream = fopen("/dev/null", "w"); if (!output_is_tty) /* Force line buffered output to be sure each event will be flushed * immediately. */ setvbuf(stdout, NULL, _IOLBF, 0); rl_callback_handler_install(prompt, rl_callback_handler); struct pollfd pfds[] = { { fileno(stdin), POLLIN, 0 }, { rfcomm_fd, POLLIN, 0 }, }; while (main_loop_on) { poll(pfds, 2, -1); if (pfds[0].revents & POLLIN) rl_callback_read_char(); else if (pfds[0].revents & POLLHUP) break; if (pfds[1].revents & POLLIN) { char buffer[256]; ssize_t ret; if ((ret = read(rfcomm_fd, buffer, sizeof(buffer) - 1)) <= 0) { if (output_is_tty) { rl_set_prompt(NULL); rl_replace_line("disconnected", 0); rl_redisplay(); } break; } buffer[ret] = '\0'; char *old_text = NULL; int old_point = 0; if (output_is_tty) { old_point = rl_point; old_text = rl_copy_text(0, rl_end); rl_save_prompt(); rl_replace_line("", 0); rl_redisplay(); } fprintf(stdout, "%s%s\n", prefix, strtrim(buffer)); if (output_is_tty) { rl_restore_prompt(); rl_replace_line(old_text, 0); rl_point = old_point; rl_redisplay(); free(old_text); } } } rl_callback_handler_remove(); fprintf(stdout, "\n"); return EXIT_SUCCESS; }