Compare commits
11 Commits
a9e8819b3e
...
master
Author | SHA1 | Date | |
---|---|---|---|
4895c94d90
|
|||
c8cfc95938
|
|||
a3b4ffc76d
|
|||
c295c941e9
|
|||
de487d18c5
|
|||
61c10df63d
|
|||
153dde40aa
|
|||
4777d04594
|
|||
eb7ad4ef9e
|
|||
2194dd4a8c
|
|||
de76cce706
|
22
build.zig
22
build.zig
@@ -4,12 +4,24 @@ pub fn build(b: *std.Build) void {
|
|||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const use_udev = b.option(
|
const libusb_use_udev = b.option(
|
||||||
bool,
|
bool,
|
||||||
"use_udev",
|
"use_udev",
|
||||||
"link and use udev (Linux only. Default: false)",
|
"link and use udev (Linux only. Default: false)",
|
||||||
) orelse 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(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "yaes",
|
.name = "yaes",
|
||||||
.root_source_file = b.path("src/main.zig"),
|
.root_source_file = b.path("src/main.zig"),
|
||||||
@@ -38,7 +50,13 @@ pub fn build(b: *std.Build) void {
|
|||||||
} else {
|
} else {
|
||||||
const ljacklm_dep = b.dependency(
|
const ljacklm_dep = b.dependency(
|
||||||
"ljacklm",
|
"ljacklm",
|
||||||
.{ .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,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
exe.linkLibrary(ljacklm_dep.artifact("ljacklm"));
|
exe.linkLibrary(ljacklm_dep.artifact("ljacklm"));
|
||||||
}
|
}
|
||||||
|
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 target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const use_udev = b.option(
|
const libusb_use_udev = b.option(
|
||||||
bool,
|
bool,
|
||||||
"use_udev",
|
"libusb_use_udev",
|
||||||
"link and use udev (Linux only. Default: false)",
|
"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;
|
) orelse false;
|
||||||
|
|
||||||
const liblabjackusb = b.addStaticLibrary(.{
|
const liblabjackusb = b.addStaticLibrary(.{
|
||||||
@@ -31,7 +43,13 @@ pub fn build(b: *std.Build) !void {
|
|||||||
|
|
||||||
const usb_dep = b.dependency(
|
const usb_dep = b.dependency(
|
||||||
"usb",
|
"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"));
|
liblabjackusb.linkLibrary(usb_dep.artifact("usb"));
|
||||||
|
|
||||||
|
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 target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const use_udev = b.option(
|
const libusb_use_udev = b.option(
|
||||||
bool,
|
bool,
|
||||||
"use_udev",
|
"libusb_use_udev",
|
||||||
"link and use udev (Linux only. Default: false)",
|
"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;
|
) orelse false;
|
||||||
|
|
||||||
const libljacklm = b.addStaticLibrary(.{
|
const libljacklm = b.addStaticLibrary(.{
|
||||||
@@ -30,7 +42,13 @@ pub fn build(b: *std.Build) !void {
|
|||||||
|
|
||||||
const usb_dep = b.dependency(
|
const usb_dep = b.dependency(
|
||||||
"labjackusb",
|
"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"));
|
libljacklm.linkLibrary(usb_dep.artifact("labjackusb"));
|
||||||
|
|
||||||
|
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)",
|
"link and use udev (Linux only. Default: false)",
|
||||||
) orelse 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(.{
|
const libusb = b.addStaticLibrary(.{
|
||||||
.name = "usb",
|
.name = "usb",
|
||||||
.target = target,
|
.target = target,
|
||||||
@@ -59,8 +71,8 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.{ .style = .{ .autoconf = b.path("config.h.in") } },
|
.{ .style = .{ .autoconf = b.path("config.h.in") } },
|
||||||
.{
|
.{
|
||||||
.DEFAULT_VISIBILITY = .@"__attribute__ ((visibility (\"default\")))",
|
.DEFAULT_VISIBILITY = .@"__attribute__ ((visibility (\"default\")))",
|
||||||
.ENABLE_DEBUG_LOGGING = oneOrNull(optimize == .Debug),
|
.ENABLE_DEBUG_LOGGING = oneOrNull(enable_debug_logging),
|
||||||
.ENABLE_LOGGING = oneOrNull(optimize == .Debug),
|
.ENABLE_LOGGING = oneOrNull(enable_logging),
|
||||||
.HAVE_ASM_TYPES_H = null,
|
.HAVE_ASM_TYPES_H = null,
|
||||||
.HAVE_CLOCK_GETTIME = oneOrNull(linux_target),
|
.HAVE_CLOCK_GETTIME = oneOrNull(linux_target),
|
||||||
.HAVE_DECL_EFD_CLOEXEC = oneOrNull(linux_target),
|
.HAVE_DECL_EFD_CLOEXEC = oneOrNull(linux_target),
|
||||||
|
@@ -24,7 +24,7 @@ Unfortunately, all platforms have additional steps that must be taken (some easi
|
|||||||
|
|
||||||
#### Windows
|
#### 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
|
#### macOS
|
||||||
|
|
||||||
@@ -33,5 +33,3 @@ This works on macOS, though it has to be run with `sudo`, as access to the USB h
|
|||||||
#### Linux
|
#### 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.
|
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
|
|
||||||
|
@@ -5,31 +5,37 @@ const lj = @import("./labjack.zig");
|
|||||||
|
|
||||||
const Config = @This();
|
const Config = @This();
|
||||||
|
|
||||||
var global_internal: Config = undefined;
|
var global_internal: std.json.Parsed(Config) = undefined;
|
||||||
pub const global: *const Config = &global_internal;
|
pub const global: *const Config = &global_internal.value;
|
||||||
|
|
||||||
pub fn load(allocator: std.mem.Allocator, reader: anytype, err_writer: 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);
|
var jread = std.json.Reader(1024, @TypeOf(reader)).init(allocator, reader);
|
||||||
defer jread.deinit();
|
defer jread.deinit();
|
||||||
|
|
||||||
global_internal = try std.json.parseFromTokenSourceLeaky(
|
global_internal = try std.json.parseFromTokenSource(
|
||||||
Config,
|
Config,
|
||||||
allocator,
|
allocator,
|
||||||
&jread,
|
&jread,
|
||||||
.{},
|
.{},
|
||||||
);
|
);
|
||||||
|
|
||||||
try global_internal.validate(err_writer);
|
try global.validate(err_writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn loadDefault(allocator: std.mem.Allocator) void {
|
pub fn loadDefault(allocator: std.mem.Allocator) void {
|
||||||
_ = allocator;
|
const arena = allocator.create(std.heap.ArenaAllocator) catch unreachable;
|
||||||
global_internal = .{};
|
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
|
// 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 {
|
pub fn validate(self: Config, err_writer: anytype) !void {
|
||||||
@@ -86,6 +92,7 @@ pub fn validate(self: Config, err_writer: anytype) !void {
|
|||||||
rotctl: RotControlConfig = .{
|
rotctl: RotControlConfig = .{
|
||||||
.listen_address = "127.0.0.1",
|
.listen_address = "127.0.0.1",
|
||||||
.listen_port = 4533,
|
.listen_port = 4533,
|
||||||
|
.autopark = false,
|
||||||
},
|
},
|
||||||
labjack: LabjackConfig = .{
|
labjack: LabjackConfig = .{
|
||||||
.device = .autodetect,
|
.device = .autodetect,
|
||||||
@@ -121,6 +128,7 @@ controller: ControllerConfig = .{
|
|||||||
// this is a symmetric mask, so the minimum usable elevation is elevation_mask deg
|
// this is a symmetric mask, so the minimum usable elevation is elevation_mask deg
|
||||||
// and the maximum usable elevation is 180 - elevation_mask deg
|
// and the maximum usable elevation is 180 - elevation_mask deg
|
||||||
.elevation_mask = 0.0,
|
.elevation_mask = 0.0,
|
||||||
|
.feedback_window_samples = 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
pub const VoltAngle = struct { voltage: f64, angle: f64 };
|
pub const VoltAngle = struct { voltage: f64, angle: f64 };
|
||||||
@@ -144,6 +152,7 @@ pub const MinMax = struct {
|
|||||||
const RotControlConfig = struct {
|
const RotControlConfig = struct {
|
||||||
listen_address: []const u8,
|
listen_address: []const u8,
|
||||||
listen_port: u16,
|
listen_port: u16,
|
||||||
|
autopark: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LabjackConfig = struct {
|
const LabjackConfig = struct {
|
||||||
@@ -173,6 +182,8 @@ const ControllerConfig = struct {
|
|||||||
angle_offset: AzEl,
|
angle_offset: AzEl,
|
||||||
elevation_mask: f64,
|
elevation_mask: f64,
|
||||||
|
|
||||||
|
feedback_window_samples: u8,
|
||||||
|
|
||||||
const OutPair = struct {
|
const OutPair = struct {
|
||||||
increase: lj.DigitalOutputChannel,
|
increase: lj.DigitalOutputChannel,
|
||||||
decrease: lj.DigitalOutputChannel,
|
decrease: lj.DigitalOutputChannel,
|
||||||
|
@@ -10,12 +10,9 @@ const log = std.log.scoped(.RotCtl);
|
|||||||
|
|
||||||
writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
|
writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
|
||||||
running: bool,
|
running: bool,
|
||||||
rotator: YaesuController,
|
rotator: *YaesuController,
|
||||||
|
|
||||||
pub fn run(allocator: std.mem.Allocator) !void {
|
pub fn run(allocator: std.mem.Allocator) !void {
|
||||||
// var server = std.net.StreamServer.init(.{ .reuse_address = true });
|
|
||||||
// defer server.deinit();
|
|
||||||
|
|
||||||
const listen_addr = try std.net.Address.parseIp(
|
const listen_addr = try std.net.Address.parseIp(
|
||||||
config.rotctl.listen_address,
|
config.rotctl.listen_address,
|
||||||
config.rotctl.listen_port,
|
config.rotctl.listen_port,
|
||||||
@@ -30,20 +27,19 @@ pub fn run(allocator: std.mem.Allocator) !void {
|
|||||||
var interface: RotCtl = .{
|
var interface: RotCtl = .{
|
||||||
.writer = undefined,
|
.writer = undefined,
|
||||||
.running = true,
|
.running = true,
|
||||||
.rotator = try YaesuController.init(allocator),
|
.rotator = try YaesuController.create(allocator),
|
||||||
};
|
};
|
||||||
|
|
||||||
while (true) {
|
while (interface.running) {
|
||||||
const client = try server.accept();
|
const client = try server.accept();
|
||||||
defer {
|
defer {
|
||||||
log.info("disconnecting client", .{});
|
log.info("disconnecting client", .{});
|
||||||
|
if (!config.rotctl.autopark)
|
||||||
interface.rotator.stop();
|
interface.rotator.stop();
|
||||||
client.stream.close();
|
client.stream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface.writer = .{ .unbuffered_writer = client.stream.writer() };
|
interface.writer = .{ .unbuffered_writer = client.stream.writer() };
|
||||||
interface.running = true;
|
|
||||||
defer interface.running = false;
|
|
||||||
|
|
||||||
log.info("client connected from {}", .{client.address});
|
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),
|
std.mem.trim(u8, fbs.getWritten(), &std.ascii.whitespace),
|
||||||
) catch break;
|
) 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 {
|
fn write(self: *RotCtl, buf: []const u8) !void {
|
||||||
@@ -172,7 +174,6 @@ fn handleHamlibCommand(
|
|||||||
if (first.len == 1 or first[0] == '\\') {
|
if (first.len == 1 or first[0] == '\\') {
|
||||||
switch (first[0]) {
|
switch (first[0]) {
|
||||||
// NOTE: this is not technically supported by rotctld.
|
// NOTE: this is not technically supported by rotctld.
|
||||||
'q', 'Q' => try self.quit(first, &tokens),
|
|
||||||
'S' => try self.stop(first, &tokens),
|
'S' => try self.stop(first, &tokens),
|
||||||
'K' => try self.park(first, &tokens),
|
'K' => try self.park(first, &tokens),
|
||||||
'p' => try self.getPosition(first, &tokens),
|
'p' => try self.getPosition(first, &tokens),
|
||||||
@@ -243,8 +244,8 @@ const HamlibCommand = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rotctl_commands = [_]HamlibCommand{
|
const rotctl_commands = [_]HamlibCommand{
|
||||||
.{ .short = 'q', .callback = quit }, // quit
|
.{ .long = "quit", .callback = quit },
|
||||||
.{ .short = 'Q', .callback = quit }, // quit
|
.{ .long = "exit", .callback = quit },
|
||||||
.{ .long = "AOS", .callback = blindAck },
|
.{ .long = "AOS", .callback = blindAck },
|
||||||
.{ .long = "LOS", .callback = blindAck },
|
.{ .long = "LOS", .callback = blindAck },
|
||||||
.{ .short = 'P', .long = "set_pos", .callback = setPosition }, // azimuth: f64, elevation: f64
|
.{ .short = 'P', .long = "set_pos", .callback = setPosition }, // azimuth: f64, elevation: f64
|
||||||
|
@@ -8,6 +8,8 @@ const log = std.log.scoped(.yaesu_controller);
|
|||||||
|
|
||||||
const YaesuController = @This();
|
const YaesuController = @This();
|
||||||
|
|
||||||
|
pub var singleton: ?*YaesuController = null;
|
||||||
|
|
||||||
control_thread: std.Thread,
|
control_thread: std.Thread,
|
||||||
lock: *std.Thread.Mutex,
|
lock: *std.Thread.Mutex,
|
||||||
controller: *const Controller,
|
controller: *const Controller,
|
||||||
@@ -17,22 +19,47 @@ pub const AzEl = struct {
|
|||||||
elevation: f64,
|
elevation: f64,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) !YaesuController {
|
pub const CalibrationRoutine = enum {
|
||||||
const lock = try allocator.create(std.Thread.Mutex);
|
feedback,
|
||||||
errdefer allocator.destroy(lock);
|
orientation,
|
||||||
lock.* = .{};
|
};
|
||||||
|
|
||||||
|
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);
|
const controller = try allocator.create(Controller);
|
||||||
errdefer allocator.destroy(controller);
|
errdefer allocator.destroy(controller);
|
||||||
controller.init(lock);
|
controller.* = try Controller.init(allocator);
|
||||||
|
errdefer controller.deinit(allocator);
|
||||||
// do this in the main thread so we can throw the error about it synchronously.
|
// do this in the main thread so we can throw the error about it synchronously.
|
||||||
try controller.connectLabjack();
|
try controller.connectLabjack();
|
||||||
|
|
||||||
return .{
|
const self = try allocator.create(YaesuController);
|
||||||
|
errdefer allocator.destroy(self);
|
||||||
|
self.* = .{
|
||||||
.control_thread = try std.Thread.spawn(.{}, runController, .{controller}),
|
.control_thread = try std.Thread.spawn(.{}, runController, .{controller}),
|
||||||
.lock = lock,
|
.lock = &controller.lock,
|
||||||
.controller = controller,
|
.controller = controller,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
singleton = self;
|
||||||
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool {
|
fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool {
|
||||||
@@ -73,7 +100,7 @@ pub fn setTarget(self: YaesuController, target: AzEl) error{OutOfRange}!void {
|
|||||||
|
|
||||||
const controller = @constCast(self.controller);
|
const controller = @constCast(self.controller);
|
||||||
controller.target = masked_target;
|
controller.target = masked_target;
|
||||||
controller.requested_state = .running;
|
controller.requestState(.running);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn currentPosition(self: YaesuController) AzEl {
|
pub fn currentPosition(self: YaesuController) AzEl {
|
||||||
@@ -83,16 +110,15 @@ pub fn currentPosition(self: YaesuController) AzEl {
|
|||||||
return self.controller.position;
|
return self.controller.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn startCalibration(self: YaesuController) void {
|
pub fn waitForUpdate(self: YaesuController) AzEl {
|
||||||
// there are two different types of calibration:
|
const controller = @constCast(self.controller);
|
||||||
// 1. feedback calibration, running to the extents of the rotator
|
|
||||||
// 2. sun calibration, which determines the azimuth and elevation angle
|
self.lock.lock();
|
||||||
// offset between the rotator's physical stops and geodetic north
|
defer self.lock.unlock();
|
||||||
//
|
|
||||||
// The former is (fairly) trivial to automate, just run until stall
|
controller.condition.wait(self.lock);
|
||||||
// (assuming there's no deadband in the feedback). The latter requires
|
|
||||||
// manual input as the human is the feedback hardware in the loop.
|
return controller.position;
|
||||||
_ = self;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn quit(self: YaesuController) void {
|
pub fn quit(self: YaesuController) void {
|
||||||
@@ -100,7 +126,7 @@ pub fn quit(self: YaesuController) void {
|
|||||||
defer self.lock.unlock();
|
defer self.lock.unlock();
|
||||||
|
|
||||||
const controller = @constCast(self.controller);
|
const controller = @constCast(self.controller);
|
||||||
controller.requested_state = .stopped;
|
controller.requestState(.stopped);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(self: YaesuController) void {
|
pub fn stop(self: YaesuController) void {
|
||||||
@@ -109,13 +135,25 @@ pub fn stop(self: YaesuController) void {
|
|||||||
|
|
||||||
const controller = @constCast(self.controller);
|
const controller = @constCast(self.controller);
|
||||||
controller.target = controller.position;
|
controller.target = controller.position;
|
||||||
controller.requested_state = .idle;
|
controller.requestState(.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn startPark(self: YaesuController) void {
|
pub fn startPark(self: YaesuController) void {
|
||||||
self.setTarget(config.controller.parking_posture) catch unreachable;
|
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 {
|
fn runController(controller: *Controller) void {
|
||||||
controller.run() catch {
|
controller.run() catch {
|
||||||
log.err(
|
log.err(
|
||||||
@@ -125,16 +163,77 @@ fn runController(controller: *Controller) void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
const Controller = struct {
|
||||||
target: AzEl,
|
target: AzEl,
|
||||||
position: AzEl,
|
position: AzEl,
|
||||||
|
feedback_buffer: FeedbackBuffer,
|
||||||
|
|
||||||
current_state: ControllerState,
|
current_state: ControllerState,
|
||||||
requested_state: ControllerState,
|
requested_state: ControllerState,
|
||||||
|
|
||||||
lock: *std.Thread.Mutex,
|
|
||||||
labjack: lj.Labjack,
|
labjack: lj.Labjack,
|
||||||
|
|
||||||
|
lock: std.Thread.Mutex = .{},
|
||||||
|
condition: std.Thread.Condition = .{},
|
||||||
|
|
||||||
const ControllerState = enum {
|
const ControllerState = enum {
|
||||||
initializing,
|
initializing,
|
||||||
idle,
|
idle,
|
||||||
@@ -143,13 +242,13 @@ const Controller = struct {
|
|||||||
stopped,
|
stopped,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(self: *Controller, lock: *std.Thread.Mutex) void {
|
fn init(allocator: std.mem.Allocator) !Controller {
|
||||||
self.* = .{
|
return .{
|
||||||
.target = .{ .azimuth = 0, .elevation = 0 },
|
.target = .{ .azimuth = 0, .elevation = 0 },
|
||||||
.position = .{ .azimuth = 0, .elevation = 0 },
|
.position = .{ .azimuth = 0, .elevation = 0 },
|
||||||
|
.feedback_buffer = try FeedbackBuffer.initZero(allocator, config.controller.feedback_window_samples),
|
||||||
.current_state = .stopped,
|
.current_state = .stopped,
|
||||||
.requested_state = .idle,
|
.requested_state = .idle,
|
||||||
.lock = lock,
|
|
||||||
.labjack = switch (config.labjack.device) {
|
.labjack = switch (config.labjack.device) {
|
||||||
.autodetect => lj.Labjack.autodetect(),
|
.autodetect => lj.Labjack.autodetect(),
|
||||||
.serial_number => |sn| lj.Labjack.with_serial_number(sn),
|
.serial_number => |sn| lj.Labjack.with_serial_number(sn),
|
||||||
@@ -157,12 +256,28 @@ const Controller = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deinit(self: Controller, allocator: std.mem.Allocator) void {
|
||||||
|
self.feedback_buffer.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
fn connectLabjack(self: *Controller) !void {
|
fn connectLabjack(self: *Controller) !void {
|
||||||
const info = try self.labjack.connect();
|
const info = try self.labjack.connect();
|
||||||
try self.labjack.setAllDigitalOutputLow();
|
try self.labjack.setAllDigitalOutputLow();
|
||||||
self.labjack.id = info.local_id;
|
self.labjack.id = info.local_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function is run with the lock already acquired
|
||||||
|
fn propagateState(self: *Controller) void {
|
||||||
|
if (self.current_state == .stopped) return;
|
||||||
|
self.current_state = self.requested_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function is run with the lock already acquired
|
||||||
|
fn requestState(self: *Controller, request: ControllerState) void {
|
||||||
|
if (self.current_state == .stopped) return;
|
||||||
|
self.requested_state = request;
|
||||||
|
}
|
||||||
|
|
||||||
fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
|
fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
|
||||||
return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle;
|
return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle;
|
||||||
}
|
}
|
||||||
@@ -180,7 +295,21 @@ const Controller = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signDeadzone(offset: f64, deadzone: f64) enum { negative, zero, positive } {
|
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)
|
return if (@abs(offset) < deadzone)
|
||||||
.zero
|
.zero
|
||||||
else if (offset < 0)
|
else if (offset < 0)
|
||||||
@@ -189,26 +318,23 @@ const Controller = struct {
|
|||||||
.positive;
|
.positive;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn updateAzEl(self: *const Controller) !AzEl {
|
fn updateFeedback(self: *Controller) !void {
|
||||||
const inputs = .{ config.controller.azimuth_input, config.controller.elevation_input };
|
const inputs = .{
|
||||||
|
config.controller.azimuth_input,
|
||||||
|
config.controller.elevation_input,
|
||||||
|
};
|
||||||
|
|
||||||
const raw = try self.labjack.readAnalogWriteDigital(
|
const raw = try self.labjack.readAnalogWriteDigital(
|
||||||
2,
|
2,
|
||||||
inputs,
|
inputs,
|
||||||
.{false} ** 4,
|
null,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
return lerpAndOffsetAngles(raw);
|
self.feedback_buffer.push(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drive(self: *const Controller, pos_error: AzEl) !AzEl {
|
fn drive(self: *const Controller, pos_error: AzEl) !void {
|
||||||
// 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(
|
const azsign = signDeadzone(
|
||||||
pos_error.azimuth,
|
pos_error.azimuth,
|
||||||
config.controller.angle_tolerance.azimuth,
|
config.controller.angle_tolerance.azimuth,
|
||||||
@@ -219,46 +345,63 @@ const Controller = struct {
|
|||||||
config.controller.angle_tolerance.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.increase.io] = azsign == .positive;
|
||||||
drive_signal[config.controller.azimuth_outputs.decrease.io] = azsign == .negative;
|
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.increase.io] = elsign == .positive;
|
||||||
drive_signal[config.controller.elevation_outputs.decrease.io] = elsign == .negative;
|
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 = self.feedback_buffer.getRaw();
|
||||||
|
|
||||||
const raw = try self.labjack.readAnalogWriteDigital(2, inputs, drive_signal, true);
|
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(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return lerpAndOffsetAngles(raw);
|
try self.labjack.writeIoLines(drive_signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setPosition(self: *Controller, position: AzEl) void {
|
||||||
|
self.position = position;
|
||||||
|
self.condition.broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(self: *Controller) !void {
|
fn run(self: *Controller) !void {
|
||||||
self.current_state = .initializing;
|
self.current_state = .initializing;
|
||||||
|
|
||||||
var timer: LoopTimer = .{ .interval_ns = config.controller.loop_interval_ns };
|
var timer = LoopTimer.init(config.controller.loop_interval_ns);
|
||||||
|
|
||||||
while (timer.mark()) : (timer.sleep()) switch (self.current_state) {
|
while (timer.mark()) : (timer.sleep()) {
|
||||||
.initializing, .idle => {
|
const fbfail = if (self.updateFeedback()) |_| false else |_| true;
|
||||||
const pos = self.updateAzEl() catch {
|
|
||||||
|
{
|
||||||
self.lock.lock();
|
self.lock.lock();
|
||||||
defer self.lock.unlock();
|
defer self.lock.unlock();
|
||||||
|
|
||||||
self.current_state = .stopped;
|
self.setPosition(self.feedback_buffer.get());
|
||||||
continue;
|
if (fbfail) self.requestState(.stopped);
|
||||||
};
|
self.propagateState();
|
||||||
|
}
|
||||||
|
|
||||||
self.lock.lock();
|
switch (self.current_state) {
|
||||||
defer self.lock.unlock();
|
.initializing, .idle => {},
|
||||||
|
|
||||||
self.position = pos;
|
|
||||||
self.current_state = self.requested_state;
|
|
||||||
},
|
|
||||||
.calibration => {
|
.calibration => {
|
||||||
self.lock.lock();
|
self.lock.lock();
|
||||||
defer self.lock.unlock();
|
defer self.lock.unlock();
|
||||||
|
|
||||||
// run calibration routine. psych, this does nothing. gottem
|
// run calibration routine. psych, this does nothing. gottem
|
||||||
self.current_state = .idle;
|
self.current_state = .idle;
|
||||||
self.requested_state = self.current_state;
|
self.requestState(.idle);
|
||||||
},
|
},
|
||||||
.running => {
|
.running => {
|
||||||
const pos_error: AzEl = blk: {
|
const pos_error: AzEl = blk: {
|
||||||
@@ -271,43 +414,42 @@ const Controller = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const pos = self.drive(pos_error) catch {
|
self.drive(pos_error) catch {
|
||||||
self.lock.lock();
|
self.lock.lock();
|
||||||
defer self.lock.unlock();
|
defer self.lock.unlock();
|
||||||
|
|
||||||
self.current_state = .stopped;
|
self.current_state = .stopped;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.lock.lock();
|
|
||||||
defer self.lock.unlock();
|
|
||||||
|
|
||||||
self.position = pos;
|
|
||||||
self.current_state = self.requested_state;
|
|
||||||
},
|
},
|
||||||
.stopped => {
|
.stopped => {
|
||||||
// attempt to reset the drive outputs
|
// attempt to reset the drive outputs
|
||||||
_ = self.updateAzEl() catch {};
|
try self.labjack.writeIoLines(.{false} ** 4);
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const LoopTimer = struct {
|
pub const LoopTimer = struct {
|
||||||
interval_ns: u64,
|
interval_ns: u64,
|
||||||
|
timer: std.time.Timer,
|
||||||
|
|
||||||
start: i128 = 0,
|
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 {
|
pub fn mark(self: *LoopTimer) bool {
|
||||||
self.start = std.time.nanoTimestamp();
|
self.timer.reset();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sleep(self: *LoopTimer) void {
|
pub fn sleep(self: *LoopTimer) void {
|
||||||
const now = std.time.nanoTimestamp();
|
const elapsed = self.timer.read();
|
||||||
const elapsed: u64 = @intCast(now - self.start);
|
std.time.sleep(self.interval_ns -| elapsed);
|
||||||
|
|
||||||
std.time.sleep(self.interval_ns - elapsed);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -68,6 +68,30 @@ pub const Labjack = struct {
|
|||||||
return status.toError();
|
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
|
/// Read one analog input channel, either single-ended or differential
|
||||||
pub fn analogReadOne(self: Labjack, input: AnalogInput) LabjackError!AnalogReadResult {
|
pub fn analogReadOne(self: Labjack, input: AnalogInput) LabjackError!AnalogReadResult {
|
||||||
if (!input.channel.isDifferential() and input.gain_index != 0) {
|
if (!input.channel.isDifferential() and input.gain_index != 0) {
|
||||||
|
130
src/main.zig
130
src/main.zig
@@ -1,18 +1,81 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const Config = @import("./Config.zig");
|
const Config = @import("./Config.zig");
|
||||||
const lj = @import("./labjack.zig");
|
const lj = @import("./labjack.zig");
|
||||||
const RotCtl = @import("./RotCtl.zig");
|
const RotCtl = @import("./RotCtl.zig");
|
||||||
|
const YaesuController = @import("./YaesuController.zig");
|
||||||
|
|
||||||
const udev = @import("udev_rules");
|
const udev = @import("udev_rules");
|
||||||
|
|
||||||
const log = std.log.scoped(.main);
|
const log = std.log.scoped(.main);
|
||||||
|
|
||||||
|
fn quit() noreturn {
|
||||||
|
if (YaesuController.singleton) |controller| {
|
||||||
|
controller.quit();
|
||||||
|
controller.control_thread.join();
|
||||||
|
}
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreposix = struct {
|
||||||
|
pub extern "c" fn sigaddset(set: *std.posix.sigset_t, signo: c_int) c_int;
|
||||||
|
pub extern "c" fn sigdelset(set: *std.posix.sigset_t, signo: c_int) c_int;
|
||||||
|
pub extern "c" fn sigemptyset(set: *std.posix.sigset_t) c_int;
|
||||||
|
pub extern "c" fn sigfillset(set: *std.posix.sigset_t) c_int;
|
||||||
|
pub extern "c" fn sigismember(set: *const std.posix.sigset_t, signo: c_int) c_int;
|
||||||
|
// stdlib prototype is wrong, it doesn't take optional pointers.
|
||||||
|
pub extern "c" fn pthread_sigmask(how: c_int, noalias set: ?*const std.posix.sigset_t, noalias oldset: ?*std.posix.sigset_t) c_int;
|
||||||
|
};
|
||||||
|
|
||||||
|
const psigs = [_]c_int{ std.posix.SIG.INT, std.posix.SIG.HUP, std.posix.SIG.QUIT };
|
||||||
|
|
||||||
|
fn posixSignalHandlerThread() void {
|
||||||
|
var set: std.posix.sigset_t = undefined;
|
||||||
|
_ = moreposix.sigemptyset(&set);
|
||||||
|
for (psigs) |sig|
|
||||||
|
_ = moreposix.sigaddset(&set, sig);
|
||||||
|
|
||||||
|
var sig: c_int = 0;
|
||||||
|
_ = std.posix.system.sigwait(&set, &sig);
|
||||||
|
log.info("Got exit signal", .{});
|
||||||
|
quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows runs this handler in a thread, so calling quit directly should be safe.
|
||||||
|
fn windowsEventHandler(code: std.os.windows.DWORD) callconv(std.os.windows.WINAPI) std.os.windows.BOOL {
|
||||||
|
_ = code;
|
||||||
|
log.info("Got exit signal", .{});
|
||||||
|
quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addExitHandler() !void {
|
||||||
|
if (comptime builtin.os.tag == .windows) {
|
||||||
|
try std.os.windows.SetConsoleCtrlHandler(windowsEventHandler, true);
|
||||||
|
} else if (comptime std.Thread.use_pthreads) {
|
||||||
|
var set: std.posix.sigset_t = undefined;
|
||||||
|
_ = moreposix.sigemptyset(&set);
|
||||||
|
for (psigs) |sig|
|
||||||
|
_ = moreposix.sigaddset(&set, sig);
|
||||||
|
|
||||||
|
_ = moreposix.pthread_sigmask(std.posix.SIG.BLOCK, &set, null);
|
||||||
|
// nobody cares about the thread
|
||||||
|
_ = try std.Thread.spawn(.{}, posixSignalHandlerThread, .{});
|
||||||
|
} else {
|
||||||
|
log.err("not windows and not pthreads = disaster", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn printStderr(comptime fmt: []const u8, args: anytype) void {
|
fn printStderr(comptime fmt: []const u8, args: anytype) void {
|
||||||
std.debug.print(fmt ++ "\n", args);
|
std.debug.print(fmt ++ "\n", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() !u8 {
|
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(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
@@ -47,37 +110,52 @@ pub fn main() !u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Config.loadDefault(allocator);
|
Config.loadDefault(allocator);
|
||||||
|
defer Config.deinit();
|
||||||
return writeDefaultConfig(if (args.len == 3) args[2] else null);
|
return writeDefaultConfig(if (args.len == 3) args[2] else null);
|
||||||
} else if (std.mem.eql(u8, args[1], commands.run)) {
|
} else if (std.mem.eql(u8, args[1], commands.run)) {
|
||||||
if (args.len > 3) {
|
if (args.len > 3) {
|
||||||
printHelp(exename, .run);
|
printHelp(exename, .run);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
blk: {
|
loadConfigOrDefault(allocator, if (args.len == 3) args[2] else null) catch
|
||||||
const confpath = if (args.len == 3) args[2] else "yaes.json";
|
return 1;
|
||||||
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();
|
|
||||||
|
|
||||||
Config.load(allocator, conf_file.reader(), std.io.getStdErr().writer()) catch |err| {
|
defer Config.deinit();
|
||||||
log.err("Could not parse config file '{s}': {s}.", .{ confpath, @errorName(err) });
|
|
||||||
|
addExitHandler() catch {
|
||||||
|
log.err("Could not install quit handler.", .{});
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
defer Config.destroy(allocator);
|
|
||||||
|
|
||||||
const ver = lj.getDriverVersion();
|
|
||||||
std.debug.print("Driver version: {d}\n", .{ver});
|
|
||||||
|
|
||||||
RotCtl.run(allocator) catch |err| {
|
RotCtl.run(allocator) catch |err| {
|
||||||
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
|
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
|
||||||
|
quit();
|
||||||
|
};
|
||||||
|
} 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;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
return 0;
|
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)) {
|
} else if (std.mem.eql(u8, args[1], commands.help)) {
|
||||||
if (args.len != 3) {
|
if (args.len != 3) {
|
||||||
printHelp(exename, .help);
|
printHelp(exename, .help);
|
||||||
@@ -97,6 +175,24 @@ pub fn main() !u8 {
|
|||||||
printHelp(exename, .main);
|
printHelp(exename, .main);
|
||||||
return 1;
|
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 {
|
fn installUdevRules(outpath: ?[]const u8) u8 {
|
||||||
@@ -261,7 +357,7 @@ const command_help = .{
|
|||||||
\\ Perform a calibration routine and write an updated configuration with its results.
|
\\ Perform a calibration routine and write an updated configuration with its results.
|
||||||
\\
|
\\
|
||||||
\\Arguments:
|
\\Arguments:
|
||||||
\\ routine Must be one of `feedback` or `orientation`. The different calibration routines have
|
\\ routine Must be either `feedback` or `orientation`. The different calibration routines have
|
||||||
\\ different requirements. `orientation` calibration is a sun-pointing-based routine and
|
\\ different requirements. `orientation` calibration is a sun-pointing-based routine and
|
||||||
\\ should be performed after `feedback` calibration is complete.
|
\\ 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
|
\\ config_file [Optional] the path of a config file to load. This file will be updated with the
|
||||||
|
Reference in New Issue
Block a user