Compare commits

...

16 Commits

Author SHA1 Message Date
4895c94d90 main: handle various forms of process termination more safely
This attempts to solve the problem where, if the rotator is actively
rotating and the program is killed, the LabJack will not be reset and
the rotator will keep running. We install various signal handlers to
try to catch common cases (ctrl+c, terminal getting closed out from
under us). There are still ways to end the process that leave the
LabJack running (such as if it crashes, though there are currently no
known crashes), but I don't think it's possible to completely avoid
that.

The posix signal handling story is a bit ugly. Trying to do pretty much
anything in the asynchronous signal handlers will cause undefined
behavior. Acquiring a mutex and joining a thread are right at the top
of the very long list of things that you cannot do safely in an async
signal handler. One potential solution to this problem is to replace
locks with atomics, which isn't appropriate for our use case (we have
to make sure the controller thread actually has shut down the LabJack
before exiting the process). The other well-known solution is to
manually create a thread that listens for signals synchronously, and
this is the approach taken here.

After having done this, I had the thought that because we are linking
libc anyway, an `atexit` handler might work, but I don't actually know
if it would, and I don't think it's worthwhile to do the work at this
point.
2024-08-01 13:51:38 -07:00
c8cfc95938 controller: fix bogus timer implementation
The previous version was using wall clock time for the timer because I
am an idiot. During time syncs or possibly due to other reasons, this
could jump backwards and cause an overflow. This obviously needed a
monotonic clock source, and now it has one.
2024-07-30 12:59:36 -07:00
a3b4ffc76d rotctl: only support long quit command
gpredict actually sends either q or Q when disconnecting. This is not
actually a supported command according to my reading of the rotctld
documentation. `q`/`Q` for quitting is limited to the interactive
rotctl prompt.

For autoparking, we don't want to quit when gpredict disconnects. Also
in general, we probably don't want to quit when gpredict disconnects.
I still want to have a quit command when using this via netcat or
whatever, so make them a form gpredict probably does not send.
2024-07-18 23:36:09 -07:00
c295c941e9 rotctl: actually quit when receiving the quit message 2024-07-18 23:36:09 -07:00
de487d18c5 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 23:36:09 -07:00
61c10df63d 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 21:47:50 -07:00
153dde40aa readme: the Windows situation has been altered
Pray I do not alter it further.
2024-07-15 17:55:31 -07:00
4777d04594 build: disable libusb logging by default
It is quite verbose and not very useful.
2024-07-15 17:55:31 -07:00
eb7ad4ef9e main: hook up calibration stubs
I guess I will be finishing this later.
2024-07-15 17:55:31 -07:00
2194dd4a8c config: make it possible not to leak
Using the GPA is a bit annoying sometimes. The other option would be to
just use page allocator to allocate the config and bypass the GPA.
2024-07-15 17:55:31 -07:00
de76cce706 controller: make controller info printout more useful
This has a lot more relevant information now. Anyway, this has been
tested on real hardware, and it appears to work pretty well. I am
considering changing the control loop so that it isn't always
operating on stale feedback (two LabJack calls per loop when actively
controlling pointing). Also the calibration routines need to be
implemented.
2024-07-15 17:55:31 -07:00
e5d8a716b0 improve a name 2024-07-11 21:55:34 -07:00
011f300f0a windows: hatred rising
The lib files are of course stubs and not actually static libraries,
which means we have to distribute the DLL as well. Unfortunately.
Incredibly bad operating system. If this doesn't work, I quit.
2024-07-10 22:28:49 -07:00
f1480bca45 build: link windows-native driver when targeting windows
This is completely experimental, but since the API is the same, it's a
drop-in replacement. The Windows driver is not apparently open source,
so I am vendoring the static libraries provided. In theory, this will
obviate the need to replace the HID driver on Windows and will thus
require no initial setup, but I cannot currently test it beyond making
sure it compiles.
2024-07-10 18:22:47 -07:00
e4393c2e5a config: change default control loop interval
On an unloaded rotator, this fixes the rapid relay toggling without
needing to implement debouncing.

An additional configuration validation should probably be added: the
rotator rotates in azimuth about 6 deg per second. If the control loop
speed is too slow or the controller angle tolerance is small enough,
then the controller will never be able settle (imagine that the loop
interval is 1 second. That means the rotator will move approximately 6
degrees every loop iteration, so it will always overshoot.
2024-07-10 12:43:11 -07:00
ccb507d4d9 cli: provide more useful help text 2024-07-10 12:39:18 -07:00
16 changed files with 2605 additions and 450 deletions

View File

@@ -4,12 +4,24 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const use_udev = b.option(
const libusb_use_udev = b.option(
bool,
"use_udev",
"link and use udev (Linux only. Default: false)",
) orelse false;
const libusb_enable_logging = b.option(
bool,
"libusb_enable_logging",
"enable libusb's built-in logging (Default: false)",
) orelse false;
const libusb_enable_debug_logging = b.option(
bool,
"libusb_enable_debug_logging",
"enable libusb's debug logging (Default: false)",
) orelse false;
const exe = b.addExecutable(.{
.name = "yaes",
.root_source_file = b.path("src/main.zig"),
@@ -17,11 +29,38 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
const ljacklm_dep = b.dependency(
"ljacklm",
.{ .target = target, .optimize = optimize, .use_udev = use_udev },
);
exe.linkLibrary(ljacklm_dep.artifact("ljacklm"));
if (target.result.os.tag == .windows) {
if (target.result.cpu.arch == .x86) {
exe.addObjectFile(b.path("deps/labjack/windows/ljackuw32.lib"));
b.getInstallStep().dependOn(
&b.addInstallBinFile(
b.path("deps/labjack/windows/ljackuw32.dll"),
"ljackuw.dll",
).step,
);
} else if (target.result.cpu.arch == .x86_64) {
exe.addObjectFile(b.path("deps/labjack/windows/ljackuw64.lib"));
b.getInstallStep().dependOn(
&b.addInstallBinFile(
b.path("deps/labjack/windows/ljackuw64.dll"),
"ljackuw.dll",
).step,
);
} else @panic("Unsupported CPU arch for Windows build (must be x86 or x86_64).");
} else {
const ljacklm_dep = b.dependency(
"ljacklm",
.{
.target = target,
.optimize = optimize,
.libusb_use_udev = libusb_use_udev,
.libusb_enable_logging = libusb_enable_logging,
.libusb_enable_debug_logging = libusb_enable_debug_logging,
},
);
exe.linkLibrary(ljacklm_dep.artifact("ljacklm"));
}
exe.root_module.addImport(
"udev_rules",
b.addModule("udev_rules", .{

View File

@@ -4,10 +4,22 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const use_udev = b.option(
const libusb_use_udev = b.option(
bool,
"use_udev",
"link and use udev (Linux only. Default: false)",
"libusb_use_udev",
"libusb: link and use udev (Linux only. Default: false)",
) orelse false;
const libusb_enable_logging = b.option(
bool,
"libusb_enable_logging",
"enable libusb's built-in logging (Default: false)",
) orelse false;
const libusb_enable_debug_logging = b.option(
bool,
"libusb_enable_debug_logging",
"enable libusb's debug logging (Default: false)",
) orelse false;
const liblabjackusb = b.addStaticLibrary(.{
@@ -31,7 +43,13 @@ pub fn build(b: *std.Build) !void {
const usb_dep = b.dependency(
"usb",
.{ .target = target, .optimize = optimize, .use_udev = use_udev },
.{
.target = target,
.optimize = optimize,
.use_udev = libusb_use_udev,
.enable_logging = libusb_enable_logging,
.enable_debug_logging = libusb_enable_debug_logging,
},
);
liblabjackusb.linkLibrary(usb_dep.artifact("usb"));

View File

@@ -4,10 +4,22 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const use_udev = b.option(
const libusb_use_udev = b.option(
bool,
"use_udev",
"link and use udev (Linux only. Default: false)",
"libusb_use_udev",
"libusb: link and use udev (Linux only. Default: false)",
) orelse false;
const libusb_enable_logging = b.option(
bool,
"libusb_enable_logging",
"enable libusb's built-in logging (Default: false)",
) orelse false;
const libusb_enable_debug_logging = b.option(
bool,
"libusb_enable_debug_logging",
"enable libusb's debug logging (Default: false)",
) orelse false;
const libljacklm = b.addStaticLibrary(.{
@@ -30,7 +42,13 @@ pub fn build(b: *std.Build) !void {
const usb_dep = b.dependency(
"labjackusb",
.{ .target = target, .optimize = optimize, .use_udev = use_udev },
.{
.target = target,
.optimize = optimize,
.libusb_use_udev = libusb_use_udev,
.libusb_enable_logging = libusb_enable_logging,
.libusb_enable_debug_logging = libusb_enable_debug_logging,
},
);
libljacklm.linkLibrary(usb_dep.artifact("labjackusb"));

1667
deps/labjack/windows/ljackuw.h vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
deps/labjack/windows/ljackuw32.dll vendored Normal file

Binary file not shown.

BIN
deps/labjack/windows/ljackuw32.lib vendored Normal file

Binary file not shown.

BIN
deps/labjack/windows/ljackuw64.dll vendored Normal file

Binary file not shown.

BIN
deps/labjack/windows/ljackuw64.lib vendored Normal file

Binary file not shown.

16
deps/libusb/build.zig vendored
View File

@@ -10,6 +10,18 @@ pub fn build(b: *std.Build) !void {
"link and use udev (Linux only. Default: false)",
) orelse false;
const enable_logging = b.option(
bool,
"enable_logging",
"enable libusb's built-in logging (Default: false)",
) orelse false;
const enable_debug_logging = b.option(
bool,
"enable_debug_logging",
"enable libusb's debug logging (Default: false)",
) orelse false;
const libusb = b.addStaticLibrary(.{
.name = "usb",
.target = target,
@@ -59,8 +71,8 @@ pub fn build(b: *std.Build) !void {
.{ .style = .{ .autoconf = b.path("config.h.in") } },
.{
.DEFAULT_VISIBILITY = .@"__attribute__ ((visibility (\"default\")))",
.ENABLE_DEBUG_LOGGING = oneOrNull(optimize == .Debug),
.ENABLE_LOGGING = oneOrNull(optimize == .Debug),
.ENABLE_DEBUG_LOGGING = oneOrNull(enable_debug_logging),
.ENABLE_LOGGING = oneOrNull(enable_logging),
.HAVE_ASM_TYPES_H = null,
.HAVE_CLOCK_GETTIME = oneOrNull(linux_target),
.HAVE_DECL_EFD_CLOEXEC = oneOrNull(linux_target),

View File

@@ -24,7 +24,7 @@ Unfortunately, all platforms have additional steps that must be taken (some easi
#### Windows
This compiles for, and runs on, Windows. However, the Labjack U12 by default gets assigned the Windows USB HID driver, which causes the USB report descriptor control transfer read to get mangled for mystery reasons that presumably made sense to some egghead at Microsoft at some point in time. Fortunately, this can be relatively easily fixed by using a tool like [zadig] to set the Labjack U12 to use the WinUSB driver instead of the HID driver. This is not an endorsement of the above outlined process, but rather an explanation for persons either foolish or desperate.
This compiles for, and runs on, Windows. However, in order for it to work out of the box, it has to link against the (proprietary) Windows LabJack U12 driver instead of the open source libUSB driver. This has two main limitations: the Windows driver is only available for the `x86` and `x86-64` architectures, and it is exclusively distributed as a dynamic library, meaning that `yaes.exe` must be distributed alongside `ljackuw.dll`. When building for Windows targets, the appropriate library file will be copied to the binary installation directory (`zig-out/bin` by default).
#### macOS
@@ -33,5 +33,3 @@ This works on macOS, though it has to be run with `sudo`, as access to the USB h
#### Linux
You probably need to install the included udev rules file in order for the USB device to be accessible as a user. This is buried in the source tree as `deps/labjack/exodriver/90-labjack.rules`. These should probably go in `/etc/udev/rules.d` if you are installing them manually and I have properly understood the various Linux folder conventions.
[zadig]: https://zadig.akeo.ie

View File

@@ -1,35 +1,41 @@
const std = @import("std");
const AzEl = @import("./LabjackYaesu.zig").AzEl;
const AzEl = @import("./YaesuController.zig").AzEl;
const lj = @import("./labjack.zig");
const Config = @This();
var global_internal: Config = undefined;
pub const global: *const Config = &global_internal;
var global_internal: std.json.Parsed(Config) = undefined;
pub const global: *const Config = &global_internal.value;
pub fn load(allocator: std.mem.Allocator, reader: anytype, err_writer: anytype) !void {
var jread = std.json.Reader(1024, @TypeOf(reader)).init(allocator, reader);
defer jread.deinit();
global_internal = try std.json.parseFromTokenSourceLeaky(
global_internal = try std.json.parseFromTokenSource(
Config,
allocator,
&jread,
.{},
);
try global_internal.validate(err_writer);
try global.validate(err_writer);
}
pub fn loadDefault(allocator: std.mem.Allocator) void {
_ = allocator;
global_internal = .{};
const arena = allocator.create(std.heap.ArenaAllocator) catch unreachable;
arena.* = std.heap.ArenaAllocator.init(allocator);
global_internal = .{
.arena = arena,
.value = .{},
};
}
pub fn destroy(allocator: std.mem.Allocator) void {
pub fn deinit() void {
// TODO: implement this probably
_ = allocator;
const allocator = global_internal.arena.child_allocator;
global_internal.arena.deinit();
allocator.destroy(global_internal.arena);
}
pub fn validate(self: Config, err_writer: anytype) !void {
@@ -86,6 +92,7 @@ pub fn validate(self: Config, err_writer: anytype) !void {
rotctl: RotControlConfig = .{
.listen_address = "127.0.0.1",
.listen_port = 4533,
.autopark = false,
},
labjack: LabjackConfig = .{
.device = .autodetect,
@@ -114,13 +121,14 @@ controller: ControllerConfig = .{
.elevation_input = .{ .channel = .diff_23, .range = .@"5 V" },
.azimuth_outputs = .{ .increase = .{ .io = 0 }, .decrease = .{ .io = 1 } },
.elevation_outputs = .{ .increase = .{ .io = 2 }, .decrease = .{ .io = 3 } },
.loop_interval_ns = 50_000_000,
.loop_interval_ns = 100_000_000,
.parking_posture = .{ .azimuth = 180, .elevation = 90 },
.angle_tolerance = .{ .azimuth = 1, .elevation = 1 },
.angle_offset = .{ .azimuth = 0, .elevation = 0 },
// this is a symmetric mask, so the minimum usable elevation is elevation_mask deg
// and the maximum usable elevation is 180 - elevation_mask deg
.elevation_mask = 0.0,
.feedback_window_samples = 3,
},
pub const VoltAngle = struct { voltage: f64, angle: f64 };
@@ -144,6 +152,7 @@ pub const MinMax = struct {
const RotControlConfig = struct {
listen_address: []const u8,
listen_port: u16,
autopark: bool,
};
const LabjackConfig = struct {
@@ -173,6 +182,8 @@ const ControllerConfig = struct {
angle_offset: AzEl,
elevation_mask: f64,
feedback_window_samples: u8,
const OutPair = struct {
increase: lj.DigitalOutputChannel,
decrease: lj.DigitalOutputChannel,

View File

@@ -1,313 +0,0 @@
const std = @import("std");
const lj = @import("./labjack.zig");
const Config = @import("./Config.zig");
const config = Config.global;
const log = std.log.scoped(.labjack_yaesu);
const LabjackYaesu = @This();
control_thread: std.Thread,
lock: *std.Thread.Mutex,
controller: *const Controller,
pub const AzEl = struct {
azimuth: f64,
elevation: f64,
};
pub fn init(allocator: std.mem.Allocator) !LabjackYaesu {
const lock = try allocator.create(std.Thread.Mutex);
errdefer allocator.destroy(lock);
lock.* = .{};
const controller = try allocator.create(Controller);
errdefer allocator.destroy(controller);
controller.init(lock);
// do this in the main thread so we can throw the error about it synchronously.
try controller.connectLabjack();
return .{
.control_thread = try std.Thread.spawn(.{}, runController, .{controller}),
.lock = lock,
.controller = controller,
};
}
fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool {
return switch (dof) {
// zig fmt: off
.azimuth => request >= (
config.labjack.feedback_calibration.azimuth.minimum.angle
+ config.controller.angle_offset.azimuth
) and request <= (
config.labjack.feedback_calibration.azimuth.maximum.angle
+ config.controller.angle_offset.azimuth
),
.elevation => request >= (
config.labjack.feedback_calibration.elevation.minimum.angle
+ config.controller.angle_offset.elevation
) and request <= (
config.labjack.feedback_calibration.elevation.maximum.angle
+ config.controller.angle_offset.elevation
),
// zig fmt: on
};
}
pub fn setTarget(self: LabjackYaesu, target: AzEl) error{OutOfRange}!void {
self.lock.lock();
defer self.lock.unlock();
const masked_target: AzEl = .{
.azimuth = target.azimuth,
.elevation = @min(
@max(target.elevation, config.controller.elevation_mask),
180.0 - config.controller.elevation_mask,
),
};
if (!inRange(masked_target.azimuth, .azimuth) or !inRange(masked_target.elevation, .elevation))
return error.OutOfRange;
const controller = @constCast(self.controller);
controller.target = masked_target;
controller.requested_state = .running;
}
pub fn currentPosition(self: LabjackYaesu) AzEl {
self.lock.lock();
defer self.lock.unlock();
return self.controller.position;
}
pub fn startCalibration(self: LabjackYaesu) void {
// there are two different types of calibration:
// 1. feedback calibration, running to the extents of the rotator
// 2. sun calibration, which determines the azimuth and elevation angle
// offset between the rotator's physical stops and geodetic north
//
// The former is (fairly) trivial to automate, just run until stall
// (assuming there's no deadband in the feedback). The latter requires
// manual input as the human is the feedback hardware in the loop.
_ = self;
}
pub fn quit(self: LabjackYaesu) void {
self.lock.lock();
defer self.lock.unlock();
const controller = @constCast(self.controller);
controller.requested_state = .stopped;
}
pub fn stop(self: LabjackYaesu) void {
self.lock.lock();
defer self.lock.unlock();
const controller = @constCast(self.controller);
controller.target = controller.position;
controller.requested_state = .idle;
}
pub fn startPark(self: LabjackYaesu) void {
self.setTarget(config.controller.parking_posture) catch unreachable;
}
fn runController(controller: *Controller) void {
controller.run() catch {
log.err(
"the labjack control loop has terminated unexpectedly!!!!",
.{},
);
};
}
const Controller = struct {
target: AzEl,
position: AzEl,
current_state: ControllerState,
requested_state: ControllerState,
lock: *std.Thread.Mutex,
labjack: lj.Labjack,
const ControllerState = enum {
initializing,
idle,
calibration,
running,
stopped,
};
fn init(self: *Controller, lock: *std.Thread.Mutex) void {
self.* = .{
.target = .{ .azimuth = 0, .elevation = 0 },
.position = .{ .azimuth = 0, .elevation = 0 },
.current_state = .stopped,
.requested_state = .idle,
.lock = lock,
.labjack = switch (config.labjack.device) {
.autodetect => lj.Labjack.autodetect(),
.serial_number => |sn| lj.Labjack.with_serial_number(sn),
},
};
}
fn connectLabjack(self: *Controller) !void {
const info = try self.labjack.connect();
try self.labjack.setAllDigitalOutputLow();
self.labjack.id = info.local_id;
}
fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle;
}
fn lerpAndOffsetAngles(input: [2]lj.AnalogReadResult) AzEl {
return .{
.azimuth = lerpOne(
input[0].voltage,
config.labjack.feedback_calibration.azimuth,
) + config.controller.angle_offset.azimuth,
.elevation = lerpOne(
input[1].voltage,
config.labjack.feedback_calibration.elevation,
) + config.controller.angle_offset.elevation,
};
}
fn signDeadzone(offset: f64, deadzone: f64) enum { negative, zero, positive } {
return if (@abs(offset) < deadzone)
.zero
else if (offset < 0)
.negative
else
.positive;
}
fn updateAzEl(self: *const Controller) !AzEl {
const inputs = .{ config.controller.azimuth_input, config.controller.elevation_input };
const raw = try self.labjack.readAnalogWriteDigital(
2,
inputs,
.{false} ** 4,
true,
);
return lerpAndOffsetAngles(raw);
}
fn drive(self: *const Controller, pos_error: AzEl) !AzEl {
// NOTE: feedback will be roughly config.controller.loop_interval_ns out of
// date. For high loop rates, this shouldn't be an issue.
const inputs = .{ config.controller.azimuth_input, config.controller.elevation_input };
var drive_signal: [4]bool = .{false} ** 4;
const azsign = signDeadzone(
pos_error.azimuth,
config.controller.angle_tolerance.azimuth,
);
const elsign = signDeadzone(
pos_error.elevation,
config.controller.angle_tolerance.elevation,
);
drive_signal[config.controller.azimuth_outputs.increase.io] = azsign == .positive;
drive_signal[config.controller.azimuth_outputs.decrease.io] = azsign == .negative;
drive_signal[config.controller.elevation_outputs.increase.io] = elsign == .positive;
drive_signal[config.controller.elevation_outputs.decrease.io] = elsign == .negative;
log.info("drive: az = {s}, el = {s}. outputs: {any}", .{ @tagName(azsign), @tagName(elsign), drive_signal });
const raw = try self.labjack.readAnalogWriteDigital(2, inputs, drive_signal, true);
return lerpAndOffsetAngles(raw);
}
fn run(self: *Controller) !void {
self.current_state = .initializing;
var timer: LoopTimer = .{ .interval_ns = config.controller.loop_interval_ns };
while (timer.mark()) : (timer.sleep()) switch (self.current_state) {
.initializing, .idle => {
const pos = self.updateAzEl() catch {
self.lock.lock();
defer self.lock.unlock();
self.current_state = .stopped;
continue;
};
self.lock.lock();
defer self.lock.unlock();
self.position = pos;
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.requested_state = self.current_state;
},
.running => {
const pos_error: AzEl = blk: {
self.lock.lock();
defer self.lock.unlock();
break :blk .{
.azimuth = self.target.azimuth - self.position.azimuth,
.elevation = self.target.elevation - self.position.elevation,
};
};
const pos = self.drive(pos_error) catch {
self.lock.lock();
defer self.lock.unlock();
self.current_state = .stopped;
continue;
};
self.lock.lock();
defer self.lock.unlock();
self.position = pos;
self.current_state = self.requested_state;
},
.stopped => {
// attempt to reset the drive outputs
_ = self.updateAzEl() catch {};
break;
},
};
}
};
pub const LoopTimer = struct {
interval_ns: u64,
start: i128 = 0,
pub fn mark(self: *LoopTimer) bool {
self.start = std.time.nanoTimestamp();
return true;
}
pub fn sleep(self: *LoopTimer) void {
const now = std.time.nanoTimestamp();
const elapsed: u64 = @intCast(now - self.start);
std.time.sleep(self.interval_ns - elapsed);
}
};

View File

@@ -2,7 +2,7 @@ const std = @import("std");
const Config = @import("./Config.zig");
const config = Config.global;
const LabjackYaesu = @import("./LabjackYaesu.zig");
const YaesuController = @import("./YaesuController.zig");
const RotCtl = @This();
@@ -10,12 +10,9 @@ const log = std.log.scoped(.RotCtl);
writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
running: bool,
rotator: LabjackYaesu,
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,20 +27,19 @@ pub fn run(allocator: std.mem.Allocator) !void {
var interface: RotCtl = .{
.writer = undefined,
.running = true,
.rotator = try LabjackYaesu.init(allocator),
.rotator = try YaesuController.create(allocator),
};
while (true) {
while (interface.running) {
const client = try server.accept();
defer {
log.info("disconnecting client", .{});
interface.rotator.stop();
if (!config.rotctl.autopark)
interface.rotator.stop();
client.stream.close();
}
interface.writer = .{ .unbuffered_writer = client.stream.writer() };
interface.running = true;
defer interface.running = false;
log.info("client connected from {}", .{client.address});
@@ -60,7 +56,13 @@ pub fn run(allocator: std.mem.Allocator) !void {
std.mem.trim(u8, fbs.getWritten(), &std.ascii.whitespace),
) catch break;
}
// loop ended due to client disconnect
if (interface.running and config.rotctl.autopark)
interface.rotator.startPark();
}
interface.rotator.control_thread.join();
}
fn write(self: *RotCtl, buf: []const u8) !void {
@@ -172,7 +174,6 @@ 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),
@@ -243,8 +244,8 @@ const HamlibCommand = struct {
};
const rotctl_commands = [_]HamlibCommand{
.{ .short = 'q', .callback = quit }, // quit
.{ .short = 'Q', .callback = quit }, // quit
.{ .long = "quit", .callback = quit },
.{ .long = "exit", .callback = quit },
.{ .long = "AOS", .callback = blindAck },
.{ .long = "LOS", .callback = blindAck },
.{ .short = 'P', .long = "set_pos", .callback = setPosition }, // azimuth: f64, elevation: f64

455
src/YaesuController.zig Normal file
View File

@@ -0,0 +1,455 @@
const std = @import("std");
const lj = @import("./labjack.zig");
const Config = @import("./Config.zig");
const config = Config.global;
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,
pub const AzEl = struct {
azimuth: f64,
elevation: f64,
};
pub const CalibrationRoutine = enum {
feedback,
orientation,
};
pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !void {
const controller = try YaesuController.create(allocator);
defer {
controller.quit();
controller.control_thread.join();
}
switch (routine) {
.feedback => try controller.calibrate_feedback(),
.orientation => try controller.calibrate_orientation(),
}
}
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);
errdefer controller.deinit(allocator);
// 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.* = .{
.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 {
return switch (dof) {
// zig fmt: off
.azimuth => request >= (
config.labjack.feedback_calibration.azimuth.minimum.angle
+ config.controller.angle_offset.azimuth
) and request <= (
config.labjack.feedback_calibration.azimuth.maximum.angle
+ config.controller.angle_offset.azimuth
),
.elevation => request >= (
config.labjack.feedback_calibration.elevation.minimum.angle
+ config.controller.angle_offset.elevation
) and request <= (
config.labjack.feedback_calibration.elevation.maximum.angle
+ config.controller.angle_offset.elevation
),
// zig fmt: on
};
}
pub fn setTarget(self: YaesuController, target: AzEl) error{OutOfRange}!void {
self.lock.lock();
defer self.lock.unlock();
const masked_target: AzEl = .{
.azimuth = target.azimuth,
.elevation = @min(
@max(target.elevation, config.controller.elevation_mask),
180.0 - config.controller.elevation_mask,
),
};
if (!inRange(masked_target.azimuth, .azimuth) or !inRange(masked_target.elevation, .elevation))
return error.OutOfRange;
const controller = @constCast(self.controller);
controller.target = masked_target;
controller.requestState(.running);
}
pub fn currentPosition(self: YaesuController) AzEl {
self.lock.lock();
defer self.lock.unlock();
return self.controller.position;
}
pub fn waitForUpdate(self: YaesuController) AzEl {
const controller = @constCast(self.controller);
self.lock.lock();
defer self.lock.unlock();
controller.condition.wait(self.lock);
return controller.position;
}
pub fn quit(self: YaesuController) void {
self.lock.lock();
defer self.lock.unlock();
const controller = @constCast(self.controller);
controller.requestState(.stopped);
}
pub fn stop(self: YaesuController) void {
self.lock.lock();
defer self.lock.unlock();
const controller = @constCast(self.controller);
controller.target = controller.position;
controller.requestState(.idle);
}
pub fn startPark(self: YaesuController) void {
self.setTarget(config.controller.parking_posture) catch unreachable;
}
fn calibrate_feedback(self: YaesuController) !void {
_ = self;
log.err("this isn't implemented yet, sorry.", .{});
return error.NotImplemented;
}
fn calibrate_orientation(self: YaesuController) !void {
_ = self;
log.err("this isn't implemented yet, sorry.", .{});
return error.NotImplemented;
}
fn runController(controller: *Controller) void {
controller.run() catch {
log.err(
"the rotator control loop has terminated unexpectedly!!!!",
.{},
);
};
}
const FeedbackBuffer = struct {
samples: []f64,
index: usize = 0,
fn initZero(allocator: std.mem.Allocator, samples: usize) !FeedbackBuffer {
const buf = try allocator.alloc(f64, samples * 2);
@memset(buf, 0);
return .{ .samples = buf };
}
fn deinit(self: FeedbackBuffer, allocator: std.mem.Allocator) void {
allocator.free(self.samples);
}
fn push(self: *FeedbackBuffer, sample: [2]lj.AnalogReadResult) void {
const halfpoint = @divExact(self.samples.len, 2);
defer self.index = (self.index + 1) % halfpoint;
self.samples[self.index] = sample[0].voltage;
self.samples[self.index + halfpoint] = sample[1].voltage;
}
inline fn mean(data: []f64) f64 {
var accum: f64 = 0;
for (data) |pt| {
accum += pt;
}
return accum / @as(f64, @floatFromInt(data.len));
}
fn lerp(input: f64, cal_points: Config.MinMax) f64 {
return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle;
}
fn get(self: FeedbackBuffer) AzEl {
const halfpoint = @divExact(self.samples.len, 2);
return .{
.azimuth = lerp(
mean(self.samples[0..halfpoint]),
config.labjack.feedback_calibration.azimuth,
) + config.controller.angle_offset.azimuth,
.elevation = lerp(
mean(self.samples[halfpoint..]),
config.labjack.feedback_calibration.elevation,
) + config.controller.angle_offset.elevation,
};
}
fn getRaw(self: FeedbackBuffer) AzEl {
const halfpoint = @divExact(self.samples.len, 2);
return .{
.azimuth = mean(self.samples[0..halfpoint]),
.elevation = mean(self.samples[halfpoint..]),
};
}
};
const Controller = struct {
target: AzEl,
position: AzEl,
feedback_buffer: FeedbackBuffer,
current_state: ControllerState,
requested_state: ControllerState,
labjack: lj.Labjack,
lock: std.Thread.Mutex = .{},
condition: std.Thread.Condition = .{},
const ControllerState = enum {
initializing,
idle,
calibration,
running,
stopped,
};
fn init(allocator: std.mem.Allocator) !Controller {
return .{
.target = .{ .azimuth = 0, .elevation = 0 },
.position = .{ .azimuth = 0, .elevation = 0 },
.feedback_buffer = try FeedbackBuffer.initZero(allocator, config.controller.feedback_window_samples),
.current_state = .stopped,
.requested_state = .idle,
.labjack = switch (config.labjack.device) {
.autodetect => lj.Labjack.autodetect(),
.serial_number => |sn| lj.Labjack.with_serial_number(sn),
},
};
}
fn deinit(self: Controller, allocator: std.mem.Allocator) void {
self.feedback_buffer.deinit(allocator);
}
fn connectLabjack(self: *Controller) !void {
const info = try self.labjack.connect();
try self.labjack.setAllDigitalOutputLow();
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;
}
fn lerpAndOffsetAngles(input: [2]lj.AnalogReadResult) AzEl {
return .{
.azimuth = lerpOne(
input[0].voltage,
config.labjack.feedback_calibration.azimuth,
) + config.controller.angle_offset.azimuth,
.elevation = lerpOne(
input[1].voltage,
config.labjack.feedback_calibration.elevation,
) + config.controller.angle_offset.elevation,
};
}
const Sign = enum {
negative,
zero,
positive,
pub fn symbol(self: Sign) u21 {
return switch (self) {
.negative => '-',
.zero => '×',
.positive => '+',
};
}
};
fn signDeadzone(offset: f64, deadzone: f64) Sign {
return if (@abs(offset) < deadzone)
.zero
else if (offset < 0)
.negative
else
.positive;
}
fn updateFeedback(self: *Controller) !void {
const inputs = .{
config.controller.azimuth_input,
config.controller.elevation_input,
};
const raw = try self.labjack.readAnalogWriteDigital(
2,
inputs,
null,
true,
);
self.feedback_buffer.push(raw);
}
fn drive(self: *const Controller, pos_error: AzEl) !void {
const azsign = signDeadzone(
pos_error.azimuth,
config.controller.angle_tolerance.azimuth,
);
const elsign = signDeadzone(
pos_error.elevation,
config.controller.angle_tolerance.elevation,
);
var drive_signal: [4]bool = .{false} ** 4;
drive_signal[config.controller.azimuth_outputs.increase.io] = azsign == .positive;
drive_signal[config.controller.azimuth_outputs.decrease.io] = azsign == .negative;
drive_signal[config.controller.elevation_outputs.increase.io] = elsign == .positive;
drive_signal[config.controller.elevation_outputs.decrease.io] = elsign == .negative;
const raw = self.feedback_buffer.getRaw();
log.info(
// -180.1 is 6 chars. -5.20 is 5 chars
"az: {d: >6.1}° ({d: >5.2} V) Δ {d: >6.1}° => {u}, el: {d: >6.1}° ({d: >5.2} V) Δ {d: >6.1}° => {u}",
.{
self.position.azimuth,
raw.azimuth,
pos_error.azimuth,
azsign.symbol(),
self.position.elevation,
raw.elevation,
pos_error.elevation,
elsign.symbol(),
},
);
try self.labjack.writeIoLines(drive_signal);
}
fn setPosition(self: *Controller, position: AzEl) void {
self.position = position;
self.condition.broadcast();
}
fn run(self: *Controller) !void {
self.current_state = .initializing;
var timer = LoopTimer.init(config.controller.loop_interval_ns);
while (timer.mark()) : (timer.sleep()) {
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 => {},
.calibration => {
self.lock.lock();
defer self.lock.unlock();
// run calibration routine. psych, this does nothing. gottem
self.current_state = .idle;
self.requestState(.idle);
},
.running => {
const pos_error: AzEl = blk: {
self.lock.lock();
defer self.lock.unlock();
break :blk .{
.azimuth = self.target.azimuth - self.position.azimuth,
.elevation = self.target.elevation - self.position.elevation,
};
};
self.drive(pos_error) catch {
self.lock.lock();
defer self.lock.unlock();
self.current_state = .stopped;
continue;
};
},
.stopped => {
// attempt to reset the drive outputs
try self.labjack.writeIoLines(.{false} ** 4);
break;
},
}
}
}
};
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"),
};
}
pub fn mark(self: *LoopTimer) bool {
self.timer.reset();
return true;
}
pub fn sleep(self: *LoopTimer) void {
const elapsed = self.timer.read();
std.time.sleep(self.interval_ns -| elapsed);
}
};

View File

@@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
pub fn getDriverVersion() f32 {
return c_api.GetDriverVersion();
@@ -67,6 +68,30 @@ pub const Labjack = struct {
return status.toError();
}
pub fn writeIoLines(self: Labjack, out: [4]bool) LabjackError!void {
var id = self.cId();
var d_modes: c_long = 0xFF_FF;
var d_outputs: c_long = 0;
var d_states: c_long = 0;
const io_modes: c_long = 0b1111;
var io_outputs: c_long = PackedOutput.fromBoolArray(out).toCLong();
const status = c_api.DigitalIO(
&id,
self.demo(),
&d_modes,
io_modes,
&d_outputs,
&io_outputs,
1, // actually update the pin modes
&d_states,
);
if (!status.okay())
return status.toError();
}
/// Read one analog input channel, either single-ended or differential
pub fn analogReadOne(self: Labjack, input: AnalogInput) LabjackError!AnalogReadResult {
if (!input.channel.isDifferential() and input.gain_index != 0) {
@@ -289,6 +314,11 @@ pub const PackedOutput = packed struct(u4) {
}
};
const Call: std.builtin.CallingConvention = if (builtin.os.tag == .windows and builtin.cpu.arch == .x86)
.Stdcall
else
.C;
pub const c_api = struct {
pub const vendor_id: u16 = 0x0CD5;
pub const u12_product_id: u16 = 0x0001;
@@ -300,9 +330,9 @@ pub const c_api = struct {
idnum: *c_long,
serialnum: *c_long,
caldata: *[20]c_long,
) c_long;
) callconv(Call) c_long;
pub extern fn CloseAll(local_id: c_long) c_long;
pub extern fn CloseAll(local_id: c_long) callconv(Call) c_long;
pub extern fn GetU12Information(
handle: *anyopaque,
@@ -312,7 +342,7 @@ pub const c_api = struct {
cal_data: *[20]c_long,
fcdd_max_size: *c_long,
hvc_max_size: *c_long,
) c_long;
) callconv(Call) c_long;
pub extern fn EAnalogIn(
idnum: *c_long,
@@ -321,14 +351,14 @@ pub const c_api = struct {
gain: c_long,
overVoltage: *c_long,
voltage: *f32,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn EAnalogOut(
idnum: *c_long,
demo: c_long,
analogOut0: f32,
analogOut1: f32,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn ECount(
idnum: *c_long,
@@ -336,7 +366,7 @@ pub const c_api = struct {
resetCounter: c_long,
count: *f64,
ms: *f64,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn EDigitalIn(
idnum: *c_long,
@@ -344,7 +374,7 @@ pub const c_api = struct {
channel: c_long,
readD: c_long,
state: *c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn EDigitalOut(
idnum: *c_long,
@@ -352,7 +382,7 @@ pub const c_api = struct {
channel: c_long,
writeD: c_long,
state: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn AsynchConfig(
idnum: *c_long,
@@ -367,7 +397,7 @@ pub const c_api = struct {
halfA: c_long,
halfB: c_long,
halfC: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn Asynch(
idnum: *c_long,
@@ -380,7 +410,7 @@ pub const c_api = struct {
numWrite: c_long,
numRead: c_long,
data: [*]c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn AISample(
idnum: *c_long,
@@ -394,7 +424,7 @@ pub const c_api = struct {
disableCal: c_long,
overVoltage: *c_long,
voltages: *[4]f32,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn AIBurst(
idnum: *c_long,
@@ -415,7 +445,7 @@ pub const c_api = struct {
stateIOout: *[4096]c_long,
overVoltage: *c_long,
transferMode: c_long, // 0=auto,1=normal,2=turbo
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn AIStreamStart(
idnum: *c_long,
@@ -430,7 +460,7 @@ pub const c_api = struct {
disableCal: c_long,
reserved1: c_long, // always 0
readCount: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn AIStreamRead(
localID: c_long,
@@ -441,11 +471,11 @@ pub const c_api = struct {
reserved: ?*c_long, // unused
ljScanBacklog: *c_long,
overVoltage: *c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn AIStreamClear(
localID: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn AOUpdate(
idnum: *c_long,
@@ -459,21 +489,21 @@ pub const c_api = struct {
count: [*c]c_ulong,
analogOut0: f32,
analogOut1: f32,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn BitsToVolts(
chnum: c_long,
chgain: c_long,
bits: c_long,
volts: [*c]f32,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn VoltsToBits(
chnum: c_long,
chgain: c_long,
volts: f32,
bits: [*c]c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn Counter(
idnum: *c_long,
@@ -483,7 +513,7 @@ pub const c_api = struct {
resetCounter: c_long,
enableSTB: c_long,
count: [*c]c_ulong,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn DigitalIO(
idnum: *c_long,
@@ -494,12 +524,12 @@ pub const c_api = struct {
stateIO: *c_long, // 4 bits
updateDigital: c_long,
outputD: *c_long, // 16 bits
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn GetDriverVersion() f32;
pub extern fn StaticErrorString(errorcode: c_long) [*:0]const u8;
pub extern fn GetErrorString(errorcode: c_long, errorString: *[50]u8) void;
pub extern fn GetFirmwareVersion(idnum: *c_long) f32;
pub extern fn GetDriverVersion() callconv(Call) f32;
pub extern fn StaticErrorString(errorcode: c_long) callconv(Call) [*:0]const u8;
pub extern fn GetErrorString(errorcode: c_long, errorString: *[50]u8) callconv(Call) void;
pub extern fn GetFirmwareVersion(idnum: *c_long) callconv(Call) f32;
pub extern fn ListAll(
productIDList: *[127]c_long,
@@ -510,12 +540,12 @@ pub const c_api = struct {
numberFound: *c_long,
fcddMaxSize: *c_long,
hvcMaxSize: *c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn LocalID(
idnum: *c_long,
localID: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn PulseOut(
idnum: *c_long,
@@ -527,7 +557,7 @@ pub const c_api = struct {
timeC1: c_long,
timeB2: c_long,
timeC2: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn PulseOutStart(
idnum: *c_long,
@@ -539,24 +569,24 @@ pub const c_api = struct {
timeC1: c_long,
timeB2: c_long,
timeC2: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn PulseOutFinish(
idnum: *c_long,
demo: c_long,
timeoutMS: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn PulseOutCalc(
frequency: *f32,
timeB: *c_long,
timeC: *c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn ReEnum(idnum: *c_long) LabjackCError;
pub extern fn Reset(idnum: *c_long) LabjackCError;
pub extern fn ResetLJ(idnum: *c_long) LabjackCError;
pub extern fn CloseLabJack(localID: c_long) LabjackCError;
pub extern fn ReEnum(idnum: *c_long) callconv(Call) LabjackCError;
pub extern fn Reset(idnum: *c_long) callconv(Call) LabjackCError;
pub extern fn ResetLJ(idnum: *c_long) callconv(Call) LabjackCError;
pub extern fn CloseLabJack(localID: c_long) callconv(Call) LabjackCError;
pub extern fn SHT1X(
idnum: *c_long,
@@ -567,7 +597,7 @@ pub const c_api = struct {
tempC: *f32,
tempF: *f32,
rh: *f32,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn SHTComm(
idnum: *c_long,
@@ -579,7 +609,7 @@ pub const c_api = struct {
numRead: c_long,
datatx: [*]u8, // numWrite length
datarx: [*]u8, // numRead length
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn SHTCRC(
statusReg: c_long,
@@ -587,7 +617,7 @@ pub const c_api = struct {
numRead: c_long,
datatx: *[4]u8,
datarx: *[4]u8,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn Synch(
idnum: *c_long,
@@ -601,7 +631,7 @@ pub const c_api = struct {
configD: c_long,
numWriteRead: c_long,
data: *[18]c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn Watchdog(
idnum: *c_long,
@@ -615,7 +645,7 @@ pub const c_api = struct {
stateD0: c_long,
stateD1: c_long,
stateD8: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn ReadMem(
idnum: *c_long,
@@ -624,7 +654,7 @@ pub const c_api = struct {
data2: *c_long,
data1: *c_long,
data0: *c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub extern fn WriteMem(
idnum: *c_long,
@@ -634,7 +664,7 @@ pub const c_api = struct {
data2: c_long,
data1: c_long,
data0: c_long,
) LabjackCError;
) callconv(Call) LabjackCError;
pub const LabjackCError = packed struct(c_ulong) {
code: LabjackErrorCode,

View File

@@ -1,30 +1,81 @@
const std = @import("std");
const builtin = @import("builtin");
const Config = @import("./Config.zig");
const lj = @import("./labjack.zig");
const RotCtl = @import("./RotCtl.zig");
const YaesuController = @import("./YaesuController.zig");
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);
}
const commands = .{
.install_udev = "install-udev-rules",
.write_config = "write-default-config",
.run = "run",
};
const command_help = .{
.install_udev = "[udev rules.d path]: Install the built-in LabJack u12 udev rules file. May require sudo privileges. Linux only.",
.write_config = "[path]: write the default configuration to a json file.",
.run = "[config path]: run the rotctl interface with the provided config.",
};
pub fn main() !u8 {
if (comptime builtin.os.tag == .windows) {
// set output to UTF-8 on Windows
_ = std.os.windows.kernel32.SetConsoleOutputCP(65001);
}
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
@@ -35,60 +86,113 @@ pub fn main() !u8 {
};
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
printHelp();
if (args.len < 1) {
printStderr("No arguments at all?", .{});
return 1;
}
if (std.mem.eql(u8, args[1], commands.install_udev)) {
const exename = std.fs.path.basename(args[0]);
if (args.len < 2) {
printHelp(exename, .main);
return 1;
} else if (std.mem.eql(u8, args[1], commands.install_udev)) {
if (args.len > 3) {
printHelp();
printHelp(exename, .install_udev);
return 1;
}
return installUdevRules(if (args.len == 3) args[2] else null);
} else if (std.mem.eql(u8, args[1], commands.write_config)) {
if (args.len > 3) {
printHelp();
printHelp(exename, .write_config);
return 1;
}
Config.loadDefault(allocator);
defer Config.deinit();
return writeDefaultConfig(if (args.len == 3) args[2] else null);
} else if (std.mem.eql(u8, args[1], commands.run)) {
if (args.len > 3) {
printHelp();
printHelp(exename, .run);
return 1;
}
blk: {
const confpath = if (args.len == 3) args[2] else "yaes.json";
const conf_file = std.fs.cwd().openFile(confpath, .{}) catch {
log.warn("Could not load config file '{s}'. Using default config.", .{confpath});
Config.loadDefault(allocator);
break :blk;
};
defer conf_file.close();
loadConfigOrDefault(allocator, if (args.len == 3) args[2] else null) catch
return 1;
Config.load(allocator, conf_file.reader(), std.io.getStdErr().writer()) catch |err| {
log.err("Could not parse config file '{s}': {s}.", .{ confpath, @errorName(err) });
return 1;
};
}
defer Config.destroy(allocator);
defer Config.deinit();
const ver = lj.getDriverVersion();
std.debug.print("Driver version: {d}\n", .{ver});
RotCtl.run(allocator) catch |err| {
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
addExitHandler() catch {
log.err("Could not install quit handler.", .{});
return 1;
};
return 0;
RotCtl.run(allocator) catch |err| {
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
quit();
};
} else if (std.mem.eql(u8, args[1], commands.calibrate)) {
if (args.len < 3 or args.len > 4) {
printHelp(exename, .calibrate);
return 1;
}
loadConfigOrDefault(allocator, if (args.len == 4) args[3] else null) catch
return 1;
defer Config.deinit();
const routine = std.meta.stringToEnum(YaesuController.CalibrationRoutine, args[2]) orelse {
log.err("{s} is not a known calibration routine.", .{args[2]});
printHelp(exename, .calibrate);
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();
};
} else if (std.mem.eql(u8, args[1], commands.help)) {
if (args.len != 3) {
printHelp(exename, .help);
return 1;
}
inline for (@typeInfo(@TypeOf(commands)).Struct.fields) |field| {
if (std.mem.eql(u8, args[2], @field(commands, field.name))) {
printHelp(exename, @field(HelpTag, field.name));
return 0;
}
} else {
printHelp(exename, .help);
return 1;
}
} else {
printHelp();
printHelp(exename, .main);
return 1;
}
return 0;
}
fn loadConfigOrDefault(allocator: std.mem.Allocator, path: ?[]const u8) !void {
const confpath = path orelse "yaes.json";
const conf_file = std.fs.cwd().openFile(confpath, .{}) catch {
log.warn("Could not load config file '{s}'. Using default config.", .{confpath});
Config.loadDefault(allocator);
return;
};
defer conf_file.close();
Config.load(allocator, conf_file.reader(), std.io.getStdErr().writer()) catch |err| {
log.err("Could not parse config file '{s}': {s}.", .{ confpath, @errorName(err) });
return error.InvalidConfig;
};
log.info("Loaded config from '{s}'.", .{confpath});
}
fn installUdevRules(outpath: ?[]const u8) u8 {
@@ -141,20 +245,135 @@ fn writeDefaultConfig(outarg: ?[]const u8) u8 {
return 0;
}
fn printHelp() void {
printStderr(
\\Usage: yaes [command] [args...]
\\
\\ Control a Yaesu G-5500DC rotator through a LabJack U12 using the hamlib TCP interface.
\\
\\Available commands:
\\
, .{});
inline for (@typeInfo(@TypeOf(commands)).Struct.fields) |field| {
printStderr(
" {s} {s}",
.{ @field(commands, field.name), @field(command_help, field.name) },
);
fn printHelp(exename: []const u8, comptime cmd: HelpTag) void {
switch (cmd) {
.main => {
printStderr(command_help.main, .{ .exename = exename });
inline for (@typeInfo(@TypeOf(commands)).Struct.fields) |field| {
printStderr(
" {s: <" ++ max_command_len ++ "} {s}",
.{ @field(commands, field.name), @field(command_help, field.name).brief },
);
}
printStderr("", .{});
},
.help => {
printStderr(command_help.help.full, .{ .exename = exename, .cmdname = "help" });
inline for (@typeInfo(@TypeOf(commands)).Struct.fields) |field| {
printStderr(
" - {s}",
.{@field(commands, field.name)},
);
}
printStderr("", .{});
},
else => {
printStderr(
@field(command_help, @tagName(cmd)).full,
.{ .exename = exename, .cmdname = @field(commands, @tagName(cmd)) },
);
printStderr("", .{});
},
}
}
const HelpTag = std.meta.FieldEnum(@TypeOf(command_help));
const commands = .{
.install_udev = "install-udev-rules",
.write_config = "write-default-config",
.run = "run",
.calibrate = "calibrate",
.help = "help",
};
const max_command_len: []const u8 = blk: {
var len: usize = 0;
for (@typeInfo(@TypeOf(commands)).Struct.fields) |field|
if (@field(commands, field.name).len > len) {
len = @field(commands, field.name).len;
};
break :blk std.fmt.comptimePrint("{d}", .{len});
};
const command_help = .{
.main =
\\Usage: {[exename]s} <command> [arguments...]
\\
\\ Calibrate/Control a Yaesu G-5500DC rotator with a LabJack U12.
\\
\\Commands:
,
.install_udev = .{
.brief = "Install a udev rules file for the LabJack U12",
.full =
\\Usage: {[exename]s} {[cmdname]s} [<rules_dir>]
\\
\\ Install a udev rules file for the LabJack U12, which allows unprivileged access to the device on
\\ Linux-based operating systems.
\\
\\Arguments:
\\ rules_dir [Optional] The path to the udev rules directory inside which the rules file will be
\\ written. (Default: /etc/udev/rules.d)
,
},
.write_config = .{
.brief = "Write the default configuration to a file",
.full =
\\Usage: {[exename]s} {[cmdname]s} [<config_file>]
\\
\\ Write the built-in configuration defaults to a file. Useful as a starting point for creating a
\\ custom configuration.
\\
\\Arguments:
\\ config_file [Optional] the path of the file to write. (Default: ./yaes.json)
,
},
.run = .{
.brief = "Run the rotator with a hamlib-compatible TCP interface",
.full =
\\Usage: {[exename]s} {[cmdname]s} [<config_file>]
\\
\\ Expose a hamlib (rotctld)-compatible TCP interface through which the rotator can be controlled.
\\ This listens on localhost port 4533 by default. Only a subset of the rotctld commands are
\\ actually supported. A brief list of supported commands:
\\
\\ P, set_pos <az> <el> - point the rotator to the given azimuth and elevation
\\ p, get_pos - return the rotator's current azimuth and elevation
\\ S, stop - stop moving the rotator if it is moving
\\ K, park - move the rotator to its parking posture (defined by the config)
\\ q, Q, quit - [nonstandard] stop the rotator control loop and exit
\\
\\Arguments:
\\ config_file [Optional] the name of the config file to load. If this file does not exist, then
\\ the built-in defaults will be used. (Default: ./yaes.json)
,
},
.calibrate = .{
.brief = "Calibrate the rotator's feedback or its orientation to geodetic North",
.full =
\\Usage: {[exename]s} {[cmdname]s} <routine> [<config_file>]
\\
\\ Perform a calibration routine and write an updated configuration with its results.
\\
\\Arguments:
\\ routine Must be either `feedback` or `orientation`. The different calibration routines have
\\ different requirements. `orientation` calibration is a sun-pointing-based routine and
\\ should be performed after `feedback` calibration is complete.
\\ config_file [Optional] the path of a config file to load. This file will be updated with the
\\ results of the calibration process. If omitted and the configuration file does not
\\ exist, then the default configuration will be used. (Default: ./yaes.json)
,
},
.help = .{
.brief = "Print detailed help for a given command",
.full =
\\Usage: {[exename]s} {[cmdname]s} <command>
\\
\\ Print information on how to use a command and exit.
\\
\\Arguments:
\\ command The name of the command to print information about. Must be one of the following:
,
},
};