yaes/src/RotCtl.zig

275 lines
10 KiB
Zig
Raw Normal View History

const std = @import("std");
const config = @import("./Config.zig").global;
const LabjackYaesu = @import("./LabjackYaesu.zig");
const RotCtl = @This();
const log = std.log.scoped(.RotCtl);
writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
running: bool,
rotator: LabjackYaesu,
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 LabjackYaesu.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 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 });
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.