commit 1f030bec1d90d6881b25b099ceb044ab6c2b4b60 Author: Matthias Portzel Date: Fri Mar 18 11:02:29 2022 -0400 v1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd8ab6d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/zig-*/ diff --git a/README b/README new file mode 100644 index 0000000..3cda5f3 --- /dev/null +++ b/README @@ -0,0 +1,12 @@ +This project uses the Linux's `ioclt` command KDMKTONE to play clock-tower chimes on the internal tty "speaker" every 15 minutes. It needs to be run as root in order to do this. + +I wanted to do this because the computer that I use as a server in my room doesn't have a real speaker and I didn't want to buy any hardware to achive my dream of being able to hear a clock tower. + +Also, I used this project as an excuse to learn Zig. The Zig code I've written is probably not the nicest way to do things, and I'd welcome feedback on it. + +Also included are the Systemd service and timer files that I use to run this script every 15 minutes. +``` +sudo cp chime.* /etc/systemd/system/ +``` + +This is free and unencumbered software released into the public domain, under the terms of The Unlicense. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..0849a57 --- /dev/null +++ b/build.zig @@ -0,0 +1,35 @@ +const std = @import("std"); + +pub fn build(b: *std.build.Builder) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard release options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. + const mode = b.standardReleaseOptions(); + + const exe = b.addExecutable("chime", "src/main.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); + exe.linkLibC(); + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_tests = b.addTest("src/main.zig"); + exe_tests.setTarget(target); + exe_tests.setBuildMode(mode); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&exe_tests.step); +} diff --git a/chime.service b/chime.service new file mode 100644 index 0000000..45bd6f4 --- /dev/null +++ b/chime.service @@ -0,0 +1,7 @@ +# The service file. Is run every 15 minutes by the timer. Just needs to run chime and exit +[Unit] +Description=Makes a chime sound depending on what time it is + +[Service] +Type=oneshot +ExecStart=/home/matthias/Programming/chime/zig-out/bin/chime diff --git a/chime.timer b/chime.timer new file mode 100644 index 0000000..f626885 --- /dev/null +++ b/chime.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Fires chime.service every 15 minutes, making a chime sound + +[Install] +WantedBy=multi-user.target + +[Timer] +# > Values may be suffixed with "/" and a repetition value, which indicates that the value itself and the value plus all multiples of the repetition value are matched. +# - SYSTEMD.TIME(7) +OnCalendar=*:00/15 diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..a4709ca --- /dev/null +++ b/src/main.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const ioctl = std.os.linux.ioctl; + +// Need current local time, which Zig std doesn't support. Need libc +const time_h = @cImport( + @cInclude("time.h") +); + +const kd_h = @cImport( + @cInclude("linux/kd.h") +); +const KDMKTONE = kd_h.KDMKTONE; +// const KIOCSOUND = kd_h.KIOCSOUND; + +const fcntl_h = @cImport( + @cInclude("fcntl.h") +); +const O_RDWR = fcntl_h.O_RDWR; +const O_RDONLY = fcntl_h.O_RDONLY; + +// From https://github.com/johnath/beep/blob/0d790fa45777896749a885c3b93b2c1476d59f20/beep.c#L48 +// Mentioned in console_ioctl(2) but under KIOCSOUND not KDMKTONE. I still don't understand the difference +// I think KIOCSOUND just doesn't let you control the time, you have to use a second syscall to stop it +// So we're using KDMKTONE +const CLOCK_TICK_RATE = 1193180; + +const Note = enum { B, E, F_sharp, G_sharp, E3 }; + +fn note_to_freq (note: Note) usize { + return switch (note) { + // E key + .B => 247, // B3 + .E => 330, // E4 + .G_sharp => 415, // G#4 + .F_sharp => 370, // F#4 + .E3 => 165 // E3 is used for hour bells, + }; +} + +// I spent so long trying to find a better way to do this in Zig +// :15 (set 1) +const sq_one = [_]Note{.G_sharp, .F_sharp, .E, .B}; +// :30 (sets 2 and 3) +const sq_two = [_]Note{.E, .G_sharp, .F_sharp, .B, .E, .F_sharp, .G_sharp, .E}; +// :45 (sets 4, 5, 1) +const sq_three = [_]Note{.G_sharp, .E, .F_sharp, .B, .B, .F_sharp, .G_sharp, .E, .G_sharp, .F_sharp, .E, .B}; +// :60 (sets 2, 3, 4, 5) +const sq_four = [_]Note{.E, .G_sharp, .F_sharp, .B, .E, .F_sharp, .G_sharp, .G_sharp, .E, .F_sharp, .B, .B, .F_sharp, .G_sharp, .E}; +const sequences = [4][]const Note{ sq_one[0..], sq_two[0..], sq_three[0..], sq_four[0..]}; + +const dur = 500; + +fn play_sequence(sequence: []const Note) std.os.OpenError!void { + for (sequence) |note, i| { + try play_tone(if (i % 4 == 3) dur*2 else dur, note_to_freq(note)); + + if (i % 4 == 3) { + // After each quartet, take a full note rest + std.time.sleep(1000000 * dur); + }else { + // Add a quarter note rest between notes + std.time.sleep(1000000 * dur / 4); + } + } +} + +/// Function play_tone +/// Takes a time in milliseconds, and a frequency in Hz +/// Plays that tone and waits milliseconds before returning +fn play_tone (ms: usize, freq: usize) std.os.OpenError!void { + //TODO: make sure there's not a tone playing + // Error handling, etc + + // TODO?: Save fd between tones? + // O_RDWR is 02 + // print("{}\n", .{ @typeInfo( @TypeOf (kd )) }); + // Right now I'm using os.open which is open(2), the Linux syscall + // Since I'm linking against glibc anyways I think I could use fopen(3) + + // > Linux reserves the special, nonstandard access mode 3 (binary 11) in flags to mean: check for read and write permission on the file and re- + // > turn a file descriptor that can't be used for reading or writing. This nonstandard access mode is used by some Linux drivers to return a + // > file descriptor that is to be used only for device-specific ioctl(2) operations. + // - open(2) + // The third option is for extra options and is unused (for these flags) but Zig won't let me pass only two paramaters like C will + const fd: i32 = try std.os.open("/dev/tty0", 3, 0); + + // i32, u32, usize + // fd, request, argp + // Instantly returns, but the tone persists for `ms` regardless + _ = ioctl(@intCast(i32, fd), KDMKTONE, (ms << 16) + CLOCK_TICK_RATE / freq); + + //nanoseconds to sleep for + std.time.sleep(1000000 * ms); + + // TODO: close the fd?? +} + +pub fn main() anyerror!void { + var rawtime: time_h.time_t = undefined; + _ = time_h.time(&rawtime); + + //localtime returns a c pointer to a tm struct + var current_time: [*c]time_h.tm = time_h.localtime( &rawtime ); + + // 0, 1, 2, or 3 for :15, :30, :45, :00, with rounding to the closest + const quarter = (@floatToInt(u8, @round(@intToFloat(f64, current_time.*.tm_min) / 60.0 * 4)) + 3) % 4; + + std.log.info("Quarter {}", .{ quarter }); + try play_sequence(sequences[quarter]); + + // If it's the 4th quarter (0-index ofc), sound bell + if (quarter == 3) { + var hours = @mod((current_time.*.tm_hour + 11), 12) + 1; + if (current_time.*.tm_min >= 53) { + hours += 1; + } + while (hours > 0) { + // Rest before playing hour bells + std.time.sleep(1000000 * dur); + + // Play a whole note chime + try play_tone(dur, note_to_freq(.E3)); + // Rest 2/4 + std.time.sleep(1000000 * dur / 2); + + hours -= 1; + } + } +} diff --git a/time_test.c b/time_test.c new file mode 100644 index 0000000..7fa6b2f --- /dev/null +++ b/time_test.c @@ -0,0 +1,13 @@ +#include +#include + +// An example of getting the current hour and minute with time.h's `localtime` +void main () { + time_t rawtime; + time(&rawtime); + + struct tm *current_time; + current_time = localtime( &rawtime ); + + printf("%d:%d", current_time->tm_hour, current_time->tm_min); +} diff --git a/time_test.zig b/time_test.zig new file mode 100644 index 0000000..b2a01dc --- /dev/null +++ b/time_test.zig @@ -0,0 +1,13 @@ +const std = @import("std"); + +/// An example of getting the current *UTC* hour and minute with the Zig standard library, no libc needed +/// Unfortunately, the Zig standard library doesn't support local time zones :( +pub fn main() anyerror!void { + const now = std.time.epoch.EpochSeconds{ + .secs = @intCast(u64, std.time.timestamp()) + }; + + std.debug.print("{}", .{ + std.time.epoch.EpochSeconds.getDaySeconds(now).getHoursIntoDay() + }); +}