yaes/src/RotCtl.zig
2024-07-11 21:55:34 -07:00

303 lines
11 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const std = @import("std");
const Config = @import("./Config.zig");
const config = Config.global;
const YaesuController = @import("./YaesuController.zig");
const RotCtl = @This();
const log = std.log.scoped(.RotCtl);
writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
running: bool,
rotator: YaesuController,
pub fn run(allocator: std.mem.Allocator) !void {
// var server = std.net.StreamServer.init(.{ .reuse_address = true });
// defer server.deinit();
const listen_addr = try std.net.Address.parseIp(
config.rotctl.listen_address,
config.rotctl.listen_port,
);
var server = listen_addr.listen(.{ .reuse_address = true }) catch {
log.err("Could not listen on {}. Is it already in use?", .{listen_addr});
return;
};
log.info("Listening for client on: {}", .{listen_addr});
var interface: RotCtl = .{
.writer = undefined,
.running = true,
.rotator = try YaesuController.init(allocator),
};
while (true) {
const client = try server.accept();
defer {
log.info("disconnecting client", .{});
interface.rotator.stop();
client.stream.close();
}
interface.writer = .{ .unbuffered_writer = client.stream.writer() };
interface.running = true;
defer interface.running = false;
log.info("client connected from {}", .{client.address});
var readbuffer = [_]u8{0} ** 512;
var fbs = std.io.fixedBufferStream(&readbuffer);
const reader = client.stream.reader();
while (interface.running) : (fbs.reset()) {
reader.streamUntilDelimiter(fbs.writer(), '\n', readbuffer.len) catch break;
// note: an error here kills this entire function, which may not be
// desirable. For example, if the client unexpectedly disconnects, we
// probably shouldn't kill the whole runloop.
interface.handleHamlibCommand(
std.mem.trim(u8, fbs.getWritten(), &std.ascii.whitespace),
) catch break;
}
}
}
fn write(self: *RotCtl, buf: []const u8) !void {
try self.writer.writer().writeAll(buf);
try self.writer.flush();
}
fn replyStatus(self: *RotCtl, comptime status: HamlibErrorCode) !void {
try self.write(comptime status.replyFrame() ++ "\n");
}
fn printReply(self: *RotCtl, comptime fmt: []const u8, args: anytype) !void {
try self.writer.writer().print(fmt ++ "\n", args);
try self.writer.flush();
}
fn quit(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
if (tokens.next() != null) return error.BadInput;
self.running = false;
self.replyStatus(.okay) catch {};
self.rotator.quit();
}
fn stop(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
if (tokens.next() != null) return error.BadInput;
self.rotator.stop();
self.replyStatus(.okay) catch return error.BadOutput;
}
fn park(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
if (tokens.next() != null) return error.BadInput;
self.rotator.startPark();
self.replyStatus(.okay) catch return error.BadOutput;
}
fn blindAck(self: *RotCtl, _: []const u8, _: *TokenIter) CommandError!void {
self.replyStatus(.okay) catch return error.BadOutput;
}
fn notSupported(self: *RotCtl, _: []const u8, _: *TokenIter) CommandError!void {
self.replyStatus(.not_supported) catch return error.BadOutput;
}
fn getPosition(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
if (tokens.next() != null) return error.BadInput;
const pos = self.rotator.currentPosition();
self.printReply("{d:.1}\n{d:.1}", .{ pos.azimuth, pos.elevation }) catch return error.BadOutput;
}
fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool {
return switch (dof) {
// zig fmt: off
.azimuth => request >= (
config.labjack.feedback_calibration.azimuth.minimum.angle
+ config.controller.angle_offset.azimuth
) and request <= (
config.labjack.feedback_calibration.azimuth.maximum.angle
+ config.controller.angle_offset.azimuth
),
.elevation => request >= (
config.labjack.feedback_calibration.elevation.minimum.angle
+ config.controller.angle_offset.elevation
) and request <= (
config.labjack.feedback_calibration.elevation.maximum.angle
+ config.controller.angle_offset.elevation
),
// zig fmt: on
};
}
fn setPosition(self: *RotCtl, _: []const u8, tokens: *TokenIter) CommandError!void {
const azimuth = std.fmt.parseFloat(f64, tokens.next() orelse {
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
}) catch {
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
};
const elevation = std.fmt.parseFloat(f64, tokens.next() orelse {
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
}) catch {
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
};
self.rotator.setTarget(.{
.azimuth = azimuth,
.elevation = elevation,
}) catch |err| switch (err) {
error.OutOfRange => return self.replyStatus(.invalid_parameter) catch error.BadOutput,
};
return self.replyStatus(.okay) catch error.BadOutput;
}
fn handleHamlibCommand(
self: *RotCtl,
command: []const u8,
) !void {
if (command.len == 0) {
return self.replyStatus(.invalid_parameter) catch error.BadOutput;
}
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
const first = tokens.next().?;
if (first.len == 1 or first[0] == '\\') {
switch (first[0]) {
// NOTE: this is not technically supported by rotctld.
'q', 'Q' => try self.quit(first, &tokens),
'S' => try self.stop(first, &tokens),
'K' => try self.park(first, &tokens),
'p' => try self.getPosition(first, &tokens),
'P' => try self.setPosition(first, &tokens),
'\\' => try self.handleLongCommand(first[1..], &tokens),
else => {
log.err("unknown short command '{s}'", .{command});
self.replyStatus(.not_supported) catch return error.BadOutput;
},
}
} else {
try self.handleLongCommand(first, &tokens);
}
}
fn handleLongCommand(
self: *RotCtl,
command: []const u8,
tokens: *TokenIter,
) !void {
inline for (rotctl_commands) |cmdef|
if (comptime cmdef.long) |long|
if (std.mem.eql(u8, long, command))
return try cmdef.callback(self, command, tokens);
return self.replyStatus(.not_supported) catch error.BadOutput;
}
const HamlibErrorCode = enum(u8) {
okay = 0,
invalid_parameter = 1,
invalid_configuration = 2,
out_of_memory = 3,
not_implemented = 4,
timeout = 5,
io_error = 6,
internal_error = 7,
protocol_error = 8,
command_rejected = 9,
parameter_truncated = 10,
not_supported = 11,
not_targetable = 12,
bus_error = 13,
bus_busy = 14,
invalid_arg = 15,
invalid_vfo = 16,
domain_error = 17,
deprecated = 18,
security = 19,
power = 20,
fn replyFrame(comptime self: HamlibErrorCode) []const u8 {
return std.fmt.comptimePrint(
"RPRT {d}",
.{-@as(i8, @intCast(@intFromEnum(self)))},
);
}
};
const CommandError = error{ BadInput, BadOutput };
const TokenIter: type = std.mem.TokenIterator(u8, .scalar);
const CommandCallback: type = *const fn (self: *RotCtl, command: []const u8, tokens: *TokenIter) CommandError!void;
const HamlibCommand = struct {
short: ?u8 = null,
long: ?[]const u8 = null,
callback: CommandCallback,
};
const rotctl_commands = [_]HamlibCommand{
.{ .short = 'q', .callback = quit }, // quit
.{ .short = 'Q', .callback = quit }, // quit
.{ .long = "AOS", .callback = blindAck },
.{ .long = "LOS", .callback = blindAck },
.{ .short = 'P', .long = "set_pos", .callback = setPosition }, // azimuth: f64, elevation: f64
.{ .short = 'p', .long = "get_pos", .callback = getPosition }, // return az: f64, el: f64
.{ .short = 'M', .long = "move", .callback = notSupported }, // direction: enum { up=2, down=4, left=8, right=16 }, speed: i8 (0-100 or -1)
.{ .short = 'S', .long = "stop", .callback = stop },
.{ .short = 'K', .long = "park", .callback = park },
.{ .short = 'C', .long = "set_conf", .callback = notSupported }, // token: []const u8, value: []const u8
.{ .short = 'R', .long = "reset", .callback = notSupported }, // u1 (1 is reset all)
.{ .short = '_', .long = "get_info", .callback = notSupported }, // return Model name
.{ .long = "dump_state", .callback = notSupported }, // ???
.{ .short = '1', .long = "dump_caps", .callback = notSupported }, // ???
.{ .short = 'w', .long = "send_cmd", .callback = notSupported }, // []const u8, send serial command directly to the rotator
.{ .short = 'L', .long = "lonlat2loc", .callback = notSupported }, // return Maidenhead locator for given long: f64 and , .callback = notSupportedlat: f64, locator precision: u4 (2-12)
.{ .short = 'l', .long = "loc2lonlat", .callback = notSupported }, // the inverse of the above
.{ .short = 'D', .long = "dms2dec", .callback = notSupported }, // deg, min, sec, 0 (positive) or 1 (negative)
.{ .short = 'd', .long = "dec2dms", .callback = notSupported },
.{ .short = 'E', .long = "dmmm2dec", .callback = notSupported },
.{ .short = 'e', .long = "dec2dmmm", .callback = notSupported },
.{ .short = 'B', .long = "grb", .callback = notSupported },
.{ .short = 'A', .long = "a_sp2a_lp", .callback = notSupported },
.{ .short = 'a', .long = "d_sp2d_lp", .callback = notSupported },
.{ .long = "pause", .callback = notSupported },
};
// D, dms2dec 'Degrees' 'Minutes' 'Seconds' 'S/W'
// Returns 'Dec Degrees', a signed floating point value.
// 'Degrees' and 'Minutes' are integer values.
// 'Seconds' is a floating point value.
// 'S/W' is a flag with 1 indicating South latitude or West longitude and 0 North or East (the flag is needed as computers dont recognize a signed zero even though only the 'Degrees' value is typically signed in DMS notation).
// d, dec2dms 'Dec Degrees'
// Returns 'Degrees' 'Minutes' 'Seconds' 'S/W'.
// Values are as in dms2dec above.
// E, dmmm2dec 'Degrees' 'Dec Minutes' 'S/W'
// Returns 'Dec Degrees', a signed floating point value.
// 'Degrees' is an integer value.
// 'Dec Minutes' is a floating point value.
// 'S/W' is a flag as in dms2dec above.
// e, dec2dmmm 'Dec Deg'
// Returns 'Degrees' 'Minutes' 'S/W'.
// Values are as in dmmm2dec above.
// B, qrb 'Lon 1' 'Lat 1' 'Lon 2' 'Lat 2'
// Returns 'Distance' and 'Azimuth'.
// 'Distance' is in km.
// 'Azimuth' is in degrees.
// Supplied Lon/Lat values are signed floating point numbers.
// A, a_sp2a_lp 'Short Path Deg'
// Returns 'Long Path Deg'.
// Both the supplied argument and returned value are floating point values within the range of 0.00 to 360.00.
// Note: Supplying a negative value will return an error message.
// a, d_sp2d_lp 'Short Path km'
// Returns 'Long Path km'.
// Both the supplied argument and returned value are floating point values.
// pause 'Seconds'
// Pause for the given whole (integer) number of 'Seconds' before sending the next command to the rotator.