Compare commits
28 Commits
dbbb5c1748
...
master
Author | SHA1 | Date | |
---|---|---|---|
4895c94d90
|
|||
c8cfc95938
|
|||
a3b4ffc76d
|
|||
c295c941e9
|
|||
de487d18c5
|
|||
61c10df63d
|
|||
153dde40aa
|
|||
4777d04594
|
|||
eb7ad4ef9e
|
|||
2194dd4a8c
|
|||
de76cce706
|
|||
e5d8a716b0
|
|||
011f300f0a
|
|||
f1480bca45
|
|||
e4393c2e5a
|
|||
ccb507d4d9
|
|||
d5f0727517
|
|||
dbb076f69b
|
|||
8fb6032a04
|
|||
c8511d8c92
|
|||
2937de6fcd
|
|||
b0aac111a2
|
|||
7fbfe1c5f7
|
|||
7105775426
|
|||
0e88022a8d
|
|||
bd465af30d
|
|||
b08f819bdc
|
|||
8feb520d8f
|
55
build.zig
55
build.zig
@@ -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,44 @@ pub fn build(b: *std.Build) void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const ljacklm_dep = b.dependency(
|
||||
"ljacklm",
|
||||
.{ .target = target, .optimize = optimize, .use_udev = use_udev },
|
||||
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", .{
|
||||
.root_source_file = b.path("deps/labjack/exodriver/udev_rules.zig"),
|
||||
}),
|
||||
);
|
||||
exe.linkLibrary(ljacklm_dep.artifact("ljacklm"));
|
||||
|
||||
b.installArtifact(exe);
|
||||
}
|
||||
|
26
deps/labjack/exodriver/build.zig
vendored
26
deps/labjack/exodriver/build.zig
vendored
@@ -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"));
|
||||
|
||||
|
2
deps/labjack/exodriver/udev_rules.zig
vendored
Normal file
2
deps/labjack/exodriver/udev_rules.zig
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
pub const rules_filename = "90-labjack.rules";
|
||||
pub const rules = @embedFile(rules_filename);
|
26
deps/labjack/ljacklm/build.zig
vendored
26
deps/labjack/ljacklm/build.zig
vendored
@@ -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
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
BIN
deps/labjack/windows/ljackuw32.dll
vendored
Normal file
Binary file not shown.
BIN
deps/labjack/windows/ljackuw32.lib
vendored
Normal file
BIN
deps/labjack/windows/ljackuw32.lib
vendored
Normal file
Binary file not shown.
BIN
deps/labjack/windows/ljackuw64.dll
vendored
Normal file
BIN
deps/labjack/windows/ljackuw64.dll
vendored
Normal file
Binary file not shown.
BIN
deps/labjack/windows/ljackuw64.lib
vendored
Normal file
BIN
deps/labjack/windows/ljackuw64.lib
vendored
Normal file
Binary file not shown.
16
deps/libusb/build.zig
vendored
16
deps/libusb/build.zig
vendored
@@ -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),
|
||||
|
22
readme.md
22
readme.md
@@ -12,8 +12,24 @@ I mostly just want this to run on Linux, as a static executable. The zig build s
|
||||
|
||||
Dependencies are fully vendored.
|
||||
|
||||
### Status
|
||||
### Build
|
||||
|
||||
Build with `zig 0.13`.
|
||||
Requires `zig` version `0.13.x` to compile.
|
||||
|
||||
It builds for Linux and macOS. It does not currently build for Windows due to `ljacklm` having a dependency on pthreads (specifically, it uses mutexes).
|
||||
```shell
|
||||
zig build
|
||||
```
|
||||
|
||||
Unfortunately, all platforms have additional steps that must be taken (some easier than others) in order to run the program successfully.
|
||||
|
||||
#### Windows
|
||||
|
||||
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
|
||||
|
||||
This works on macOS, though it has to be run with `sudo`, as access to the USB hardware at user privilege level is gated by entitlements. Using entitlements is in turn gated by signature with an Apple Developer Certificate and Notarization, which may only be acquired with a paid Apple Developer Program subscription. If you have those, great. The provided `yaes.entitlements` is almost certainly not the correct set of entitlements, since I can't test it. Just run it with `sudo` whilst praising Tim Apple for the Most Advanced Operating System on the Planet.
|
||||
|
||||
#### 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.
|
||||
|
103
src/Config.zig
103
src/Config.zig
@@ -1,42 +1,111 @@
|
||||
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) !void {
|
||||
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.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 {
|
||||
var valid: bool = true;
|
||||
|
||||
// zig fmt: off
|
||||
if (
|
||||
self.controller.parking_posture.azimuth < (
|
||||
self.labjack.feedback_calibration.azimuth.minimum.angle
|
||||
+ self.controller.angle_offset.azimuth
|
||||
) or self.controller.parking_posture.azimuth > (
|
||||
self.labjack.feedback_calibration.azimuth.maximum.angle
|
||||
+ self.controller.angle_offset.azimuth
|
||||
)
|
||||
) {
|
||||
// zig fmt: on
|
||||
valid = false;
|
||||
try err_writer.print(
|
||||
"Config validation failed: Parking azimuth {d:.1} is outside of the valid azimuth range {d:.1} - {d:.1}\n",
|
||||
.{
|
||||
self.controller.parking_posture.azimuth,
|
||||
self.labjack.feedback_calibration.azimuth.minimum.angle + self.controller.angle_offset.azimuth,
|
||||
self.labjack.feedback_calibration.azimuth.maximum.angle + self.controller.angle_offset.azimuth,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// zig fmt: off
|
||||
if (
|
||||
self.controller.parking_posture.elevation < (
|
||||
self.labjack.feedback_calibration.elevation.minimum.angle
|
||||
+ self.controller.angle_offset.elevation
|
||||
) or self.controller.parking_posture.elevation > (
|
||||
self.labjack.feedback_calibration.elevation.maximum.angle
|
||||
+ self.controller.angle_offset.elevation
|
||||
)
|
||||
) {
|
||||
// zig fmt: on
|
||||
valid = false;
|
||||
try err_writer.print(
|
||||
"Config validation failed: Parking elevation {d:.1} is outside of the valid elevation range {d:.1} - {d:.1}\n",
|
||||
.{
|
||||
self.controller.parking_posture.elevation,
|
||||
self.labjack.feedback_calibration.elevation.minimum.angle + self.controller.angle_offset.elevation,
|
||||
self.labjack.feedback_calibration.elevation.maximum.angle + self.controller.angle_offset.elevation,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
return error.InvalidConfig;
|
||||
}
|
||||
|
||||
rotctl: RotControlConfig = .{
|
||||
.listen_address = "127.0.0.1",
|
||||
.listen_port = 5432,
|
||||
.listen_port = 4533,
|
||||
.autopark = false,
|
||||
},
|
||||
labjack: LabjackConfig = .{
|
||||
.device = .autodetect,
|
||||
.feedback_calibration = .{
|
||||
// NOTE: these min and max angles are treated as hardware limits. This serves
|
||||
// two purposes: first, it means that feedback is always interpolated,
|
||||
// never extrapolated (though with a two point calibration, that doesn't
|
||||
// matter much). Second, it prevents having a redundant set of bounds
|
||||
// values that could potentially desync from these and cause problems.
|
||||
//
|
||||
// The functional min and max are these plus the angle offset values. For
|
||||
// example, given controller.angle_offset.azimuth = -6, the practical minimum
|
||||
// azimuth would be -6 deg and the practical maximum would be 444 deg.
|
||||
.azimuth = .{
|
||||
.minimum = .{ .voltage = 0.0, .angle = 0.0 },
|
||||
.maximum = .{ .voltage = 5.0, .angle = 450.0 },
|
||||
@@ -48,14 +117,18 @@ labjack: LabjackConfig = .{
|
||||
},
|
||||
},
|
||||
controller: ControllerConfig = .{
|
||||
.azimuth_input = .{ .channel = .diff_01, .gain_index = 2 },
|
||||
.elevation_input = .{ .channel = .diff_23, .gain_index = 2 },
|
||||
.azimuth_input = .{ .channel = .diff_01, .range = .@"5 V" },
|
||||
.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 };
|
||||
@@ -79,6 +152,7 @@ pub const MinMax = struct {
|
||||
const RotControlConfig = struct {
|
||||
listen_address: []const u8,
|
||||
listen_port: u16,
|
||||
autopark: bool,
|
||||
};
|
||||
|
||||
const LabjackConfig = struct {
|
||||
@@ -106,6 +180,9 @@ const ControllerConfig = struct {
|
||||
parking_posture: AzEl,
|
||||
angle_tolerance: AzEl,
|
||||
angle_offset: AzEl,
|
||||
elevation_mask: f64,
|
||||
|
||||
feedback_window_samples: u8,
|
||||
|
||||
const OutPair = struct {
|
||||
increase: lj.DigitalOutputChannel,
|
||||
|
@@ -1,267 +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,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setTarget(self: LabjackYaesu, target: AzEl) void {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
const controller = @constCast(self.controller);
|
||||
controller.target = target;
|
||||
controller.requested_state = .running;
|
||||
}
|
||||
|
||||
pub fn position(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 idle(self: LabjackYaesu) void {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
const controller = @constCast(self.controller);
|
||||
controller.requested_state = .idle;
|
||||
}
|
||||
|
||||
pub fn stop(self: LabjackYaesu) void {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
const controller = @constCast(self.controller);
|
||||
controller.requested_state = .stopped;
|
||||
}
|
||||
|
||||
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();
|
||||
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 lerpAngles(input: [2]lj.AnalogReadResult) AzEl {
|
||||
return .{
|
||||
.azimuth = lerpOne(input[0].voltage, config.labjack.feedback_calibration.azimuth),
|
||||
.elevation = lerpOne(input[1].voltage, config.labjack.feedback_calibration.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 lerpAngles(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;
|
||||
|
||||
const raw = try self.labjack.readAnalogWriteDigital(2, inputs, drive_signal, true);
|
||||
|
||||
return lerpAngles(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);
|
||||
}
|
||||
};
|
215
src/RotCtl.zig
215
src/RotCtl.zig
@@ -1,7 +1,8 @@
|
||||
const std = @import("std");
|
||||
|
||||
const config = @import("./Config.zig").global;
|
||||
const LabjackYaesu = @import("./LabjackYaesu.zig");
|
||||
const Config = @import("./Config.zig");
|
||||
const config = Config.global;
|
||||
const YaesuController = @import("./YaesuController.zig");
|
||||
|
||||
const RotCtl = @This();
|
||||
|
||||
@@ -9,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,
|
||||
@@ -29,19 +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", .{});
|
||||
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});
|
||||
|
||||
@@ -51,11 +49,20 @@ pub fn run(allocator: std.mem.Allocator) !void {
|
||||
|
||||
while (interface.running) : (fbs.reset()) {
|
||||
reader.streamUntilDelimiter(fbs.writer(), '\n', readbuffer.len) catch break;
|
||||
try interface.handleHamlibCommand(
|
||||
// note: an error here kills this entire function, which may not be
|
||||
// desirable. For example, if the client unexpectedly disconnects, we
|
||||
// probably shouldn't kill the whole runloop.
|
||||
interface.handleHamlibCommand(
|
||||
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 {
|
||||
@@ -72,55 +79,127 @@ fn printReply(self: *RotCtl, comptime fmt: []const u8, args: anytype) !void {
|
||||
try self.writer.flush();
|
||||
}
|
||||
|
||||
fn quit(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
|
||||
if (tokens.next() != null) return error.BadInput;
|
||||
|
||||
self.running = false;
|
||||
self.replyStatus(.okay) catch {};
|
||||
self.rotator.quit();
|
||||
}
|
||||
|
||||
fn stop(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
|
||||
if (tokens.next() != null) return error.BadInput;
|
||||
|
||||
self.rotator.stop();
|
||||
self.replyStatus(.okay) catch return error.BadOutput;
|
||||
}
|
||||
|
||||
fn park(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
|
||||
if (tokens.next() != null) return error.BadInput;
|
||||
|
||||
self.rotator.startPark();
|
||||
self.replyStatus(.okay) catch return error.BadOutput;
|
||||
}
|
||||
|
||||
fn blindAck(self: *RotCtl, _: []const u8, _: *TokenIter) CommandError!void {
|
||||
self.replyStatus(.okay) catch return error.BadOutput;
|
||||
}
|
||||
|
||||
fn notSupported(self: *RotCtl, _: []const u8, _: *TokenIter) CommandError!void {
|
||||
self.replyStatus(.not_supported) catch return error.BadOutput;
|
||||
}
|
||||
|
||||
fn getPosition(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
|
||||
if (tokens.next() != null) return error.BadInput;
|
||||
|
||||
const pos = self.rotator.currentPosition();
|
||||
self.printReply("{d:.1}\n{d:.1}", .{ pos.azimuth, pos.elevation }) catch return error.BadOutput;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
fn setPosition(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
|
||||
const azimuth = std.fmt.parseFloat(f64, tokens.next() orelse {
|
||||
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
|
||||
}) catch {
|
||||
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
|
||||
};
|
||||
|
||||
const elevation = std.fmt.parseFloat(f64, tokens.next() orelse {
|
||||
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
|
||||
}) catch {
|
||||
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
|
||||
};
|
||||
|
||||
self.rotator.setTarget(.{
|
||||
.azimuth = azimuth,
|
||||
.elevation = elevation,
|
||||
}) catch |err| switch (err) {
|
||||
error.OutOfRange => return self.replyStatus(.invalid_parameter) catch error.BadOutput,
|
||||
};
|
||||
|
||||
return self.replyStatus(.okay) catch error.BadOutput;
|
||||
}
|
||||
|
||||
fn handleHamlibCommand(
|
||||
self: *RotCtl,
|
||||
command: []const u8,
|
||||
) !void {
|
||||
if (command.len == 0) {
|
||||
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
|
||||
}
|
||||
|
||||
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
|
||||
|
||||
const first = tokens.next().?;
|
||||
if (first.len == 1 or first[0] == '\\') {
|
||||
switch (first[0]) {
|
||||
'q', 'Q' => {
|
||||
self.running = false;
|
||||
self.replyStatus(.okay) catch {};
|
||||
self.rotator.stop();
|
||||
},
|
||||
'P' => {
|
||||
const pos = self.rotator.position();
|
||||
try self.printReply("{d:.1} {d:.1}", .{ pos.azimuth, pos.elevation });
|
||||
},
|
||||
'\\' => {
|
||||
try self.parseLongCommand(first[1..], &tokens);
|
||||
},
|
||||
// NOTE: this is not technically supported by rotctld.
|
||||
'S' => try self.stop(first, &tokens),
|
||||
'K' => try self.park(first, &tokens),
|
||||
'p' => try self.getPosition(first, &tokens),
|
||||
'P' => try self.setPosition(first, &tokens),
|
||||
'\\' => try self.handleLongCommand(first[1..], &tokens),
|
||||
else => {
|
||||
log.err("unknown short command '{s}'", .{command});
|
||||
try self.replyStatus(.not_implemented);
|
||||
self.replyStatus(.not_supported) catch return error.BadOutput;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
try self.parseLongCommand(first, &tokens);
|
||||
try self.handleLongCommand(first, &tokens);
|
||||
}
|
||||
}
|
||||
|
||||
fn parseLongCommand(
|
||||
fn handleLongCommand(
|
||||
self: *RotCtl,
|
||||
command: []const u8,
|
||||
tokens: *std.mem.TokenIterator(u8, .scalar),
|
||||
tokens: *TokenIter,
|
||||
) !void {
|
||||
_ = tokens;
|
||||
inline for (rotctl_commands) |cmdef|
|
||||
if (comptime cmdef.long) |long|
|
||||
if (std.mem.eql(u8, long, command))
|
||||
return try cmdef.callback(self, command, tokens);
|
||||
|
||||
for (rotctl_commands) |check| {
|
||||
if (check.long) |long| {
|
||||
if (command.len >= long.len and std.mem.eql(u8, long, command)) {
|
||||
log.warn("Unsupported long command {s}", .{command});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Unknown long command '{s}'", .{command});
|
||||
}
|
||||
return self.replyStatus(.not_supported);
|
||||
return self.replyStatus(.not_supported) catch error.BadOutput;
|
||||
}
|
||||
|
||||
const HamlibErrorCode = enum(u8) {
|
||||
@@ -154,38 +233,42 @@ const HamlibErrorCode = enum(u8) {
|
||||
}
|
||||
};
|
||||
|
||||
const CommandError = error{ BadInput, BadOutput };
|
||||
const TokenIter: type = std.mem.TokenIterator(u8, .scalar);
|
||||
const CommandCallback: type = *const fn (self: *RotCtl, command: []const u8, tokens: *TokenIter) CommandError!void;
|
||||
|
||||
const HamlibCommand = struct {
|
||||
short: ?u8 = null,
|
||||
long: ?[]const u8 = null,
|
||||
callback: CommandCallback,
|
||||
};
|
||||
|
||||
const rotctl_commands = [_]HamlibCommand{
|
||||
.{ .short = 'q' }, // quit
|
||||
.{ .short = 'Q' }, // quit
|
||||
.{ .long = "AOS" },
|
||||
.{ .long = "LOS" },
|
||||
.{ .short = 'P', .long = "set_pos" }, // azimuth: f64, elevation: f64
|
||||
.{ .short = 'p', .long = "get_pos" }, // return az: f64, el: f64
|
||||
.{ .short = 'M', .long = "move" }, // direction: enum { up=2, down=4, left=8, right=16 }, speed: i8 (0-100 or -1)
|
||||
.{ .short = 'S', .long = "stop" },
|
||||
.{ .short = 'K', .long = "park" },
|
||||
.{ .short = 'C', .long = "set_conf" }, // token: []const u8, value: []const u8
|
||||
.{ .short = 'R', .long = "reset" }, // u1 (1 is reset all)
|
||||
.{ .short = '_', .long = "get_info" }, // return Model name
|
||||
.{ .short = 'K', .long = "park" },
|
||||
.{ .long = "dump_state" }, // ???
|
||||
.{ .short = '1', .long = "dump_caps" }, // ???
|
||||
.{ .short = 'w', .long = "send_cmd" }, // []const u8, send serial command directly to the rotator
|
||||
.{ .short = 'L', .long = "lonlat2loc" }, // return Maidenhead locator for given long: f64 and lat: f64, locator precision: u4 (2-12)
|
||||
.{ .short = 'l', .long = "loc2lonlat" }, // the inverse of the above
|
||||
.{ .short = 'D', .long = "dms2dec" }, // deg, min, sec, 0 (positive) or 1 (negative)
|
||||
.{ .short = 'd', .long = "dec2dms" },
|
||||
.{ .short = 'E', .long = "dmmm2dec" },
|
||||
.{ .short = 'e', .long = "dec2dmmm" },
|
||||
.{ .short = 'B', .long = "grb" },
|
||||
.{ .short = 'A', .long = "a_sp2a_lp" },
|
||||
.{ .short = 'a', .long = "d_sp2d_lp" },
|
||||
.{ .long = "pause" },
|
||||
.{ .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
|
||||
.{ .short = 'p', .long = "get_pos", .callback = getPosition }, // return az: f64, el: f64
|
||||
.{ .short = 'M', .long = "move", .callback = notSupported }, // direction: enum { up=2, down=4, left=8, right=16 }, speed: i8 (0-100 or -1)
|
||||
.{ .short = 'S', .long = "stop", .callback = stop },
|
||||
.{ .short = 'K', .long = "park", .callback = park },
|
||||
.{ .short = 'C', .long = "set_conf", .callback = notSupported }, // token: []const u8, value: []const u8
|
||||
.{ .short = 'R', .long = "reset", .callback = notSupported }, // u1 (1 is reset all)
|
||||
.{ .short = '_', .long = "get_info", .callback = notSupported }, // return Model name
|
||||
.{ .long = "dump_state", .callback = notSupported }, // ???
|
||||
.{ .short = '1', .long = "dump_caps", .callback = notSupported }, // ???
|
||||
.{ .short = 'w', .long = "send_cmd", .callback = notSupported }, // []const u8, send serial command directly to the rotator
|
||||
.{ .short = 'L', .long = "lonlat2loc", .callback = notSupported }, // return Maidenhead locator for given long: f64 and , .callback = notSupportedlat: f64, locator precision: u4 (2-12)
|
||||
.{ .short = 'l', .long = "loc2lonlat", .callback = notSupported }, // the inverse of the above
|
||||
.{ .short = 'D', .long = "dms2dec", .callback = notSupported }, // deg, min, sec, 0 (positive) or 1 (negative)
|
||||
.{ .short = 'd', .long = "dec2dms", .callback = notSupported },
|
||||
.{ .short = 'E', .long = "dmmm2dec", .callback = notSupported },
|
||||
.{ .short = 'e', .long = "dec2dmmm", .callback = notSupported },
|
||||
.{ .short = 'B', .long = "grb", .callback = notSupported },
|
||||
.{ .short = 'A', .long = "a_sp2a_lp", .callback = notSupported },
|
||||
.{ .short = 'a', .long = "d_sp2d_lp", .callback = notSupported },
|
||||
.{ .long = "pause", .callback = notSupported },
|
||||
};
|
||||
|
||||
// D, dms2dec 'Degrees' 'Minutes' 'Seconds' 'S/W'
|
||||
|
455
src/YaesuController.zig
Normal file
455
src/YaesuController.zig
Normal 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);
|
||||
}
|
||||
};
|
176
src/labjack.zig
176
src/labjack.zig
@@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn getDriverVersion() f32 {
|
||||
return c_api.GetDriverVersion();
|
||||
@@ -6,7 +7,7 @@ pub fn getDriverVersion() f32 {
|
||||
|
||||
pub const Labjack = struct {
|
||||
id: ?i32 = null,
|
||||
demo: bool = false,
|
||||
demobit: bool = false,
|
||||
|
||||
pub fn autodetect() Labjack {
|
||||
return .{};
|
||||
@@ -36,6 +37,61 @@ pub const Labjack = struct {
|
||||
version;
|
||||
}
|
||||
|
||||
pub fn setAllDigitalOutputLow(self: Labjack) LabjackError!void {
|
||||
var id = self.cId();
|
||||
|
||||
// bitmask. D0 to D15 (LSB is D0). 0 is input, 1 is output
|
||||
var d_modes: c_long = 0xFF_FF;
|
||||
// bitmask. D0 to D15 (LSB is D0). 0 is output low, 1 is output high
|
||||
var d_outputs: c_long = 0;
|
||||
// bitmask. D0 to D15 (LSB is D0). 0 is output low, 1 is output high
|
||||
// the actual pin states read back from the device. an outvar from the API call.
|
||||
var d_states: c_long = 0;
|
||||
|
||||
// bitmask. IO0 to IO3 (LSB is IO0). 0 is input, 1 is output
|
||||
const io_modes: c_long = 0b1111;
|
||||
// bitmask. IO0 to IO3 (LSB is IO0). 0 is output low, 1 is output high
|
||||
var io_outputs: c_long = 0;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -48,9 +104,9 @@ pub const Labjack = struct {
|
||||
|
||||
const status = c_api.EAnalogIn(
|
||||
&id,
|
||||
@intFromBool(self.demo),
|
||||
self.demo(),
|
||||
input.channelNumber(),
|
||||
input.gain_index,
|
||||
input.gainIndex(),
|
||||
&over_v,
|
||||
&res.voltage,
|
||||
);
|
||||
@@ -67,7 +123,7 @@ pub const Labjack = struct {
|
||||
var id = self.cId();
|
||||
const status = c_api.EDigitalOut(
|
||||
&id,
|
||||
@intFromBool(self.demo),
|
||||
self.demo(),
|
||||
output.channelNumber(),
|
||||
@intFromBool(output.isDLine()),
|
||||
@intFromBool(output.level),
|
||||
@@ -93,14 +149,14 @@ pub const Labjack = struct {
|
||||
var gains: [incount]c_long = undefined;
|
||||
for (inputs, &in_channels, &gains) |from, *inc, *gain| {
|
||||
inc.* = from.channelNumber();
|
||||
gain.* = from.gain_index;
|
||||
gain.* = from.gainIndex();
|
||||
}
|
||||
var v_out: [4]f32 = .{0} ** 4;
|
||||
var over_v: c_long = 0;
|
||||
|
||||
const status = c_api.AISample(
|
||||
&id,
|
||||
@intFromBool(self.demo),
|
||||
self.demo(),
|
||||
&out_states,
|
||||
@intFromBool(outputs != null),
|
||||
@intFromBool(ledOn),
|
||||
@@ -129,15 +185,23 @@ pub const Labjack = struct {
|
||||
fn cId(self: Labjack) c_long {
|
||||
return self.id orelse -1;
|
||||
}
|
||||
|
||||
fn demo(self: Labjack) c_long {
|
||||
return @intFromBool(self.demobit);
|
||||
}
|
||||
};
|
||||
|
||||
pub const AnalogInput = struct {
|
||||
channel: AnalogInputChannel,
|
||||
gain_index: GainIndex = 0,
|
||||
range: InputRange = .@"20 V",
|
||||
|
||||
pub fn channelNumber(self: AnalogInput) u4 {
|
||||
return @intFromEnum(self.channel);
|
||||
}
|
||||
|
||||
pub fn gainIndex(self: AnalogInput) GainIndex {
|
||||
return @intFromEnum(self.range);
|
||||
}
|
||||
};
|
||||
|
||||
pub const AnalogReadResult = struct {
|
||||
@@ -207,6 +271,17 @@ pub const DigitalOutputChannel = union(enum) {
|
||||
// 7 => G=20 ±1 volt
|
||||
pub const GainIndex = u3;
|
||||
|
||||
pub const InputRange = enum(GainIndex) {
|
||||
@"20 V" = 0,
|
||||
@"10 V" = 1,
|
||||
@"5 V" = 2,
|
||||
@"4 V" = 3,
|
||||
@"2.5 V" = 4,
|
||||
@"2 V" = 5,
|
||||
@"1.25 V" = 6,
|
||||
@"1 V" = 7,
|
||||
};
|
||||
|
||||
pub const PackedOutput = packed struct(u4) {
|
||||
io0: bool,
|
||||
io1: bool,
|
||||
@@ -239,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;
|
||||
@@ -250,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,
|
||||
@@ -262,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,
|
||||
@@ -271,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,
|
||||
@@ -286,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,
|
||||
@@ -294,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,
|
||||
@@ -302,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,
|
||||
@@ -317,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,
|
||||
@@ -330,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,
|
||||
@@ -344,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,
|
||||
@@ -365,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,
|
||||
@@ -380,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,
|
||||
@@ -391,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,
|
||||
@@ -409,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,
|
||||
@@ -433,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,
|
||||
@@ -444,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,
|
||||
@@ -460,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,
|
||||
@@ -477,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,
|
||||
@@ -489,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,
|
||||
@@ -517,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,
|
||||
@@ -529,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,
|
||||
@@ -537,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,
|
||||
@@ -551,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,
|
||||
@@ -565,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,
|
||||
@@ -574,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,
|
||||
@@ -584,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,
|
||||
|
369
src/main.zig
369
src/main.zig
@@ -1,38 +1,379 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
blk: {
|
||||
const conf_file = std.fs.cwd().openFile("yaes.json", .{}) catch {
|
||||
log.warn("Could not load config file yaes.json. Using default config.", .{});
|
||||
Config.loadDefault(allocator);
|
||||
break :blk;
|
||||
};
|
||||
defer conf_file.close();
|
||||
const args = std.process.argsAlloc(allocator) catch {
|
||||
printStderr("Couldn't allocate arguments array", .{});
|
||||
return 1;
|
||||
};
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
Config.load(allocator, conf_file.reader()) catch {
|
||||
log.err("Could not parse config file yaes.json. Good luck figuring out why.", .{});
|
||||
if (args.len < 1) {
|
||||
printStderr("No arguments at all?", .{});
|
||||
return 1;
|
||||
}
|
||||
|
||||
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(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(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(exename, .run);
|
||||
return 1;
|
||||
}
|
||||
loadConfigOrDefault(allocator, if (args.len == 3) args[2] else null) catch
|
||||
return 1;
|
||||
|
||||
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();
|
||||
};
|
||||
} 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(exename, .main);
|
||||
return 1;
|
||||
}
|
||||
defer Config.destroy(allocator);
|
||||
|
||||
const ver = lj.getDriverVersion();
|
||||
std.debug.print("Driver version: {d}\n", .{ver});
|
||||
return 0;
|
||||
}
|
||||
|
||||
RotCtl.run(allocator) catch |err| {
|
||||
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
|
||||
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 {
|
||||
const rules_path = outpath orelse "/etc/udev/rules.d";
|
||||
var rules_d = std.fs.cwd().openDir(rules_path, .{}) catch |err| {
|
||||
printStderr(
|
||||
"could not open udev rules path '{s}': {s}",
|
||||
.{ rules_path, @errorName(err) },
|
||||
);
|
||||
return 1;
|
||||
};
|
||||
defer rules_d.close();
|
||||
|
||||
rules_d.writeFile(.{ .sub_path = udev.rules_filename, .data = udev.rules }) catch |err| {
|
||||
printStderr(
|
||||
"could not write rules file '{s}{s}{s}': {s}",
|
||||
.{
|
||||
rules_path,
|
||||
if (rules_path.len == 0)
|
||||
"./"
|
||||
else if (rules_path[rules_path.len - 1] == '/')
|
||||
""
|
||||
else
|
||||
"/",
|
||||
udev.rules_filename,
|
||||
@errorName(err),
|
||||
},
|
||||
);
|
||||
return 1;
|
||||
};
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn writeDefaultConfig(outarg: ?[]const u8) u8 {
|
||||
const outpath = outarg orelse "yaes.json";
|
||||
|
||||
const outfile = std.fs.cwd().createFile(outpath, .{}) catch |err| {
|
||||
printStderr("Could not write config file '{s}': {s}", .{ outpath, @errorName(err) });
|
||||
return 1;
|
||||
};
|
||||
defer outfile.close();
|
||||
|
||||
std.json.stringify(Config.global.*, .{ .whitespace = .indent_4 }, outfile.writer()) catch |err| {
|
||||
printStderr("Could not serialize config file '{s}': {s}", .{ outpath, @errorName(err) });
|
||||
return 1;
|
||||
};
|
||||
|
||||
printStderr("config written to {s}", .{outpath});
|
||||
return 0;
|
||||
}
|
||||
|
||||
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:
|
||||
,
|
||||
},
|
||||
};
|
||||
|
19
yaes.entitlements
Normal file
19
yaes.entitlements
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.driverkit.transport.usb</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>idVendor</key>
|
||||
<integer>3285</integer>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
Reference in New Issue
Block a user