211 lines
7.1 KiB
Zig
211 lines
7.1 KiB
Zig
|
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", .{});
|
|||
|
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;
|
|||
|
try interface.handleHamlibCommand(fbs.getWritten());
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
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 handleHamlibCommand(
|
|||
|
self: *RotCtl,
|
|||
|
command: []const u8,
|
|||
|
) !void {
|
|||
|
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
|
|||
|
|
|||
|
const first = tokens.next().?;
|
|||
|
if (first.len == 1 or first[0] == '\\') {
|
|||
|
switch (first[0]) {
|
|||
|
'q' => {
|
|||
|
self.running = false;
|
|||
|
self.replyStatus(.okay) catch return;
|
|||
|
},
|
|||
|
'\\' => {
|
|||
|
return try self.parseLongCommand(first[1..], &tokens);
|
|||
|
},
|
|||
|
else => |cmd| {
|
|||
|
log.err("unknown command {}", .{cmd});
|
|||
|
try self.replyStatus(.not_implemented);
|
|||
|
},
|
|||
|
}
|
|||
|
} else if (std.mem.eql(u8, first, "AOS")) {
|
|||
|
// gpredict just kind of shoves this message in on top of the HamLib
|
|||
|
// protocol.
|
|||
|
try self.replyStatus(.okay);
|
|||
|
} else if (std.mem.eql(u8, first, "LOS")) {
|
|||
|
try self.replyStatus(.okay);
|
|||
|
} else try self.replyStatus(.not_supported);
|
|||
|
}
|
|||
|
|
|||
|
fn parseLongCommand(
|
|||
|
self: *RotCtl,
|
|||
|
command: []const u8,
|
|||
|
tokens: *std.mem.TokenIterator(u8, .scalar),
|
|||
|
) !void {
|
|||
|
_ = tokens;
|
|||
|
|
|||
|
for (rotctl_commands) |check| {
|
|||
|
if (check.long) |long| {
|
|||
|
if (command.len >= long.len and std.mem.eql(u8, long, command)) {
|
|||
|
log.warn("Unsupported command {s}", .{command});
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
log.warn("Unknown command {s}", .{command});
|
|||
|
}
|
|||
|
return self.replyStatus(.not_supported);
|
|||
|
}
|
|||
|
|
|||
|
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 HamlibCommand = struct {
|
|||
|
short: ?u8 = null,
|
|||
|
long: ?[]const u8 = null,
|
|||
|
};
|
|||
|
|
|||
|
const rotctl_commands = [_]HamlibCommand{
|
|||
|
.{ .short = 'q' }, // quit
|
|||
|
.{ .short = 'Q' }, // quit
|
|||
|
.{ .short = 'P', .long = "set_pos" }, // azimuth: f64, elevation: f64
|
|||
|
.{ .short = 'p', .long = "get_pos" }, // return az: f64, el: f64
|
|||
|
.{ .short = 'M', .long = "move" }, // direction: enum { up=2, down=4, left=8, right=16 }, speed: i8 (0-100 or -1)
|
|||
|
.{ .short = 'S', .long = "stop" },
|
|||
|
.{ .short = 'K', .long = "park" },
|
|||
|
.{ .short = 'C', .long = "set_conf" }, // token: []const u8, value: []const u8
|
|||
|
.{ .short = 'R', .long = "reset" }, // u1 (1 is reset all)
|
|||
|
.{ .short = '_', .long = "get_info" }, // return Model name
|
|||
|
.{ .short = 'K', .long = "park" },
|
|||
|
.{ .long = "dump_state" }, // ???
|
|||
|
.{ .short = '1', .long = "dump_caps" }, // ???
|
|||
|
.{ .short = 'w', .long = "send_cmd" }, // []const u8, send serial command directly to the rotator
|
|||
|
.{ .short = 'L', .long = "lonlat2loc" }, // return Maidenhead locator for given long: f64 and lat: f64, locator precision: u4 (2-12)
|
|||
|
.{ .short = 'l', .long = "loc2lonlat" }, // the inverse of the above
|
|||
|
.{ .short = 'D', .long = "dms2dec" }, // deg, min, sec, 0 (positive) or 1 (negative)
|
|||
|
.{ .short = 'd', .long = "dec2dms" },
|
|||
|
.{ .short = 'E', .long = "dmmm2dec" },
|
|||
|
.{ .short = 'e', .long = "dec2dmmm" },
|
|||
|
.{ .short = 'B', .long = "grb" },
|
|||
|
.{ .short = 'A', .long = "a_sp2a_lp" },
|
|||
|
.{ .short = 'a', .long = "d_sp2d_lp" },
|
|||
|
.{ .long = "pause" },
|
|||
|
};
|
|||
|
|
|||
|
// 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.
|