yaes/src/Config.zig

190 lines
6.4 KiB
Zig
Raw Normal View History

const std = @import("std");
2024-07-11 16:32:53 -07:00
const AzEl = @import("./YaesuController.zig").AzEl;
const lj = @import("./labjack.zig");
const Config = @This();
var global_internal: std.json.Parsed(Config) = undefined;
pub const global: *const Config = &global_internal.value;
pub fn load(allocator: std.mem.Allocator, reader: anytype, err_writer: anytype) !void {
var jread = std.json.Reader(1024, @TypeOf(reader)).init(allocator, reader);
defer jread.deinit();
global_internal = try std.json.parseFromTokenSource(
Config,
allocator,
&jread,
.{},
);
try global.validate(err_writer);
}
pub fn loadDefault(allocator: std.mem.Allocator) void {
const arena = allocator.create(std.heap.ArenaAllocator) catch unreachable;
arena.* = std.heap.ArenaAllocator.init(allocator);
global_internal = .{
.arena = arena,
.value = .{},
};
}
pub fn deinit() void {
// TODO: implement this probably
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 = 4533,
},
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 },
},
.elevation = .{
.minimum = .{ .voltage = 0.0, .angle = 0.0 },
.maximum = .{ .voltage = 5.0, .angle = 180.0 },
},
},
},
controller: ControllerConfig = .{
.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 = 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 };
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,
angle_offset: AzEl,
elevation_mask: f64,
feedback_window_samples: u8,
const OutPair = struct {
increase: lj.DigitalOutputChannel,
decrease: lj.DigitalOutputChannel,
};
};