This is done silently, in the sense that the UI doesn't show that it is clamping the requested pointing target in this way.
442 lines
16 KiB
Zig
442 lines
16 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
const vaxis = @import("vaxis");
|
|
const xev = @import("xev");
|
|
|
|
const networking = @import("./networking.zig");
|
|
const rotctl = @import("./rotctl.zig");
|
|
const Config = @import("./Config.zig");
|
|
|
|
const log = std.log.scoped(.rotint);
|
|
|
|
pub const panic = vaxis.panic_handler;
|
|
|
|
pub const std_options: std.Options = .{
|
|
.log_level = if (builtin.mode == .Debug) .debug else .err,
|
|
};
|
|
|
|
// these are all in degrees
|
|
pub const AzEl = struct { az: f64, el: f64 };
|
|
|
|
pub const Log = struct {
|
|
file: std.fs.File,
|
|
|
|
pub fn init() !Log {
|
|
var buf: [64]u8 = undefined;
|
|
const now = std.time.milliTimestamp();
|
|
const filename = try std.fmt.bufPrint(buf[0..], "rotint-{d}.eventlog", .{now});
|
|
return .{
|
|
.file = try std.fs.cwd().createFile(filename, .{ .exclusive = true }),
|
|
};
|
|
}
|
|
|
|
pub fn writeEvent(self: Log, event: Event) void {
|
|
var buf: [128]u8 = undefined;
|
|
var fbs = std.io.fixedBufferStream(buf[0..]);
|
|
const writer = fbs.writer().any();
|
|
|
|
const now = std.time.milliTimestamp();
|
|
writer.print("{d},", .{now}) catch return;
|
|
event.write(writer) catch return;
|
|
writer.writeByte('\n') catch return;
|
|
self.file.writeAll(fbs.getWritten()) catch return;
|
|
}
|
|
|
|
pub const Event = union(enum) {
|
|
offset_updated: AzEl,
|
|
request_updated: AzEl,
|
|
command_sent: AzEl,
|
|
position_updated: AzEl,
|
|
|
|
pub fn designator(self: Event) []const u8 {
|
|
return switch (self) {
|
|
.offset_updated => "O",
|
|
.request_updated => "R",
|
|
.command_sent => "C",
|
|
.position_updated => "P",
|
|
};
|
|
}
|
|
|
|
pub fn write(self: Event, writer: std.io.AnyWriter) !void {
|
|
switch (self) {
|
|
.offset_updated,
|
|
.request_updated,
|
|
.command_sent,
|
|
.position_updated,
|
|
=> |pos| {
|
|
try writer.print(
|
|
"{s},{d:.1},{d:.1}",
|
|
.{ self.designator(), pos.az, pos.el },
|
|
);
|
|
},
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
pub const RotInt = struct {
|
|
allocator: std.mem.Allocator,
|
|
// TODO: associate timestamps with this somehow
|
|
offsets: AzEl = .{ .az = 0, .el = 0 },
|
|
requested_posture: AzEl = .{ .az = 0, .el = 0 },
|
|
current_posture: AzEl = .{ .az = 0, .el = 0 },
|
|
poll_interval: u64 = 500,
|
|
command_freq: u8 = 4,
|
|
pollcount: u9 = 0,
|
|
state: State = .initial,
|
|
conf: Config,
|
|
|
|
termbuffer: std.io.BufferedWriter(4096, std.io.AnyWriter),
|
|
vx: *vaxis.Vaxis,
|
|
loop: *xev.Loop,
|
|
|
|
parser: rotctl.RotCtl = .{},
|
|
last_command: rotctl.RotCommand = undefined,
|
|
server: networking.Server = .{},
|
|
rotator: networking.Client = .{},
|
|
poller: xev.Timer,
|
|
poll_completion: xev.Completion = undefined,
|
|
|
|
log: Log = undefined,
|
|
|
|
pub const State = enum {
|
|
initial,
|
|
rotator_connected,
|
|
rotator_ready,
|
|
server_connected,
|
|
};
|
|
|
|
pub fn initInPlace(self: *RotInt) !void {
|
|
self.server.rotint = self;
|
|
self.rotator.rotint = self;
|
|
self.log = try Log.init();
|
|
|
|
const rotctld_addr = std.net.Address.parseIp(
|
|
self.conf.rotctld.address,
|
|
self.conf.rotctld.port,
|
|
) catch |err| {
|
|
log.err("could not parse rotctld address {s}:{d}: {s}", .{
|
|
self.conf.rotctld.address,
|
|
self.conf.rotctld.port,
|
|
@errorName(err),
|
|
});
|
|
return err;
|
|
};
|
|
try self.rotator.connect(self.loop, rotctld_addr);
|
|
}
|
|
|
|
pub fn stateEvent(self: *RotInt, event: State) void {
|
|
switch (event) {
|
|
.initial => {},
|
|
.rotator_connected => if (self.state == .initial) {
|
|
self.warn("rotator connected", .{});
|
|
self.sendRotatorCommand(.get_position);
|
|
self.state = .rotator_connected;
|
|
self.vx.queueRefresh();
|
|
self.draw() catch {};
|
|
},
|
|
.rotator_ready => if (self.state == .rotator_connected) {
|
|
self.warn("rotator ready", .{});
|
|
const listen_addr = std.net.Address.parseIp(
|
|
self.conf.listen.address,
|
|
self.conf.listen.port,
|
|
) catch |err| {
|
|
self.showError("bogus listen address: {s}", .{@errorName(err)});
|
|
return;
|
|
};
|
|
self.server.listen(self.loop, listen_addr) catch |err| {
|
|
self.showError("listen problem: {s}", .{@errorName(err)});
|
|
return;
|
|
};
|
|
// demangle here to avoid causing initial moves
|
|
self.requested_posture = .{
|
|
.az = self.current_posture.az - self.offsets.az,
|
|
.el = self.current_posture.el - self.offsets.el,
|
|
};
|
|
self.state = .rotator_ready;
|
|
},
|
|
.server_connected => if (self.state == .rotator_ready) {
|
|
self.warn("server listening", .{});
|
|
self.state = .server_connected;
|
|
},
|
|
}
|
|
}
|
|
|
|
fn poll(
|
|
self_: ?*RotInt,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
result: xev.Timer.RunError!void,
|
|
) xev.CallbackAction {
|
|
const self = self_.?;
|
|
result catch {
|
|
self.warn("timer error???", .{});
|
|
return .disarm;
|
|
};
|
|
|
|
// pre-increment so that this does not try to command when pollcount = 0 on the
|
|
// first call
|
|
self.pollcount = (self.pollcount + 1) % self.command_freq;
|
|
|
|
if (self.pollcount == 0) {
|
|
var mangled: AzEl = .{
|
|
.az = self.requested_posture.az + self.offsets.az,
|
|
.el = self.requested_posture.el + self.offsets.el,
|
|
};
|
|
mangled.el = if (mangled.el > 90)
|
|
@min(mangled.el, 180 - self.conf.elevation_mask)
|
|
else
|
|
@max(mangled.el, self.conf.elevation_mask);
|
|
|
|
self.sendRotatorCommand(.{ .set_position = mangled });
|
|
} else {
|
|
self.sendRotatorCommand(.get_position);
|
|
}
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
fn sendRotatorCommand(self: *RotInt, command: rotctl.RotCommand) void {
|
|
self.last_command = command;
|
|
self.rotator.sendCommand(self.loop, command);
|
|
}
|
|
|
|
pub fn handleRotatorReply(self: *RotInt, res: []const u8) void {
|
|
const reply = self.parser.parseReplyFor(self.last_command, res) catch |err| switch (err) {
|
|
error.Incomplete => return,
|
|
error.InvalidParameter => {
|
|
return;
|
|
},
|
|
};
|
|
|
|
switch (reply) {
|
|
.okay => if (self.last_command == .set_position)
|
|
self.log.writeEvent(.{ .command_sent = self.last_command.set_position }),
|
|
.get_position => |pos| {
|
|
self.current_posture = pos;
|
|
self.log.writeEvent(.{ .position_updated = pos });
|
|
if (self.state == .rotator_connected) self.stateEvent(.rotator_ready);
|
|
self.draw() catch {};
|
|
},
|
|
.status => |code| if (code != .okay)
|
|
self.warn("rotctl error {s}", .{@tagName(code)}),
|
|
}
|
|
self.poller.run(self.loop, &self.poll_completion, self.poll_interval, RotInt, self, poll);
|
|
}
|
|
|
|
pub fn warn(_: *RotInt, comptime fmt: []const u8, args: anytype) void {
|
|
log.warn(fmt, args);
|
|
}
|
|
|
|
pub fn showError(_: *RotInt, comptime fmt: []const u8, args: anytype) void {
|
|
log.err(fmt, args);
|
|
}
|
|
|
|
pub fn handleControlRequest(self: *RotInt, req: []const u8) void {
|
|
const command = self.parser.parseCommand(req) catch |err| switch (err) {
|
|
error.Incomplete => return,
|
|
error.NotSupported => {
|
|
self.server.respond(self.loop, .{ .status = .not_supported });
|
|
return;
|
|
},
|
|
error.InvalidParameter => {
|
|
self.server.respond(self.loop, .{ .status = .invalid_parameter });
|
|
return;
|
|
},
|
|
};
|
|
|
|
switch (command) {
|
|
.get_position => self.server.respond(self.loop, .{ .get_position = self.current_posture }),
|
|
|
|
.set_position => |pos| {
|
|
self.requested_posture = pos;
|
|
self.server.respond(self.loop, .okay);
|
|
self.log.writeEvent(.{ .request_updated = pos });
|
|
self.draw() catch {};
|
|
},
|
|
|
|
.stop => self.server.respond(self.loop, .okay),
|
|
.park => self.server.respond(self.loop, .okay),
|
|
.quit => {
|
|
self.server.respond(self.loop, .okay);
|
|
self.server.should_disconnect = true;
|
|
},
|
|
}
|
|
}
|
|
|
|
fn draw(self: *RotInt) !void {
|
|
const win = self.vx.window();
|
|
win.clear();
|
|
|
|
var lines: [4][128]u8 = undefined;
|
|
const offsets: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
|
|
lines[0][0..],
|
|
"Offsets: Az: {d: >6.1}, El: {d: >6.1}",
|
|
.{ self.offsets.az, self.offsets.el },
|
|
) };
|
|
const requested: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
|
|
lines[1][0..],
|
|
"Requested: Az: {d: >6.1}, El: {d: >6.1}",
|
|
.{ self.requested_posture.az, self.requested_posture.el },
|
|
) };
|
|
const current: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
|
|
lines[2][0..],
|
|
"Current: Az: {d: >6.1}, El: {d: >6.1}",
|
|
.{ self.current_posture.az, self.current_posture.el },
|
|
) };
|
|
const pollinfo: vaxis.Segment = .{ .text = try std.fmt.bufPrint(
|
|
lines[3][0..],
|
|
"Poll: {d} ms, Command: {d} ms",
|
|
.{ self.poll_interval, self.command_freq * self.poll_interval },
|
|
) };
|
|
|
|
const center = vaxis.widgets.alignment.center(win, offsets.text.len, 1);
|
|
_ = try center.printSegment(offsets, .{});
|
|
const center_up = win.initChild(center.x_off, center.y_off - 1, .{ .limit = requested.text.len }, .{ .limit = 1 });
|
|
_ = try center_up.printSegment(requested, .{});
|
|
const center_down = win.initChild(center.x_off, center.y_off + 1, .{ .limit = current.text.len }, .{ .limit = 1 });
|
|
_ = try center_down.printSegment(current, .{});
|
|
const poll_win = win.initChild(center.x_off, center.y_off + 3, .{ .limit = pollinfo.text.len }, .{ .limit = 1 });
|
|
_ = try poll_win.printSegment(pollinfo, .{});
|
|
|
|
const help: vaxis.Segment = .{
|
|
.text = "Keys: q/^c - quit, ↑/↓ - el +/- 1° (+shift - 0.1°), ←/→ - az -/+ 1° (+shift - 0.1°), ⏎ - reset offsets, ^l - redraw, w/a/s/d - poll rates",
|
|
.style = .{ .fg = .{ .index = 245 } },
|
|
};
|
|
|
|
const helpwin = win.child(.{ .x_off = 0, .y_off = 0 });
|
|
_ = try helpwin.printSegment(help, .{ .wrap = .word });
|
|
|
|
try self.vx.render(self.termbuffer.writer().any());
|
|
try self.termbuffer.flush();
|
|
}
|
|
|
|
fn terminalEvent(
|
|
self_: ?*RotInt,
|
|
loop: *xev.Loop,
|
|
watcher: *vaxis.xev.TtyWatcher(RotInt),
|
|
event: vaxis.xev.Event,
|
|
) xev.CallbackAction {
|
|
const self = self_.?;
|
|
switch (event) {
|
|
.key_press => |key| {
|
|
var mods = key.mods;
|
|
mods.caps_lock = false;
|
|
mods.num_lock = false;
|
|
|
|
switch (key.codepoint) {
|
|
vaxis.Key.left,
|
|
vaxis.Key.kp_left,
|
|
vaxis.Key.right,
|
|
vaxis.Key.kp_right,
|
|
vaxis.Key.up,
|
|
vaxis.Key.kp_up,
|
|
vaxis.Key.down,
|
|
vaxis.Key.kp_down,
|
|
vaxis.Key.enter,
|
|
vaxis.Key.kp_enter,
|
|
=> |arrow| {
|
|
const scale: f64 = if (std.meta.eql(mods, .{ .shift = true })) 1 else 10;
|
|
const delta: AzEl = switch (arrow) {
|
|
vaxis.Key.left, vaxis.Key.kp_left => .{ .az = -0.1 * scale, .el = 0 },
|
|
vaxis.Key.right, vaxis.Key.kp_right => .{ .az = 0.1 * scale, .el = 0 },
|
|
vaxis.Key.up, vaxis.Key.kp_up => .{ .az = 0, .el = 0.1 * scale },
|
|
vaxis.Key.down, vaxis.Key.kp_down => .{ .az = 0 * scale, .el = -0.1 * scale },
|
|
vaxis.Key.enter, vaxis.Key.kp_enter => .{ .az = -self.offsets.az, .el = -self.offsets.el },
|
|
else => unreachable,
|
|
};
|
|
|
|
self.offsets.az += delta.az;
|
|
self.offsets.el += delta.el;
|
|
self.log.writeEvent(.{ .offset_updated = self.offsets });
|
|
self.draw() catch {
|
|
self.warn("draw failure", .{});
|
|
};
|
|
},
|
|
'w', 'a', 's', 'd' => |wasd| {
|
|
switch (wasd) {
|
|
'w' => self.poll_interval += 100,
|
|
'a' => if (self.command_freq > 2) {
|
|
self.command_freq -= 1;
|
|
},
|
|
's' => if (self.poll_interval > 100) {
|
|
self.poll_interval -= 100;
|
|
},
|
|
'd' => self.command_freq += 1,
|
|
else => unreachable,
|
|
}
|
|
self.draw() catch {};
|
|
},
|
|
'l' => if (std.meta.eql(mods, .{ .ctrl = true })) {
|
|
self.vx.queueRefresh();
|
|
self.draw() catch {};
|
|
},
|
|
'c' => if (std.meta.eql(mods, .{ .ctrl = true })) {
|
|
loop.stop();
|
|
return .disarm;
|
|
},
|
|
'q' => if (std.meta.eql(mods, .{})) {
|
|
loop.stop();
|
|
return .disarm;
|
|
},
|
|
else => {},
|
|
}
|
|
},
|
|
.winsize => |ws| {
|
|
watcher.vx.resize(self.allocator, watcher.tty.anyWriter(), ws) catch
|
|
return .disarm;
|
|
},
|
|
else => {},
|
|
}
|
|
return .rearm;
|
|
}
|
|
};
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
|
|
const alloc = gpa.allocator();
|
|
|
|
var tty = try vaxis.Tty.init();
|
|
defer tty.deinit();
|
|
|
|
var vx = try vaxis.init(alloc, .{});
|
|
defer vx.deinit(alloc, tty.anyWriter());
|
|
|
|
var pool = xev.ThreadPool.init(.{});
|
|
var loop = try xev.Loop.init(.{
|
|
.thread_pool = &pool,
|
|
});
|
|
defer loop.deinit();
|
|
|
|
var env = try std.process.getEnvMap(alloc);
|
|
defer env.deinit();
|
|
|
|
var app: RotInt = .{
|
|
.allocator = alloc,
|
|
.conf = Config.load(alloc, &env) catch cfg: {
|
|
log.warn("Could not load config file. Using defaults.", .{});
|
|
break :cfg Config.default();
|
|
},
|
|
.termbuffer = tty.bufferedWriter(),
|
|
.vx = &vx,
|
|
.loop = &loop,
|
|
.poller = try xev.Timer.init(),
|
|
};
|
|
try app.initInPlace();
|
|
|
|
var vx_loop: vaxis.xev.TtyWatcher(RotInt) = undefined;
|
|
try vx_loop.init(&tty, &vx, &loop, &app, RotInt.terminalEvent);
|
|
|
|
try vx.enterAltScreen(tty.anyWriter());
|
|
try vx.queryTerminalSend(tty.anyWriter());
|
|
// Window size appears to be left uninitialized unless we manually set it here. This
|
|
// seems sketchy to me (tty fd should be nonblocking for the event loop)
|
|
const size = try vaxis.Tty.getWinsize(tty.fd);
|
|
vx.resize(alloc, tty.anyWriter(), size) catch @panic("TODO");
|
|
|
|
try loop.run(.until_done);
|
|
}
|