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, .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, }; };