This commit is contained in:
Matthias Portzel 2022-03-18 11:02:29 -04:00
commit 1f030bec1d
8 changed files with 220 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/zig-*/

12
README Normal file
View File

@ -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.

35
build.zig Normal file
View File

@ -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);
}

7
chime.service Normal file
View File

@ -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

10
chime.timer Normal file
View File

@ -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

129
src/main.zig Normal file
View File

@ -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;
}
}
}

13
time_test.c Normal file
View File

@ -0,0 +1,13 @@
#include <time.h>
#include <stdio.h>
// 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);
}

13
time_test.zig Normal file
View File

@ -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()
});
}