rotint/src/main.zig

346 lines
12 KiB
Zig
Raw Normal View History

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 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 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 = 100,
command_freq: u8 = 10,
pollcount: u9 = 0,
state: State = .initial,
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,
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;
const connect_addr = try std.net.Address.parseIp("127.0.0.1", 4533);
try self.rotator.connect(self.loop, connect_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;
},
.rotator_ready => if (self.state == .rotator_connected) {
self.warn("rotator ready", .{});
const listen_addr = std.net.Address.parseIp("127.0.0.1", 42069) catch {
self.warn("bogus listen address", .{});
return;
};
self.server.listen(self.loop, listen_addr) catch {
self.warn("listen problem", .{});
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) {
const mangled: AzEl = .{
.az = self.requested_posture.az + self.offsets.az,
.el = self.requested_posture.el + self.offsets.el,
};
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 => {},
.get_position => |pos| {
self.current_posture = 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 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.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();
}
2024-08-10 10:52:57 -07:00
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| {
2024-08-10 10:52:57 -07:00
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.draw() catch {
self.warn("draw failure", .{});
};
2024-08-10 10:52:57 -07:00
},
'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,
2024-08-10 10:52:57 -07:00
}
self.draw() catch {};
2024-08-10 10:52:57 -07:00
},
'l' => if (std.meta.eql(mods, .{ .ctrl = true })) {
self.vx.queueRefresh();
self.draw() catch {};
2024-08-10 10:52:57 -07:00
},
'c' => if (std.meta.eql(mods, .{ .ctrl = true })) {
loop.stop();
return .disarm;
},
'q' => if (std.meta.eql(mods, .{})) {
loop.stop();
return .disarm;
},
else => {},
}
2024-08-10 10:52:57 -07:00
},
.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 app: RotInt = .{
.allocator = alloc,
.termbuffer = tty.bufferedWriter(),
.vx = &vx,
.loop = &loop,
.poller = try xev.Timer.init(),
};
try app.initInPlace();
var vx_loop: vaxis.xev.TtyWatcher(RotInt) = undefined;
2024-08-10 10:52:57 -07:00
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);
}