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),
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,
@@ -27,15 +30,14 @@ pub fn run(allocator: std.mem.Allocator) !void {
var interface: RotCtl = .{
.writer = undefined,
.running = true,
.rotator = try YaesuController.create(allocator),
.rotator = try YaesuController.init(allocator),
};
while (interface.running) {
const client = try server.accept();
defer {
log.info("disconnecting client", .{});
if (!config.rotctl.autopark)
interface.rotator.stop();
interface.rotator.stop();
client.stream.close();
}
@@ -58,11 +60,10 @@ pub fn run(allocator: std.mem.Allocator) !void {
}
// 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.control_thread.join();
}
fn write(self: *RotCtl, buf: []const u8) !void {
@@ -174,6 +175,7 @@ fn handleHamlibCommand(
if (first.len == 1 or first[0] == '\\') {
switch (first[0]) {
// NOTE: this is not technically supported by rotctld.
'q', 'Q' => try self.quit(first, &tokens),
'S' => try self.stop(first, &tokens),
'K' => try self.park(first, &tokens),
'p' => try self.getPosition(first, &tokens),
@@ -244,8 +246,8 @@ const HamlibCommand = struct {
};
const rotctl_commands = [_]HamlibCommand{
.{ .long = "quit", .callback = quit },
.{ .long = "exit", .callback = quit },
.{ .short = 'q', .callback = quit }, // quit
.{ .short = 'Q', .callback = quit }, // quit
.{ .long = "AOS", .callback = blindAck },
.{ .long = "LOS", .callback = blindAck },
.{ .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();
pub var singleton: ?*YaesuController = null;
control_thread: std.Thread,
lock: *std.Thread.Mutex,
controller: *const Controller,
@@ -25,7 +23,7 @@ pub const CalibrationRoutine = enum {
};
pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !void {
const controller = try YaesuController.create(allocator);
const controller = try YaesuController.init(allocator);
defer {
controller.quit();
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 {
if (singleton) |_| {
log.err("Controller singleton already exists.", .{});
return error.AlreadyInitialized;
}
pub fn init(allocator: std.mem.Allocator) !YaesuController {
const controller = try allocator.create(Controller);
errdefer allocator.destroy(controller);
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.
try controller.connectLabjack();
const self = try allocator.create(YaesuController);
errdefer allocator.destroy(self);
self.* = .{
return .{
.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 {
@@ -100,7 +88,7 @@ pub fn setTarget(self: YaesuController, target: AzEl) error{OutOfRange}!void {
const controller = @constCast(self.controller);
controller.target = masked_target;
controller.requestState(.running);
controller.requested_state = .running;
}
pub fn currentPosition(self: YaesuController) AzEl {
@@ -126,7 +114,7 @@ pub fn quit(self: YaesuController) void {
defer self.lock.unlock();
const controller = @constCast(self.controller);
controller.requestState(.stopped);
controller.requested_state = .stopped;
}
pub fn stop(self: YaesuController) void {
@@ -135,7 +123,7 @@ pub fn stop(self: YaesuController) void {
const controller = @constCast(self.controller);
controller.target = controller.position;
controller.requestState(.idle);
controller.requested_state = .idle;
}
pub fn startPark(self: YaesuController) void {
@@ -266,18 +254,6 @@ 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;
}
@@ -379,29 +355,35 @@ const Controller = struct {
fn run(self: *Controller) !void {
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()) {
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();
defer self.lock.unlock();
self.setPosition(self.feedback_buffer.get());
if (fbfail) self.requestState(.stopped);
self.propagateState();
}
switch (self.current_state) {
.initializing, .idle => {},
.initializing, .idle => {
self.current_state = self.requested_state;
},
.calibration => {
self.lock.lock();
defer self.lock.unlock();
// run calibration routine. psych, this does nothing. gottem
self.current_state = .idle;
self.requestState(.idle);
self.requested_state = self.current_state;
},
.running => {
const pos_error: AzEl = blk: {
@@ -434,22 +416,18 @@ const Controller = struct {
pub const LoopTimer = struct {
interval_ns: u64,
timer: std.time.Timer,
pub fn init(interval_ns: u64) LoopTimer {
return .{
.interval_ns = interval_ns,
.timer = std.time.Timer.start() catch @panic("Could not create timer"),
};
}
start: i128 = 0,
pub fn mark(self: *LoopTimer) bool {
self.timer.reset();
self.start = std.time.nanoTimestamp();
return true;
}
pub fn sleep(self: *LoopTimer) void {
const elapsed = self.timer.read();
std.time.sleep(self.interval_ns -| elapsed);
const now = std.time.nanoTimestamp();
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);
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);
}
@@ -122,14 +66,9 @@ 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)});
quit();
return 1;
};
} else if (std.mem.eql(u8, args[1], commands.calibrate)) {
if (args.len < 3 or args.len > 4) {
@@ -147,14 +86,9 @@ 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)});
quit();
return 1;
};
} else if (std.mem.eql(u8, args[1], commands.help)) {
if (args.len != 3) {