const std = @import("std"); const Config = @import("./Config.zig"); const config = Config.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 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; }; if (!inRange(azimuth, .azimuth)) 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; }; if (!inRange(elevation, .elevation)) 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 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.