Compare commits

..

10 Commits

Author SHA1 Message Date
ddbfb9746d main: respect the elevation mask after applying offsets
This prevents offsets from being able to point below the elevation
mask. The way this is done should probably be reworked, but this whole
thing is such a messy hack anyway that, like, whatever, man.
2024-08-22 13:59:43 -07:00
db55a5081d main: treat elevation offset as offset from horizon
Rather than having somewhat confusing flipped logic when the rotator is
operating in the 180-90 degree elevation regime, this internally
performs sign flipping. This means that a 3 degree elevation offset
means "point an additional 3 degrees up from the horizon" regardless
of whether the rotator is operating in the 0-90 or 180-90 elevation
ranges.
2024-08-22 13:06:27 -07:00
b6caab906a main: respect elevation mask
This is done silently, in the sense that the UI doesn't show that it is
clamping the requested pointing target in this way.
2024-08-22 11:49:01 -07:00
511f2ca903 add config
This is very basic, but it beats having everything be hardcoded.
2024-08-22 11:46:54 -07:00
1a541c71ea main: full redraw on initial connection
This seems to help if rotint is started before rotctld (which just
crashes on mac).
2024-08-14 15:26:06 -07:00
7bdea9644c main: change default polling intervals
The serial interface on the MD-01 seems to be extremely slow (this is
with USB serial, though the RS232 ports probably have the exact same
issue. It seems that sending requests too quickly may negatively
impact the performance of the controller/rotator (i.e. makes it shake
more)? I'm not exactly sure this is the correct conclusion, since our
timings do not compensate for the actual communication time with the
MD-01 (i.e. the 100ms poll rate sends the next request at least 100ms
after it has received a reply). However, based on empirical testing,
running these at a lower pace (perhaps particularly, the command
frequency?) seems to make a big difference in the amount of shake
observed. We will need to keep an eye on this over time.
2024-08-14 14:51:47 -07:00
adc2997f0d main: add logging to a file
This is done synchronously, but assumed to be fast enough that it won't
cause problems for the event loop. I may try to use the libxev file
abstraction in the future but this is the real fast way of
implementing it. This codebase is a mess, and that's an endorsement.
2024-08-12 20:29:50 -07:00
e1c54fec91 main: add configurable poll/command rates
These are coupled in a somewhat nonintuitive manner. Previously, we
would send a command every other request. Due to an oversight, we were
only sending a request once per second, which meant we were reading
position once every two seconds and writing position once every two
seconds. However, this seemed to work pretty well (at least the radio
performance was largely improved and qualitatively it seemed like the
controller was doing a better job of keeping up with the pointing
target).

I wanted the UI to update the position more frequently, so we keep the
scheme of sending a command every N requests, but we send
significantly more get position requests. These can be adjusted on the
fly in order to get an idea of how much the communication pattern
actually impacts the pointing performance.
2024-08-11 20:31:01 -07:00
8c6d9431c8 main: some minor cleanup 2024-08-10 10:54:15 -07:00
02381539a7 main: draw something resembling a user interface
Very simplistic, but, and here's the key, it works.
2024-08-10 10:53:30 -07:00
3 changed files with 273 additions and 92 deletions

47
src/Config.zig Normal file
View 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,
};

View File

@@ -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,22 +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,
@@ -163,6 +264,7 @@ 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 {};
},
@@ -179,7 +281,7 @@ pub const RotInt = struct {
const win = self.vx.window();
win.clear();
var lines: [3][128]u8 = undefined;
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}",
@@ -195,17 +297,112 @@ pub const RotInt = struct {
"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 });
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 });
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 {
@@ -226,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,
@@ -236,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());
@@ -247,75 +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;
self.draw() catch {
self.warn("draw failure", .{});
};
},
.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;
}

View File

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