Newer
Older
evtone / evtone.c
/* 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.2";
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(event)) != sizeof(event)) {
    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;
}