303 lines
11 KiB
Zig
303 lines
11 KiB
Zig
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 don’t 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.
|