Using the GPA is a bit annoying sometimes. The other option would be to just use page allocator to allocate the config and bypass the GPA.
187 lines
6.3 KiB
Zig
187 lines
6.3 KiB
Zig
const std = @import("std");
|
|
|
|
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,
|
|
},
|
|
|
|
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,
|
|
|
|
const OutPair = struct {
|
|
increase: lj.DigitalOutputChannel,
|
|
decrease: lj.DigitalOutputChannel,
|
|
};
|
|
};
|