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