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( std.mem.trim(u8, fbs.getWritten(), &std.ascii.whitespace), ); } } } 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 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', 'Q' => { self.running = false; self.replyStatus(.okay) catch {}; self.rotator.stop(); }, 'P' => { const pos = self.rotator.position(); try self.printReply("{d:.1} {d:.1}", .{ pos.azimuth, pos.elevation }); }, '\\' => { try self.parseLongCommand(first[1..], &tokens); }, else => { log.err("unknown short command '{s}'", .{command}); try self.replyStatus(.not_implemented); }, } } else { try self.parseLongCommand(first, &tokens); } } 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 long command {s}", .{command}); break; } } } else { log.warn("Unknown long 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 .{ .long = "AOS" }, .{ .long = "LOS" }, .{ .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.