/* SPDX-License-Identifier: MIT */ /* Copyright 2022 Jookia <contact@jookia.org> */ /* PROGRAM OPTIONS */ #define TONE_MAX 256 #define TONE_MAX_STR "256" struct tone_cmd { int tone_hz; int duration_ms; }; struct program_opts { int dry_run; int tone_count; const char *device; struct tone_cmd tones[TONE_MAX]; }; /* OPTIONS PARSING */ #include <argp.h> #include <stdio.h> error_t argp_parser(int key, char *arg, struct argp_state *state); const char *argp_program_version = "evtone 1.1"; const char argp_args_doc[] = "TONE [TONE...]"; const char argp_doc[] = "TONE is of the format HZ:MILLISECONDS\n" "Up to " TONE_MAX_STR " TONEs may be specified at once.\v" "Examples:\n" " \"evtone 440:1500\" plays a tone of 440Hz for 1500 milliseconds.\n" " \"evtone 440:1500 300:10\" plays a 440Hz tone for 1500 milliseconds\n" " followed by a 300Hz tone for 10 milliseconds\n" " \"evtone -d 440:1500 300:10\" will print what the program will do\n" " instead of actually doing it\n" " \"evtone -D /dev/input/event2 440:1000\" will play a 440Hz tone for\n" " 1000 milliseconds using the /dev/input/event2 device"; const struct argp_option argp_options[] = { {.name = "dry-run", .key = 'd', .doc = "Print what the program will do"}, {.name = "device", .key = 'D', .arg = "FILE", .doc = "/dev/input device to use to play tones\n" "If not supplied the device will be guessed"}, {0}}; const struct argp args = { .options = argp_options, .parser = argp_parser, .args_doc = argp_args_doc, .doc = argp_doc, }; int parse_tone(char *arg, struct program_opts *opts) { if (opts->tone_count >= TONE_MAX) { fprintf(stderr, "Error: Too many TONEs specified at once\n"); return 1; } struct tone_cmd *tone = &opts->tones[opts->tone_count++]; char canary; int rc = sscanf(arg, "%i:%i%c", &tone->tone_hz, &tone->duration_ms, &canary); if (tone->tone_hz < 0 || tone->duration_ms < 0) { fprintf(stderr, "Error: TONE HZ or DURATION must be over 0\n"); return 1; } if (rc != 2) { fprintf(stderr, "Error: Invalid TONE format\n"); return 1; } return 0; } error_t argp_parser(int key, char *arg, struct argp_state *state) { struct program_opts *opts = state->input; switch (key) { case 'd': opts->dry_run = 1; break; case 'D': opts->device = arg; break; case ARGP_KEY_ARG: if (parse_tone(arg, opts)) { argp_usage(state); } break; case ARGP_KEY_END: if (state->arg_num < 1) { argp_usage(state); } break; default: return ARGP_ERR_UNKNOWN; }; return 0; } /* MAIN */ #include <stdlib.h> #include <unistd.h> struct program_opts *main_opts = 0; void cleanup_main_opts(void) { free(main_opts); } int play_tones(int dry_run, const char *device, struct tone_cmd *tones, int tone_count); int main(int argc, char *argv[]) { main_opts = calloc(sizeof(struct program_opts), 1); if (main_opts == NULL) { fprintf(stderr, "Unable to allocate main program options. Out of memory?\n"); return 1; } atexit(cleanup_main_opts); /* argp will exit instead of returning */ error_t err = argp_parse(&args, argc, argv, 0, 0, main_opts); if (err) { fprintf(stderr, "Unable to parse program arguments. Please report this bug.\n"); return 1; } return play_tones(main_opts->dry_run, main_opts->device, main_opts->tones, main_opts->tone_count); } /* TONE PROCESSING */ #include <signal.h> static void cleanup_fd(int *fd) { if (*fd >= 0) { close(*fd); } } #define _cleanup_fd_ __attribute__((cleanup(cleanup_fd))) int open_device(int dry_run, const char *device); int play_tone(int dry_run, int device, int tone_hz); int wait_ms(int dry_run, int duration_ms); int stop_tone(int dry_run, int device); volatile sig_atomic_t got_signal = 0; void handle_signal(int signal) { got_signal = signal; } int play_tones(int dry_run, const char *device, struct tone_cmd *tones, int tone_count) { _cleanup_fd_ int dev = open_device(dry_run, device); signal(SIGINT, handle_signal); signal(SIGTERM, handle_signal); signal(SIGPIPE, handle_signal); if (dev == -1) { return 1; } for (int i = 0; i < tone_count; ++i) { int tone_hz = tones[i].tone_hz; int duration_ms = tones[i].duration_ms; if (play_tone(dry_run, dev, tone_hz)) { return 1; } if (wait_ms(dry_run, duration_ms)) { return 1; } if (got_signal) { fprintf(stdout, "Got signal %i, quitting\n", got_signal); break; } } if (stop_tone(dry_run, dev)) { return 1; } return 0; } /* SYSTEM SPECIFIC */ #include <errno.h> #include <fcntl.h> #include <linux/input.h> #include <string.h> #include <time.h> #include <unistd.h> const char *device_guesses[] = { "/dev/input/by-path/platform-pcspkr-event-spkr", /* pcspkr on x86 */ "/dev/input/by-path/platform-beeper-event", /* pwm-beeper */ 0}; int try_open_device(int dry_run, const char *device) { if (dry_run) { fprintf(stdout, "Try opening device: %s\n", device); return -2; } int dev = open(device, O_WRONLY); return dev; } int open_device(int dry_run, const char *device) { int dev = -1; if (device) { int dev = try_open_device(dry_run, device); if (dev == -1) { fprintf(stdout, "Couldn't open %s: %s\n", device, strerror(errno)); return -1; } } else { const char **guess = device_guesses; do { dev = try_open_device(dry_run, *guess); } while (*++guess && dev < 0); if (dev == -1) { fprintf(stdout, "Couldn't guess device\n"); return -1; } } return dev; } int play_tone(int dry_run, int device, int tone_hz) { if (dry_run) { fprintf(stdout, "Send event EV_SND SND_TONE %i to device\n", tone_hz); return 0; } struct input_event event = {0}; event.type = EV_SND; event.code = SND_TONE; event.value = tone_hz; if (write(device, &event, sizeof(struct input_event)) == -1) { fprintf(stdout, "Couldn't write %iHz SND_TONE event to device: %s\n", tone_hz, strerror(errno)); return 1; } return 0; } int stop_tone(int dry_run, int device) { return play_tone(dry_run, device, 0); } int wait_ms(int dry_run, int duration_ms) { int sec = duration_ms / 1000; int ns = (duration_ms % 1000) * 1000000; if (dry_run) { fprintf(stdout, "nanosleep for %i second %i nanoseconds\n", sec, ns); return 0; } struct timespec time = {sec, ns}; if (nanosleep(&time, NULL) == -1) { if (!(errno == EINTR && got_signal)) { fprintf(stdout, "Couldn't nanosleep for %i second %i nanoseconds: %s\n", sec, ns, strerror(errno)); } return 1; } return 0; }