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.4";
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>
#include <string.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 setup_signals(void) {
	struct sigaction action;
	action.sa_handler = handle_signal;
	action.sa_flags = 0;
	if (sigemptyset(&action.sa_mask)) {
		fprintf(stderr, "Unable to empty signal mask: %s\n",
			strerror(errno));
		return 1;
	}
	if (sigaction(SIGINT, &action, NULL) == -1) {
		fprintf(stderr, "Unable to catch SIGINT: %s\n",
			strerror(errno));
		return 1;
	}
	if (sigaction(SIGTERM, &action, NULL) == -1) {
		fprintf(stderr, "Unable to catch SIGTERM: %s\n",
			strerror(errno));
		return 1;
	}
	if (sigaction(SIGPIPE, &action, NULL) == -1) {
		fprintf(stderr, "Unable to catch SIGPIPE: %s\n",
			strerror(errno));
		return 1;
	}
	return 0;
}

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);
	if (setup_signals() == -1) {
		return 1;
	}
	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 <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(stderr, "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(stderr, "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(stderr,
			"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(stderr,
				"Couldn't nanosleep for %i second %i "
				"nanoseconds: %s\n",
				sec, ns, strerror(errno));
			return 1;
		}
	}
	return 0;
}