Compare commits

..

3 Commits

Author SHA1 Message Date
38a286da11 rotctl: actually quit when receiving the quit message 2024-07-18 19:44:41 -07:00
6bd401d9c8 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 19:44:41 -07:00
0ccc65fd10 controller: restructure control loop
This should have been multiple commits, but it isn't. Sue me. This
change has two main goals:

1. Sample feedback at the beginning of the control loop iteration so
   that it is always up-to-date when we are computing the actual drive
   outputs. This means we're doing twice the amount of communication
   with the labjack (previously, setting the output and reading the
   feedback was done with a singe command). However, this makes the
   loop structure much more standard, and it means that we aren't
   constantly operating on feedback that is stale by one loop
   interval.

2. Sample feedback into a (configurable size) buffer. This lets us
   operate on aggregated feedback rather than on a single instantaneous
   data point. Right now, feedback is computed as a moving average,
   which acts as a rudimentary low-pass filter, reducing spurious
   single-loop actions due to feedback spikes or other noise. However,
   the other reason to aggregate some backwards data is that it will
   let us do automatic stall detection in a simple way, although that
   is not currently done.
2024-07-18 19:43:14 -07:00
4 changed files with 40 additions and 128 deletions

View File

@@ -92,7 +92,7 @@ pub fn validate(self: Config, err_writer: anytype) !void {
rotctl: RotControlConfig = .{
.listen_address = "127.0.0.1",
.listen_port = 4533,
.autopark = false,
.autopark = true,
},
labjack: LabjackConfig = .{
.device = .autodetect,

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,33 @@ 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.setPosition(self.feedback_buffer.get());
if (fbfail) self.requestState(.stopped);
self.propagateState();
}
self.current_state = .stopped;
continue;
};
self.lock.lock();
defer self.lock.unlock();
self.setPosition(self.feedback_buffer.get());
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 +414,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) {