314 lines
11 KiB
Zig
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:
|
|
,
|
|
},
|
|
};
|