2024-08-09 17:32:06 -07:00
|
|
|
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;
|
2024-08-10 00:45:28 -07:00
|
|
|
while (std.mem.indexOfScalarPos(u8, self.readbuf[0..], offset, '\n')) |idx| {
|
|
|
|
const line = self.readbuf[offset..idx];
|
|
|
|
offset += idx + 1;
|
2024-08-09 17:32:06 -07:00
|
|
|
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) {
|
2024-08-10 00:45:28 -07:00
|
|
|
okay,
|
2024-08-09 17:32:06 -07:00
|
|
|
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) {
|
2024-08-10 00:45:28 -07:00
|
|
|
1 => blk: {
|
|
|
|
const code = try Status.parse(lines[0]);
|
|
|
|
break :blk if (code == .okay) .okay else .{ .status = code };
|
|
|
|
},
|
2024-08-09 17:32:06 -07:00
|
|
|
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 }),
|
2024-08-10 00:45:28 -07:00
|
|
|
.okay => try Status.okay.write(buf),
|
2024-08-09 17:32:06 -07:00
|
|
|
.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;
|
|
|
|
}
|
|
|
|
};
|