rotint/src/rotctl.zig
torque 351b5ccf59
main: functioning(?) man in the middle
This is very messy. I forgot how much callback-based async programming
sucks, especially without closures. Now I need to slap together the
UI.

This essentially has a "fixed" rate poll loop that alternates between
setting the position and getting the position. We return mangled
positions through the interface for display purposes, so this might be
usable.
2024-08-10 00:45:28 -07:00

261 lines
8.6 KiB
Zig

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;
}
};