Compare commits

..

2 Commits

Author SHA1 Message Date
d4bc537cbd rotctl: actually quit when receiving the quit message 2024-07-18 21:47:50 -07:00
8037568f38 rotctl: add autopark functionality
Since gpredict doesn't have a park button or anything, this will just
automatically park the antenna when the gpredict rotator controller
disconnects. This may or may not actually be a good idea. We will see.
2024-07-18 21:47:50 -07:00
3 changed files with 37 additions and 123 deletions

View File

@@ -10,9 +10,12 @@ const log = std.log.scoped(.RotCtl);
writer: std.io.BufferedWriter(512, std.net.Stream.Writer), writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
running: bool, running: bool,
rotator: *YaesuController, rotator: YaesuController,
pub fn run(allocator: std.mem.Allocator) !void { 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( const listen_addr = try std.net.Address.parseIp(
config.rotctl.listen_address, config.rotctl.listen_address,
config.rotctl.listen_port, config.rotctl.listen_port,
@@ -27,15 +30,14 @@ pub fn run(allocator: std.mem.Allocator) !void {
var interface: RotCtl = .{ var interface: RotCtl = .{
.writer = undefined, .writer = undefined,
.running = true, .running = true,
.rotator = try YaesuController.create(allocator), .rotator = try YaesuController.init(allocator),
}; };
while (interface.running) { while (interface.running) {
const client = try server.accept(); const client = try server.accept();
defer { defer {
log.info("disconnecting client", .{}); log.info("disconnecting client", .{});
if (!config.rotctl.autopark) interface.rotator.stop();
interface.rotator.stop();
client.stream.close(); client.stream.close();
} }
@@ -58,11 +60,10 @@ pub fn run(allocator: std.mem.Allocator) !void {
} }
// loop ended due to client disconnect // loop ended due to client disconnect
if (interface.running and config.rotctl.autopark) if (interface.running and config.rotctl.autopark) {
interface.rotator.startPark(); interface.rotator.startPark();
}
} }
interface.rotator.control_thread.join();
} }
fn write(self: *RotCtl, buf: []const u8) !void { fn write(self: *RotCtl, buf: []const u8) !void {
@@ -174,6 +175,7 @@ fn handleHamlibCommand(
if (first.len == 1 or first[0] == '\\') { if (first.len == 1 or first[0] == '\\') {
switch (first[0]) { switch (first[0]) {
// NOTE: this is not technically supported by rotctld. // NOTE: this is not technically supported by rotctld.
'q', 'Q' => try self.quit(first, &tokens),
'S' => try self.stop(first, &tokens), 'S' => try self.stop(first, &tokens),
'K' => try self.park(first, &tokens), 'K' => try self.park(first, &tokens),
'p' => try self.getPosition(first, &tokens), 'p' => try self.getPosition(first, &tokens),
@@ -244,8 +246,8 @@ const HamlibCommand = struct {
}; };
const rotctl_commands = [_]HamlibCommand{ const rotctl_commands = [_]HamlibCommand{
.{ .long = "quit", .callback = quit }, .{ .short = 'q', .callback = quit }, // quit
.{ .long = "exit", .callback = quit }, .{ .short = 'Q', .callback = quit }, // quit
.{ .long = "AOS", .callback = blindAck }, .{ .long = "AOS", .callback = blindAck },
.{ .long = "LOS", .callback = blindAck }, .{ .long = "LOS", .callback = blindAck },
.{ .short = 'P', .long = "set_pos", .callback = setPosition }, // azimuth: f64, elevation: f64 .{ .short = 'P', .long = "set_pos", .callback = setPosition }, // azimuth: f64, elevation: f64

View File

@@ -8,8 +8,6 @@ const log = std.log.scoped(.yaesu_controller);
const YaesuController = @This(); const YaesuController = @This();
pub var singleton: ?*YaesuController = null;
control_thread: std.Thread, control_thread: std.Thread,
lock: *std.Thread.Mutex, lock: *std.Thread.Mutex,
controller: *const Controller, controller: *const Controller,
@@ -25,7 +23,7 @@ pub const CalibrationRoutine = enum {
}; };
pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !void { pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !void {
const controller = try YaesuController.create(allocator); const controller = try YaesuController.init(allocator);
defer { defer {
controller.quit(); controller.quit();
controller.control_thread.join(); controller.control_thread.join();
@@ -37,12 +35,7 @@ pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !voi
} }
} }
pub fn create(allocator: std.mem.Allocator) !*YaesuController { pub fn init(allocator: std.mem.Allocator) !YaesuController {
if (singleton) |_| {
log.err("Controller singleton already exists.", .{});
return error.AlreadyInitialized;
}
const controller = try allocator.create(Controller); const controller = try allocator.create(Controller);
errdefer allocator.destroy(controller); errdefer allocator.destroy(controller);
controller.* = try Controller.init(allocator); controller.* = try Controller.init(allocator);
@@ -50,16 +43,11 @@ pub fn create(allocator: std.mem.Allocator) !*YaesuController {
// do this in the main thread so we can throw the error about it synchronously. // do this in the main thread so we can throw the error about it synchronously.
try controller.connectLabjack(); try controller.connectLabjack();
const self = try allocator.create(YaesuController); return .{
errdefer allocator.destroy(self);
self.* = .{
.control_thread = try std.Thread.spawn(.{}, runController, .{controller}), .control_thread = try std.Thread.spawn(.{}, runController, .{controller}),
.lock = &controller.lock, .lock = &controller.lock,
.controller = controller, .controller = controller,
}; };
singleton = self;
return self;
} }
fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool { fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool {
@@ -100,7 +88,7 @@ pub fn setTarget(self: YaesuController, target: AzEl) error{OutOfRange}!void {
const controller = @constCast(self.controller); const controller = @constCast(self.controller);
controller.target = masked_target; controller.target = masked_target;
controller.requestState(.running); controller.requested_state = .running;
} }
pub fn currentPosition(self: YaesuController) AzEl { pub fn currentPosition(self: YaesuController) AzEl {
@@ -126,7 +114,7 @@ pub fn quit(self: YaesuController) void {
defer self.lock.unlock(); defer self.lock.unlock();
const controller = @constCast(self.controller); const controller = @constCast(self.controller);
controller.requestState(.stopped); controller.requested_state = .stopped;
} }
pub fn stop(self: YaesuController) void { pub fn stop(self: YaesuController) void {
@@ -135,7 +123,7 @@ pub fn stop(self: YaesuController) void {
const controller = @constCast(self.controller); const controller = @constCast(self.controller);
controller.target = controller.position; controller.target = controller.position;
controller.requestState(.idle); controller.requested_state = .idle;
} }
pub fn startPark(self: YaesuController) void { pub fn startPark(self: YaesuController) void {
@@ -266,18 +254,6 @@ const Controller = struct {
self.labjack.id = info.local_id; 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 { fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle; return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle;
} }
@@ -379,29 +355,35 @@ const Controller = struct {
fn run(self: *Controller) !void { fn run(self: *Controller) !void {
self.current_state = .initializing; self.current_state = .initializing;
var timer = LoopTimer.init(config.controller.loop_interval_ns); var timer: LoopTimer = .{ .interval_ns = config.controller.loop_interval_ns };
while (timer.mark()) : (timer.sleep()) { while (timer.mark()) : (timer.sleep()) {
const fbfail = if (self.updateFeedback()) |_| false else |_| true; self.updateFeedback() catch {
self.lock.lock();
defer self.lock.unlock();
self.current_state = .stopped;
continue;
};
{ {
self.lock.lock(); self.lock.lock();
defer self.lock.unlock(); defer self.lock.unlock();
self.setPosition(self.feedback_buffer.get()); self.setPosition(self.feedback_buffer.get());
if (fbfail) self.requestState(.stopped);
self.propagateState();
} }
switch (self.current_state) { switch (self.current_state) {
.initializing, .idle => {}, .initializing, .idle => {
self.current_state = self.requested_state;
},
.calibration => { .calibration => {
self.lock.lock(); self.lock.lock();
defer self.lock.unlock(); defer self.lock.unlock();
// run calibration routine. psych, this does nothing. gottem // run calibration routine. psych, this does nothing. gottem
self.current_state = .idle; self.current_state = .idle;
self.requestState(.idle); self.requested_state = self.current_state;
}, },
.running => { .running => {
const pos_error: AzEl = blk: { const pos_error: AzEl = blk: {
@@ -434,22 +416,18 @@ const Controller = struct {
pub const LoopTimer = struct { pub const LoopTimer = struct {
interval_ns: u64, interval_ns: u64,
timer: std.time.Timer,
pub fn init(interval_ns: u64) LoopTimer { start: i128 = 0,
return .{
.interval_ns = interval_ns,
.timer = std.time.Timer.start() catch @panic("Could not create timer"),
};
}
pub fn mark(self: *LoopTimer) bool { pub fn mark(self: *LoopTimer) bool {
self.timer.reset(); self.start = std.time.nanoTimestamp();
return true; return true;
} }
pub fn sleep(self: *LoopTimer) void { pub fn sleep(self: *LoopTimer) void {
const elapsed = self.timer.read(); const now = std.time.nanoTimestamp();
std.time.sleep(self.interval_ns -| elapsed); const elapsed: u64 = @intCast(now - self.start);
std.time.sleep(self.interval_ns - elapsed);
} }
}; };

View File

@@ -10,62 +10,6 @@ const udev = @import("udev_rules");
const log = std.log.scoped(.main); 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 { fn printStderr(comptime fmt: []const u8, args: anytype) void {
std.debug.print(fmt ++ "\n", args); std.debug.print(fmt ++ "\n", args);
} }
@@ -122,14 +66,9 @@ pub fn main() !u8 {
defer Config.deinit(); defer Config.deinit();
addExitHandler() catch {
log.err("Could not install quit handler.", .{});
return 1;
};
RotCtl.run(allocator) catch |err| { RotCtl.run(allocator) catch |err| {
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)}); log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
quit(); return 1;
}; };
} else if (std.mem.eql(u8, args[1], commands.calibrate)) { } else if (std.mem.eql(u8, args[1], commands.calibrate)) {
if (args.len < 3 or args.len > 4) { if (args.len < 3 or args.len > 4) {
@@ -147,14 +86,9 @@ pub fn main() !u8 {
return 1; return 1;
}; };
addExitHandler() catch {
log.err("Could not install quit handler.", .{});
return 1;
};
YaesuController.calibrate(allocator, routine) catch |err| { YaesuController.calibrate(allocator, routine) catch |err| {
log.err("Calibration failed: {s}", .{@errorName(err)}); log.err("Calibration failed: {s}", .{@errorName(err)});
quit(); return 1;
}; };
} else if (std.mem.eql(u8, args[1], commands.help)) { } else if (std.mem.eql(u8, args[1], commands.help)) {
if (args.len != 3) { if (args.len != 3) {