diff --git a/src/RotCtl.zig b/src/RotCtl.zig index 4520205..add4cf0 100644 --- a/src/RotCtl.zig +++ b/src/RotCtl.zig @@ -10,12 +10,9 @@ const log = std.log.scoped(.RotCtl); writer: std.io.BufferedWriter(512, std.net.Stream.Writer), running: bool, -rotator: YaesuController, +rotator: *YaesuController, pub fn run(allocator: std.mem.Allocator) !void { - // var server = std.net.StreamServer.init(.{ .reuse_address = true }); - // defer server.deinit(); - const listen_addr = try std.net.Address.parseIp( config.rotctl.listen_address, config.rotctl.listen_port, @@ -30,7 +27,7 @@ pub fn run(allocator: std.mem.Allocator) !void { var interface: RotCtl = .{ .writer = undefined, .running = true, - .rotator = try YaesuController.init(allocator), + .rotator = try YaesuController.create(allocator), }; while (interface.running) { diff --git a/src/YaesuController.zig b/src/YaesuController.zig index 64628f6..0a787a9 100644 --- a/src/YaesuController.zig +++ b/src/YaesuController.zig @@ -8,6 +8,8 @@ const log = std.log.scoped(.yaesu_controller); const YaesuController = @This(); +pub var singleton: ?*YaesuController = null; + control_thread: std.Thread, lock: *std.Thread.Mutex, controller: *const Controller, @@ -23,7 +25,7 @@ pub const CalibrationRoutine = enum { }; pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !void { - const controller = try YaesuController.init(allocator); + const controller = try YaesuController.create(allocator); defer { controller.quit(); controller.control_thread.join(); @@ -35,7 +37,12 @@ pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !voi } } -pub fn init(allocator: std.mem.Allocator) !YaesuController { +pub fn create(allocator: std.mem.Allocator) !*YaesuController { + if (singleton) |_| { + log.err("Controller singleton already exists.", .{}); + return error.AlreadyInitialized; + } + const controller = try allocator.create(Controller); errdefer allocator.destroy(controller); controller.* = try Controller.init(allocator); @@ -43,11 +50,16 @@ pub fn init(allocator: std.mem.Allocator) !YaesuController { // do this in the main thread so we can throw the error about it synchronously. try controller.connectLabjack(); - return .{ + const self = try allocator.create(YaesuController); + errdefer allocator.destroy(self); + self.* = .{ .control_thread = try std.Thread.spawn(.{}, runController, .{controller}), .lock = &controller.lock, .controller = controller, }; + + singleton = self; + return self; } fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool { @@ -88,7 +100,7 @@ pub fn setTarget(self: YaesuController, target: AzEl) error{OutOfRange}!void { const controller = @constCast(self.controller); controller.target = masked_target; - controller.requested_state = .running; + controller.requestState(.running); } pub fn currentPosition(self: YaesuController) AzEl { @@ -114,7 +126,7 @@ pub fn quit(self: YaesuController) void { defer self.lock.unlock(); const controller = @constCast(self.controller); - controller.requested_state = .stopped; + controller.requestState(.stopped); } pub fn stop(self: YaesuController) void { @@ -123,7 +135,7 @@ pub fn stop(self: YaesuController) void { const controller = @constCast(self.controller); controller.target = controller.position; - controller.requested_state = .idle; + controller.requestState(.idle); } pub fn startPark(self: YaesuController) void { @@ -254,6 +266,18 @@ const Controller = struct { self.labjack.id = info.local_id; } + // this function is run with the lock already acquired + fn propagateState(self: *Controller) void { + if (self.current_state == .stopped) return; + self.current_state = self.requested_state; + } + + // this function is run with the lock already acquired + fn requestState(self: *Controller, request: ControllerState) void { + if (self.current_state == .stopped) return; + self.requested_state = request; + } + fn lerpOne(input: f64, cal_points: Config.MinMax) f64 { return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle; } @@ -358,32 +382,26 @@ const Controller = struct { var timer = LoopTimer.init(config.controller.loop_interval_ns); while (timer.mark()) : (timer.sleep()) { - self.updateFeedback() catch { - self.lock.lock(); - defer self.lock.unlock(); - - self.current_state = .stopped; - continue; - }; + const fbfail = if (self.updateFeedback()) |_| false else |_| true; { self.lock.lock(); defer self.lock.unlock(); self.setPosition(self.feedback_buffer.get()); + if (fbfail) self.requestState(.stopped); + self.propagateState(); } switch (self.current_state) { - .initializing, .idle => { - self.current_state = self.requested_state; - }, + .initializing, .idle => {}, .calibration => { self.lock.lock(); defer self.lock.unlock(); // run calibration routine. psych, this does nothing. gottem self.current_state = .idle; - self.requested_state = self.current_state; + self.requestState(.idle); }, .running => { const pos_error: AzEl = blk: { diff --git a/src/main.zig b/src/main.zig index 8ab4eda..0e895a9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,6 +10,62 @@ const udev = @import("udev_rules"); const log = std.log.scoped(.main); +fn quit() noreturn { + if (YaesuController.singleton) |controller| { + controller.quit(); + controller.control_thread.join(); + } + std.process.exit(1); +} + +const moreposix = struct { + pub extern "c" fn sigaddset(set: *std.posix.sigset_t, signo: c_int) c_int; + pub extern "c" fn sigdelset(set: *std.posix.sigset_t, signo: c_int) c_int; + pub extern "c" fn sigemptyset(set: *std.posix.sigset_t) c_int; + pub extern "c" fn sigfillset(set: *std.posix.sigset_t) c_int; + pub extern "c" fn sigismember(set: *const std.posix.sigset_t, signo: c_int) c_int; + // stdlib prototype is wrong, it doesn't take optional pointers. + pub extern "c" fn pthread_sigmask(how: c_int, noalias set: ?*const std.posix.sigset_t, noalias oldset: ?*std.posix.sigset_t) c_int; +}; + +const psigs = [_]c_int{ std.posix.SIG.INT, std.posix.SIG.HUP, std.posix.SIG.QUIT }; + +fn posixSignalHandlerThread() void { + var set: std.posix.sigset_t = undefined; + _ = moreposix.sigemptyset(&set); + for (psigs) |sig| + _ = moreposix.sigaddset(&set, sig); + + var sig: c_int = 0; + _ = std.posix.system.sigwait(&set, &sig); + log.info("Got exit signal", .{}); + quit(); +} + +// Windows runs this handler in a thread, so calling quit directly should be safe. +fn windowsEventHandler(code: std.os.windows.DWORD) callconv(std.os.windows.WINAPI) std.os.windows.BOOL { + _ = code; + log.info("Got exit signal", .{}); + quit(); +} + +fn addExitHandler() !void { + if (comptime builtin.os.tag == .windows) { + try std.os.windows.SetConsoleCtrlHandler(windowsEventHandler, true); + } else if (comptime std.Thread.use_pthreads) { + var set: std.posix.sigset_t = undefined; + _ = moreposix.sigemptyset(&set); + for (psigs) |sig| + _ = moreposix.sigaddset(&set, sig); + + _ = moreposix.pthread_sigmask(std.posix.SIG.BLOCK, &set, null); + // nobody cares about the thread + _ = try std.Thread.spawn(.{}, posixSignalHandlerThread, .{}); + } else { + log.err("not windows and not pthreads = disaster", .{}); + } +} + fn printStderr(comptime fmt: []const u8, args: anytype) void { std.debug.print(fmt ++ "\n", args); } @@ -66,9 +122,14 @@ pub fn main() !u8 { defer Config.deinit(); + addExitHandler() catch { + log.err("Could not install quit handler.", .{}); + return 1; + }; + RotCtl.run(allocator) catch |err| { log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)}); - return 1; + quit(); }; } else if (std.mem.eql(u8, args[1], commands.calibrate)) { if (args.len < 3 or args.len > 4) { @@ -86,9 +147,14 @@ pub fn main() !u8 { return 1; }; + addExitHandler() catch { + log.err("Could not install quit handler.", .{}); + return 1; + }; + YaesuController.calibrate(allocator, routine) catch |err| { log.err("Calibration failed: {s}", .{@errorName(err)}); - return 1; + quit(); }; } else if (std.mem.eql(u8, args[1], commands.help)) { if (args.len != 3) {