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.
261 lines
8.6 KiB
Zig
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;
|
|
}
|
|
};
|