2024-07-01 23:55:56 -07:00
|
|
|
const std = @import("std");
|
|
|
|
|
2024-07-11 16:32:53 -07:00
|
|
|
const AzEl = @import("./YaesuController.zig").AzEl;
|
2024-07-01 23:55:56 -07:00
|
|
|
const lj = @import("./labjack.zig");
|
|
|
|
|
|
|
|
const Config = @This();
|
|
|
|
|
2024-07-11 21:48:49 -07:00
|
|
|
var global_internal: std.json.Parsed(Config) = undefined;
|
|
|
|
pub const global: *const Config = &global_internal.value;
|
2024-07-01 23:55:56 -07:00
|
|
|
|
2024-07-07 15:36:14 -07:00
|
|
|
pub fn load(allocator: std.mem.Allocator, reader: anytype, err_writer: anytype) !void {
|
2024-07-01 23:55:56 -07:00
|
|
|
var jread = std.json.Reader(1024, @TypeOf(reader)).init(allocator, reader);
|
|
|
|
defer jread.deinit();
|
|
|
|
|
2024-07-11 21:48:49 -07:00
|
|
|
global_internal = try std.json.parseFromTokenSource(
|
2024-07-01 23:55:56 -07:00
|
|
|
Config,
|
|
|
|
allocator,
|
|
|
|
&jread,
|
|
|
|
.{},
|
|
|
|
);
|
2024-07-07 15:36:14 -07:00
|
|
|
|
2024-07-11 21:48:49 -07:00
|
|
|
try global.validate(err_writer);
|
2024-07-01 23:55:56 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn loadDefault(allocator: std.mem.Allocator) void {
|
2024-07-11 21:48:49 -07:00
|
|
|
const arena = allocator.create(std.heap.ArenaAllocator) catch unreachable;
|
|
|
|
arena.* = std.heap.ArenaAllocator.init(allocator);
|
|
|
|
global_internal = .{
|
|
|
|
.arena = arena,
|
|
|
|
.value = .{},
|
|
|
|
};
|
2024-07-01 23:55:56 -07:00
|
|
|
}
|
|
|
|
|
2024-07-11 21:48:49 -07:00
|
|
|
pub fn deinit() void {
|
2024-07-01 23:55:56 -07:00
|
|
|
// TODO: implement this probably
|
2024-07-11 21:48:49 -07:00
|
|
|
const allocator = global_internal.arena.child_allocator;
|
|
|
|
global_internal.arena.deinit();
|
|
|
|
allocator.destroy(global_internal.arena);
|
2024-07-01 23:55:56 -07:00
|
|
|
}
|
|
|
|
|
2024-07-07 15:36:14 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-07-01 23:55:56 -07:00
|
|
|
rotctl: RotControlConfig = .{
|
|
|
|
.listen_address = "127.0.0.1",
|
2024-07-06 12:56:10 -07:00
|
|
|
.listen_port = 4533,
|
2024-07-01 23:55:56 -07:00
|
|
|
},
|
|
|
|
labjack: LabjackConfig = .{
|
|
|
|
.device = .autodetect,
|
|
|
|
.feedback_calibration = .{
|
2024-07-06 12:59:37 -07:00
|
|
|
// 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.
|
2024-07-01 23:55:56 -07:00
|
|
|
.azimuth = .{
|
|
|
|
.minimum = .{ .voltage = 0.0, .angle = 0.0 },
|
|
|
|
.maximum = .{ .voltage = 5.0, .angle = 450.0 },
|
|
|
|
},
|
|
|
|
.elevation = .{
|
|
|
|
.minimum = .{ .voltage = 0.0, .angle = 0.0 },
|
|
|
|
.maximum = .{ .voltage = 5.0, .angle = 180.0 },
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
controller: ControllerConfig = .{
|
2024-07-06 12:58:27 -07:00
|
|
|
.azimuth_input = .{ .channel = .diff_01, .range = .@"5 V" },
|
|
|
|
.elevation_input = .{ .channel = .diff_23, .range = .@"5 V" },
|
2024-07-01 23:55:56 -07:00
|
|
|
.azimuth_outputs = .{ .increase = .{ .io = 0 }, .decrease = .{ .io = 1 } },
|
|
|
|
.elevation_outputs = .{ .increase = .{ .io = 2 }, .decrease = .{ .io = 3 } },
|
2024-07-10 12:43:11 -07:00
|
|
|
.loop_interval_ns = 100_000_000,
|
2024-07-01 23:55:56 -07:00
|
|
|
.parking_posture = .{ .azimuth = 180, .elevation = 90 },
|
|
|
|
.angle_tolerance = .{ .azimuth = 1, .elevation = 1 },
|
2024-07-03 17:42:36 -07:00
|
|
|
.angle_offset = .{ .azimuth = 0, .elevation = 0 },
|
2024-07-06 13:04:55 -07:00
|
|
|
// 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,
|
2024-07-18 19:34:58 -07:00
|
|
|
.feedback_window_samples = 3,
|
2024-07-01 23:55:56 -07:00
|
|
|
},
|
|
|
|
|
|
|
|
pub const VoltAngle = struct { voltage: f64, angle: f64 };
|
|
|
|
pub const MinMax = struct {
|
|
|
|
minimum: VoltAngle,
|
|
|
|
maximum: VoltAngle,
|
|
|
|
|
|
|
|
pub inline fn slope(self: MinMax) f64 {
|
|
|
|
return self.angleDiff() / self.voltDiff();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub inline fn voltDiff(self: MinMax) f64 {
|
|
|
|
return self.maximum.voltage - self.minimum.voltage;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub inline fn angleDiff(self: MinMax) f64 {
|
|
|
|
return self.maximum.angle - self.minimum.angle;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const RotControlConfig = struct {
|
|
|
|
listen_address: []const u8,
|
|
|
|
listen_port: u16,
|
|
|
|
};
|
|
|
|
|
|
|
|
const LabjackConfig = struct {
|
|
|
|
device: union(enum) {
|
|
|
|
autodetect,
|
|
|
|
serial_number: i32,
|
|
|
|
},
|
|
|
|
// Very basic two-point calibration for each degree of freedom. All other angles are
|
|
|
|
// linearly interpolated from these two points. This assumes the feedback is linear,
|
|
|
|
// which seems to be a mostly reasonable assumption in practice.
|
|
|
|
feedback_calibration: struct {
|
|
|
|
azimuth: MinMax,
|
|
|
|
elevation: MinMax,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const ControllerConfig = struct {
|
|
|
|
azimuth_input: lj.AnalogInput,
|
|
|
|
elevation_input: lj.AnalogInput,
|
|
|
|
|
|
|
|
azimuth_outputs: OutPair,
|
|
|
|
elevation_outputs: OutPair,
|
|
|
|
|
|
|
|
loop_interval_ns: u64,
|
|
|
|
parking_posture: AzEl,
|
|
|
|
angle_tolerance: AzEl,
|
2024-07-03 17:42:36 -07:00
|
|
|
angle_offset: AzEl,
|
2024-07-06 13:04:55 -07:00
|
|
|
elevation_mask: f64,
|
2024-07-01 23:55:56 -07:00
|
|
|
|
2024-07-18 19:34:58 -07:00
|
|
|
feedback_window_samples: u8,
|
|
|
|
|
2024-07-01 23:55:56 -07:00
|
|
|
const OutPair = struct {
|
|
|
|
increase: lj.DigitalOutputChannel,
|
|
|
|
decrease: lj.DigitalOutputChannel,
|
|
|
|
};
|
|
|
|
};
|