Compare commits
10 Commits
33adcfc104
...
master
Author | SHA1 | Date | |
---|---|---|---|
ddbfb9746d
|
|||
db55a5081d
|
|||
b6caab906a
|
|||
511f2ca903
|
|||
1a541c71ea
|
|||
7bdea9644c
|
|||
adc2997f0d
|
|||
e1c54fec91
|
|||
8c6d9431c8
|
|||
02381539a7
|
47
src/Config.zig
Normal file
47
src/Config.zig
Normal file
@@ -0,0 +1,47 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Config = @This();
|
||||
|
||||
elevation_mask: f64 = 19,
|
||||
rotctld: AddressPort = .{ .address = "127.0.0.1", .port = 4533 },
|
||||
listen: AddressPort = .{ .address = "127.0.0.1", .port = 42069 },
|
||||
|
||||
const confdir = "rotint";
|
||||
const conffile = "config.json";
|
||||
|
||||
pub fn default() Config {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn load(allocator: std.mem.Allocator, env: *const std.process.EnvMap) !Config {
|
||||
if (env.get("XDG_CONFIG_HOME")) |cfg|
|
||||
if (cfg.len > 0) {
|
||||
return try loadPath(allocator, try std.fs.path.join(
|
||||
allocator,
|
||||
&.{ cfg, confdir, conffile },
|
||||
));
|
||||
};
|
||||
|
||||
if (env.get("HOME")) |home|
|
||||
if (home.len > 0) {
|
||||
return try loadPath(allocator, try std.fs.path.join(
|
||||
allocator,
|
||||
&.{ home, ".config", confdir, conffile },
|
||||
));
|
||||
};
|
||||
|
||||
return error.ConfigHomeMissing;
|
||||
}
|
||||
|
||||
fn loadPath(allocator: std.mem.Allocator, path: []const u8) !Config {
|
||||
defer allocator.free(path);
|
||||
const data = try std.fs.cwd().readFileAlloc(allocator, path, 1024);
|
||||
defer allocator.free(data);
|
||||
|
||||
return try std.json.parseFromSliceLeaky(Config, allocator, data, .{});
|
||||
}
|
||||
|
||||
const AddressPort = struct {
|
||||
address: []const u8,
|
||||
port: u16,
|
||||
};
|
366
src/main.zig
366
src/main.zig
@@ -3,10 +3,10 @@ const builtin = @import("builtin");
|
||||
|
||||
const vaxis = @import("vaxis");
|
||||
const xev = @import("xev");
|
||||
const Cell = vaxis.Cell;
|
||||
|
||||
const networking = @import("./networking.zig");
|
||||
const rotctl = @import("./rotctl.zig");
|
||||
const Config = @import("./Config.zig");
|
||||
|
||||
const log = std.log.scoped(.rotint);
|
||||
|
||||
@@ -19,14 +19,73 @@ pub const std_options: std.Options = .{
|
||||
// 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 },
|
||||
flipflop: bool = false,
|
||||
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,
|
||||
@@ -39,7 +98,7 @@ pub const RotInt = struct {
|
||||
poller: xev.Timer,
|
||||
poll_completion: xev.Completion = undefined,
|
||||
|
||||
const poll_interval: u64 = 1000;
|
||||
log: Log = undefined,
|
||||
|
||||
pub const State = enum {
|
||||
initial,
|
||||
@@ -51,9 +110,20 @@ pub const RotInt = struct {
|
||||
pub fn initInPlace(self: *RotInt) !void {
|
||||
self.server.rotint = self;
|
||||
self.rotator.rotint = self;
|
||||
self.log = try Log.init();
|
||||
|
||||
const connect_addr = try std.net.Address.parseIp("127.0.0.1", 4533);
|
||||
try self.rotator.connect(self.loop, connect_addr);
|
||||
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 {
|
||||
@@ -63,15 +133,20 @@ pub const RotInt = struct {
|
||||
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("127.0.0.1", 42069) catch {
|
||||
self.warn("bogus listen address", .{});
|
||||
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 {
|
||||
self.warn("listen problem", .{});
|
||||
self.server.listen(self.loop, listen_addr) catch |err| {
|
||||
self.showError("listen problem: {s}", .{@errorName(err)});
|
||||
return;
|
||||
};
|
||||
// demangle here to avoid causing initial moves
|
||||
@@ -100,16 +175,36 @@ pub const RotInt = struct {
|
||||
return .disarm;
|
||||
};
|
||||
|
||||
if (self.flipflop) {
|
||||
const mangled: AzEl = .{
|
||||
// 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,
|
||||
.el = self.requested_posture.el,
|
||||
};
|
||||
mangled.el = if (mangled.el > 90)
|
||||
@max(
|
||||
@min(
|
||||
@min(mangled.el, 180 - self.conf.elevation_mask) - self.offsets.el,
|
||||
180 - self.conf.elevation_mask,
|
||||
),
|
||||
90,
|
||||
)
|
||||
else
|
||||
@min(
|
||||
@max(
|
||||
@max(mangled.el, self.conf.elevation_mask) + self.offsets.el,
|
||||
self.conf.elevation_mask,
|
||||
),
|
||||
90,
|
||||
);
|
||||
|
||||
self.sendRotatorCommand(.{ .set_position = mangled });
|
||||
} else {
|
||||
self.sendRotatorCommand(.get_position);
|
||||
}
|
||||
self.flipflop = !self.flipflop;
|
||||
|
||||
return .disarm;
|
||||
}
|
||||
@@ -128,21 +223,28 @@ pub const RotInt = struct {
|
||||
};
|
||||
|
||||
switch (reply) {
|
||||
.okay => {},
|
||||
.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, poll_interval, RotInt, self, poll);
|
||||
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,
|
||||
@@ -162,6 +264,8 @@ pub const RotInt = struct {
|
||||
.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),
|
||||
@@ -174,47 +278,131 @@ pub const RotInt = struct {
|
||||
}
|
||||
|
||||
fn draw(self: *RotInt) !void {
|
||||
const Static = struct {
|
||||
const lower_limit: u8 = 30;
|
||||
const next_ms: u64 = 8;
|
||||
var color_idx: u8 = lower_limit;
|
||||
var dir: enum { up, down } = .up;
|
||||
};
|
||||
|
||||
const style: vaxis.Style = .{
|
||||
.fg = .{ .rgb = [_]u8{ Static.color_idx, Static.color_idx, Static.color_idx } },
|
||||
};
|
||||
|
||||
const segment: vaxis.Segment = .{
|
||||
.text = "yeah ok",
|
||||
.style = style,
|
||||
};
|
||||
const win = self.vx.window();
|
||||
win.clear();
|
||||
|
||||
const y_off = (win.height / 2) -| (6 / 2);
|
||||
const x_off = (win.width / 2) -| (30 / 2);
|
||||
const center = win.child(.{
|
||||
.x_off = x_off + win.x_off,
|
||||
.y_off = y_off + win.y_off,
|
||||
.width = .{ .limit = 30 },
|
||||
.height = .{ .limit = 6 },
|
||||
.border = .{ .where = .all, .style = style },
|
||||
});
|
||||
_ = try center.printSegment(segment, .{ .wrap = .grapheme });
|
||||
switch (Static.dir) {
|
||||
.up => {
|
||||
Static.color_idx += 1;
|
||||
if (Static.color_idx == 255) Static.dir = .down;
|
||||
},
|
||||
.down => {
|
||||
Static.color_idx -= 1;
|
||||
if (Static.color_idx == Static.lower_limit) Static.dir = .up;
|
||||
},
|
||||
}
|
||||
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 {
|
||||
@@ -235,8 +423,15 @@ pub fn main() !void {
|
||||
});
|
||||
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,
|
||||
@@ -245,7 +440,7 @@ pub fn main() !void {
|
||||
try app.initInPlace();
|
||||
|
||||
var vx_loop: vaxis.xev.TtyWatcher(RotInt) = undefined;
|
||||
try vx_loop.init(&tty, &vx, &loop, &app, eventCallback);
|
||||
try vx_loop.init(&tty, &vx, &loop, &app, RotInt.terminalEvent);
|
||||
|
||||
try vx.enterAltScreen(tty.anyWriter());
|
||||
try vx.queryTerminalSend(tty.anyWriter());
|
||||
@@ -256,72 +451,3 @@ pub fn main() !void {
|
||||
|
||||
try loop.run(.until_done);
|
||||
}
|
||||
|
||||
fn eventCallback(
|
||||
self_: ?*RotInt,
|
||||
loop: *xev.Loop,
|
||||
watcher: *vaxis.xev.TtyWatcher(RotInt),
|
||||
event: vaxis.xev.Event,
|
||||
) xev.CallbackAction {
|
||||
const self = self_.?;
|
||||
switch (event) {
|
||||
.key_press => |key| keyp: {
|
||||
var mods = key.mods;
|
||||
mods.caps_lock = false;
|
||||
mods.num_lock = false;
|
||||
const scale: f64 = if (std.meta.eql(mods, .{ .shift = true })) 1 else 10;
|
||||
|
||||
const delta: AzEl = switch (key.codepoint) {
|
||||
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 },
|
||||
'l' => {
|
||||
if (std.meta.eql(mods, .{ .ctrl = true }))
|
||||
self.vx.queueRefresh();
|
||||
break :keyp;
|
||||
},
|
||||
'c' => {
|
||||
if (std.meta.eql(mods, .{ .ctrl = true })) {
|
||||
loop.stop();
|
||||
return .disarm;
|
||||
}
|
||||
break :keyp;
|
||||
},
|
||||
'q' => {
|
||||
if (std.meta.eql(mods, .{})) {
|
||||
loop.stop();
|
||||
return .disarm;
|
||||
}
|
||||
break :keyp;
|
||||
},
|
||||
else => break :keyp,
|
||||
};
|
||||
|
||||
self.offsets.az += delta.az;
|
||||
self.offsets.el += delta.el;
|
||||
},
|
||||
.winsize => |ws| {
|
||||
watcher.vx.resize(self.allocator, watcher.tty.anyWriter(), ws) catch
|
||||
return .disarm;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return .rearm;
|
||||
}
|
||||
|
||||
fn timerCallback(
|
||||
ud: ?*RotInt,
|
||||
loop: *xev.Loop,
|
||||
completion: *xev.Completion,
|
||||
err: xev.Timer.RunError!void,
|
||||
) xev.CallbackAction {
|
||||
_ = err catch @panic("timer error");
|
||||
|
||||
var app = ud orelse return .disarm;
|
||||
app.draw() catch @panic("couldn't draw");
|
||||
|
||||
const timer = xev.Timer.init() catch unreachable;
|
||||
timer.run(loop, completion, 8, RotInt, app, timerCallback);
|
||||
return .disarm;
|
||||
}
|
||||
|
@@ -218,7 +218,9 @@ pub const Client = struct {
|
||||
|
||||
_ = result catch |err| {
|
||||
self.rotint.warn("connect failed {}", .{err});
|
||||
// retry bayeb
|
||||
// Note: this loops instantaneously if the target address is on localhost
|
||||
// and is not being listened on. Actually just straight up crashes on macOS.
|
||||
// Probably need to use a timer to handle this better
|
||||
return .rearm;
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user