const std = @import("std"); const AzEl = @import("./main.zig").AzEl; const TokenIter: type = std.mem.TokenIterator(u8, .scalar); const log = std.log.scoped(.rotctl); pub const RotCtl = struct { // commands writebuf: [128]u8 = undefined, wlen: usize = 0, // replies readbuf: [128]u8 = undefined, rlen: usize = 0, // correlate a reply to a command // last_command: ?RotCommand = null, const ParseError = error{Incomplete}; pub fn parseCommand(self: *RotCtl, incoming: []const u8) (ParseError || RotCommand.ParseError)!RotCommand { @memcpy(self.writebuf[self.wlen .. self.wlen + incoming.len], incoming); self.wlen += incoming.len; const end = std.mem.indexOfScalarPos(u8, self.writebuf[0..], 0, '\n') orelse return error.Incomplete; defer { self.wlen = shiftBuf(&self.writebuf[0..self.wlen], end + 1); } return try RotCommand.parse(self.writebuf[0..end]); } pub fn parseReplyFor(self: *RotCtl, command: RotCommand, incoming: []const u8) (ParseError || RotReply.ParseError)!RotReply { @memcpy(self.readbuf[self.rlen .. self.rlen + incoming.len], incoming); self.rlen += incoming.len; var lines: [3][]const u8 = undefined; var lslice: [][]const u8 = lines[0..0]; var offset: usize = 0; while (std.mem.indexOfScalarPos(u8, self.readbuf[0..], offset, '\n')) |idx| { const line = self.readbuf[offset..idx]; offset += idx + 1; lslice.len += 1; lslice[lslice.len - 1] = line; if (lslice.len == command.expectedResponseLines() or (std.mem.startsWith(u8, line, "RPRT ") and line.len > 5)) { defer { self.rlen = shiftBuf(&self.readbuf[0..self.rlen], offset); } return RotReply.parse(command, lslice); } } else return error.Incomplete; } fn shiftBuf(buf: *const []u8, end: usize) usize { if (end < buf.len) std.mem.copyForwards(u8, buf.*[0..], buf.*[end..buf.len]); return buf.len -| end; } }; // consider: if we want to support passthrough, then RotCommand needs to also have space // to store the line to pass through, since it can't store a slice to any of our // existing buffers (those potentially get invalidated immediately after parsing). We // don't benefit from this much, since we will not be doing direct passthrough. // // pub const RotCommand = struct { // buf: [128]u8, // command: union(enum) { // invalid: Status, // set_position: AzEl, // passthrough: usize, // length of passthrough string in buf // }, // }; pub const RotCommand = union(enum) { get_position, set_position: AzEl, stop, park, quit, const ParseError = error{ NotSupported, InvalidParameter, }; pub fn write(self: RotCommand, buf: []u8) ![]u8 { return switch (self) { .get_position => try writeLiteral(buf, "p\n"), .set_position => |pos| try std.fmt.bufPrint( buf, "P {d:.1} {d:.1}\n", .{ pos.az, pos.el }, ), .stop => try writeLiteral(buf, "S\n"), .park => try writeLiteral(buf, "K\n"), .quit => try writeLiteral(buf, "q\n"), }; } pub fn expectedResponseLines(self: RotCommand) usize { return switch (self) { .get_position => 2, .set_position, .stop, .park, .quit => 1, }; } fn writeLiteral(buf: []u8, lit: []const u8) error{NoSpaceLeft}![]u8 { if (buf.len < lit.len) return error.NoSpaceLeft; @memcpy(buf[0..lit.len], lit); return buf[0..lit.len]; } pub fn parse(line: []const u8) ParseError!RotCommand { var tok = std.mem.tokenizeScalar(u8, line, ' '); // the first token is only null if the line is empty, and we've already // guaranteed it isn't. const first = tok.next() orelse unreachable; if (first.len == 1) { return switch (first[0]) { 'p' => parseNoArgs(&tok, .get_position), 'P' => parseSetPosition(&tok), 'S' => parseNoArgs(&tok, .stop), 'K' => parseNoArgs(&tok, .park), 'q', 'Q' => parseNoArgs(&tok, .quit), else => error.NotSupported, }; } else for (longs) |spec| { if (std.mem.eql(u8, spec.name, first)) return spec.parse(&tok); } else return error.NotSupported; } fn parseSetPosition(tok: *TokenIter) ParseError!RotCommand { const azstr = tok.next() orelse return error.InvalidParameter; const elstr = tok.next() orelse return error.InvalidParameter; if (tok.next() != null) return error.InvalidParameter; return .{ .set_position = .{ .az = std.fmt.parseFloat(f64, azstr) catch return error.InvalidParameter, .el = std.fmt.parseFloat(f64, elstr) catch return error.InvalidParameter, } }; } fn parseNoArgs(tok: *TokenIter, ret: RotCommand) ParseError!RotCommand { return if (tok.next() != null) error.InvalidParameter else ret; } const SpaceTokenParser = *const fn (tok: *TokenIter) ParseError!RotCommand; const LongSpec = struct { name: []const u8, parse: SpaceTokenParser }; const longs = [_]LongSpec{ .{ .name = "get_pos", .parse = partialParseNoArgs(.get_position) }, .{ .name = "set_pos", .parse = parseSetPosition }, .{ .name = "stop", .parse = partialParseNoArgs(.stop) }, .{ .name = "park", .parse = partialParseNoArgs(.park) }, }; fn partialParseNoArgs(comptime res: RotCommand) SpaceTokenParser { return struct { fn thunk(tok: *TokenIter) ParseError!RotCommand { return parseNoArgs(tok, res); } }.thunk; } }; pub const RotReply = union(enum) { okay, get_position: AzEl, status: Status, pub const ParseError = error{ InvalidParameter, } || Status.ParseError; pub fn parse(command: RotCommand, lines: []const []const u8) ParseError!RotReply { return switch (command) { .set_position, .park, .stop, .quit => switch (lines.len) { 1 => blk: { const code = try Status.parse(lines[0]); break :blk if (code == .okay) .okay else .{ .status = code }; }, else => error.InvalidParameter, }, .get_position => switch (lines.len) { 1 => .{ .status = try Status.parse(lines[0]) }, 2 => .{ .get_position = .{ .az = std.fmt.parseFloat(f64, lines[0]) catch return error.InvalidParameter, .el = std.fmt.parseFloat(f64, lines[1]) catch return error.InvalidParameter, } }, else => error.InvalidParameter, }, }; } pub fn write(self: RotReply, buf: []u8) ![]const u8 { return switch (self) { .get_position => |pos| try std.fmt.bufPrint(buf, "{d:.1}\n{d:.1}\n", .{ pos.az, pos.el }), .okay => try Status.okay.write(buf), .status => |code| try code.write(buf), }; } }; pub const Status = enum(i8) { 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, // make this nonexhaustive for kicks _, const ParseError = error{InvalidParameter}; pub fn parse(buf: []const u8) ParseError!Status { if (!std.mem.startsWith(u8, buf, "RPRT ") or !(buf.len > 5)) return error.InvalidParameter; const code = std.fmt.parseInt(i8, buf[5..], 10) catch return error.InvalidParameter; return @enumFromInt(-code); } pub fn write(self: Status, buf: []u8) ![]u8 { if (buf.len < 10) return error.NoSpaceLeft; return self.replyFrame(buf[0..10]); } pub fn format(self: Status, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { _ = fmt; _ = options; try writer.print("RPRT {d}\n", .{-@intFromEnum(self)}); } pub fn replyFrame(self: Status, buf: *[10]u8) []u8 { return std.fmt.bufPrint(buf, "{}", .{self}) catch unreachable; } };