yaes/src/LabjackYaesu.zig

268 lines
7.9 KiB
Zig
Raw Normal View History

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);
}
};