Files
yaes/src/main.zig
torque eb7ad4ef9e main: hook up calibration stubs
I guess I will be finishing this later.
2024-07-15 17:55:31 -07:00

314 lines
11 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const Config = @import("./Config.zig");
const lj = @import("./labjack.zig");
const RotCtl = @import("./RotCtl.zig");
const YaesuController = @import("./YaesuController.zig");
const udev = @import("udev_rules");
const log = std.log.scoped(.main);
fn printStderr(comptime fmt: []const u8, args: anytype) void {
std.debug.print(fmt ++ "\n", args);
}
pub fn main() !u8 {
if (comptime builtin.os.tag == .windows) {
// set output to UTF-8 on Windows
_ = std.os.windows.kernel32.SetConsoleOutputCP(65001);
}
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = std.process.argsAlloc(allocator) catch {
printStderr("Couldn't allocate arguments array", .{});
return 1;
};
defer std.process.argsFree(allocator, args);
if (args.len < 1) {
printStderr("No arguments at all?", .{});
return 1;
}
const exename = std.fs.path.basename(args[0]);
if (args.len < 2) {
printHelp(exename, .main);
return 1;
} else if (std.mem.eql(u8, args[1], commands.install_udev)) {
if (args.len > 3) {
printHelp(exename, .install_udev);
return 1;
}
return installUdevRules(if (args.len == 3) args[2] else null);
} else if (std.mem.eql(u8, args[1], commands.write_config)) {
if (args.len > 3) {
printHelp(exename, .write_config);
return 1;
}
Config.loadDefault(allocator);
defer Config.deinit();
return writeDefaultConfig(if (args.len == 3) args[2] else null);
} else if (std.mem.eql(u8, args[1], commands.run)) {
if (args.len > 3) {
printHelp(exename, .run);
return 1;
}
loadConfigOrDefault(allocator, if (args.len == 3) args[2] else null) catch
return 1;
defer Config.deinit();
RotCtl.run(allocator) catch |err| {
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
return 1;
};
} else if (std.mem.eql(u8, args[1], commands.calibrate)) {
if (args.len < 3 or args.len > 4) {
printHelp(exename, .calibrate);
return 1;
}
loadConfigOrDefault(allocator, if (args.len == 4) args[3] else null) catch
return 1;
defer Config.deinit();
const routine = std.meta.stringToEnum(YaesuController.CalibrationRoutine, args[2]) orelse {
log.err("{s} is not a known calibration routine.", .{args[2]});
printHelp(exename, .calibrate);
return 1;
};
YaesuController.calibrate(allocator, routine) catch |err| {
log.err("Calibration failed: {s}", .{@errorName(err)});
return 1;
};
} else if (std.mem.eql(u8, args[1], commands.help)) {
if (args.len != 3) {
printHelp(exename, .help);
return 1;
}
inline for (@typeInfo(@TypeOf(commands)).Struct.fields) |field| {
if (std.mem.eql(u8, args[2], @field(commands, field.name))) {
printHelp(exename, @field(HelpTag, field.name));
return 0;
}
} else {
printHelp(exename, .help);
return 1;
}
} else {
printHelp(exename, .main);
return 1;
}
return 0;
}
fn loadConfigOrDefault(allocator: std.mem.Allocator, path: ?[]const u8) !void {
const confpath = path orelse "yaes.json";
const conf_file = std.fs.cwd().openFile(confpath, .{}) catch {
log.warn("Could not load config file '{s}'. Using default config.", .{confpath});
Config.loadDefault(allocator);
return;
};
defer conf_file.close();
Config.load(allocator, conf_file.reader(), std.io.getStdErr().writer()) catch |err| {
log.err("Could not parse config file '{s}': {s}.", .{ confpath, @errorName(err) });
return error.InvalidConfig;
};
log.info("Loaded config from '{s}'.", .{confpath});
}
fn installUdevRules(outpath: ?[]const u8) u8 {
const rules_path = outpath orelse "/etc/udev/rules.d";
var rules_d = std.fs.cwd().openDir(rules_path, .{}) catch |err| {
printStderr(
"could not open udev rules path '{s}': {s}",
.{ rules_path, @errorName(err) },
);
return 1;
};
defer rules_d.close();
rules_d.writeFile(.{ .sub_path = udev.rules_filename, .data = udev.rules }) catch |err| {
printStderr(
"could not write rules file '{s}{s}{s}': {s}",
.{
rules_path,
if (rules_path.len == 0)
"./"
else if (rules_path[rules_path.len - 1] == '/')
""
else
"/",
udev.rules_filename,
@errorName(err),
},
);
return 1;
};
return 0;
}
fn writeDefaultConfig(outarg: ?[]const u8) u8 {
const outpath = outarg orelse "yaes.json";
const outfile = std.fs.cwd().createFile(outpath, .{}) catch |err| {
printStderr("Could not write config file '{s}': {s}", .{ outpath, @errorName(err) });
return 1;
};
defer outfile.close();
std.json.stringify(Config.global.*, .{ .whitespace = .indent_4 }, outfile.writer()) catch |err| {
printStderr("Could not serialize config file '{s}': {s}", .{ outpath, @errorName(err) });
return 1;
};
printStderr("config written to {s}", .{outpath});
return 0;
}
fn printHelp(exename: []const u8, comptime cmd: HelpTag) void {
switch (cmd) {
.main => {
printStderr(command_help.main, .{ .exename = exename });
inline for (@typeInfo(@TypeOf(commands)).Struct.fields) |field| {
printStderr(
" {s: <" ++ max_command_len ++ "} {s}",
.{ @field(commands, field.name), @field(command_help, field.name).brief },
);
}
printStderr("", .{});
},
.help => {
printStderr(command_help.help.full, .{ .exename = exename, .cmdname = "help" });
inline for (@typeInfo(@TypeOf(commands)).Struct.fields) |field| {
printStderr(
" - {s}",
.{@field(commands, field.name)},
);
}
printStderr("", .{});
},
else => {
printStderr(
@field(command_help, @tagName(cmd)).full,
.{ .exename = exename, .cmdname = @field(commands, @tagName(cmd)) },
);
printStderr("", .{});
},
}
}
const HelpTag = std.meta.FieldEnum(@TypeOf(command_help));
const commands = .{
.install_udev = "install-udev-rules",
.write_config = "write-default-config",
.run = "run",
.calibrate = "calibrate",
.help = "help",
};
const max_command_len: []const u8 = blk: {
var len: usize = 0;
for (@typeInfo(@TypeOf(commands)).Struct.fields) |field|
if (@field(commands, field.name).len > len) {
len = @field(commands, field.name).len;
};
break :blk std.fmt.comptimePrint("{d}", .{len});
};
const command_help = .{
.main =
\\Usage: {[exename]s} <command> [arguments...]
\\
\\ Calibrate/Control a Yaesu G-5500DC rotator with a LabJack U12.
\\
\\Commands:
,
.install_udev = .{
.brief = "Install a udev rules file for the LabJack U12",
.full =
\\Usage: {[exename]s} {[cmdname]s} [<rules_dir>]
\\
\\ Install a udev rules file for the LabJack U12, which allows unprivileged access to the device on
\\ Linux-based operating systems.
\\
\\Arguments:
\\ rules_dir [Optional] The path to the udev rules directory inside which the rules file will be
\\ written. (Default: /etc/udev/rules.d)
,
},
.write_config = .{
.brief = "Write the default configuration to a file",
.full =
\\Usage: {[exename]s} {[cmdname]s} [<config_file>]
\\
\\ Write the built-in configuration defaults to a file. Useful as a starting point for creating a
\\ custom configuration.
\\
\\Arguments:
\\ config_file [Optional] the path of the file to write. (Default: ./yaes.json)
,
},
.run = .{
.brief = "Run the rotator with a hamlib-compatible TCP interface",
.full =
\\Usage: {[exename]s} {[cmdname]s} [<config_file>]
\\
\\ Expose a hamlib (rotctld)-compatible TCP interface through which the rotator can be controlled.
\\ This listens on localhost port 4533 by default. Only a subset of the rotctld commands are
\\ actually supported. A brief list of supported commands:
\\
\\ P, set_pos <az> <el> - point the rotator to the given azimuth and elevation
\\ p, get_pos - return the rotator's current azimuth and elevation
\\ S, stop - stop moving the rotator if it is moving
\\ K, park - move the rotator to its parking posture (defined by the config)
\\ q, Q, quit - [nonstandard] stop the rotator control loop and exit
\\
\\Arguments:
\\ config_file [Optional] the name of the config file to load. If this file does not exist, then
\\ the built-in defaults will be used. (Default: ./yaes.json)
,
},
.calibrate = .{
.brief = "Calibrate the rotator's feedback or its orientation to geodetic North",
.full =
\\Usage: {[exename]s} {[cmdname]s} <routine> [<config_file>]
\\
\\ Perform a calibration routine and write an updated configuration with its results.
\\
\\Arguments:
\\ routine Must be either `feedback` or `orientation`. The different calibration routines have
\\ different requirements. `orientation` calibration is a sun-pointing-based routine and
\\ should be performed after `feedback` calibration is complete.
\\ config_file [Optional] the path of a config file to load. This file will be updated with the
\\ results of the calibration process. If omitted and the configuration file does not
\\ exist, then the default configuration will be used. (Default: ./yaes.json)
,
},
.help = .{
.brief = "Print detailed help for a given command",
.full =
\\Usage: {[exename]s} {[cmdname]s} <command>
\\
\\ Print information on how to use a command and exit.
\\
\\Arguments:
\\ command The name of the command to print information about. Must be one of the following:
,
},
};