Compare commits

..

3 Commits

Author SHA1 Message Date
e639a17424
start writing control and config functionality
In theory, this will poll the feedback lines, but in practice, it
probably crashes or catches on fire or something.
2024-07-03 00:22:40 -07:00
10c40d7d50
deps.labjack: fiddle with Windows support a little
I will probably spend more time on this than I plan to, though I am not
really writing this to run on Windows. At this point, Windows
compilation works but when the driver attempts to read the HID
descriptor, the response is only 45 bytes instead of the expected 75
(which is what it gets when run on Linux). I was under the impression
that this response was just raw data from the device itself, but it's
clearly handled differently on the different platforms, so I think it
will be somewhat interesting to dig into what is different between the
two and why. It's possible I may actually learn something along the
way, unfortunately.
2024-07-03 00:22:02 -07:00
c32390f7c1
deps.labjack: support building for windows
The only pthread functionality this seems to use is mutexes, which are
(fortunately) theoretically trivial to wrap for windows. This
compiles, though it is not clear if it actually works correctly.
2024-07-02 21:30:31 -07:00
9 changed files with 155 additions and 165 deletions

View File

@ -17,6 +17,10 @@ pub fn build(b: *std.Build) !void {
.link_libc = true,
});
if (optimize == .Debug) {
liblabjackusb.defineCMacro("LJ_DEBUG", "1");
}
liblabjackusb.addCSourceFile(.{ .file = b.path("liblabjackusb/labjackusb.c") });
liblabjackusb.installHeader(b.path("liblabjackusb/labjackusb.h"), "labjackusb.h");

View File

@ -505,6 +505,7 @@ static HANDLE LJUSB_OpenSpecificDevice(libusb_device *dev, const struct libusb_d
return NULL;
}
#if defined(_WIN32)
// Test if the kernel driver has the U12.
if (desc->idProduct == U12_PRODUCT_ID && libusb_kernel_driver_active(devh, 0)) {
#if LJ_DEBUG
@ -521,6 +522,7 @@ static HANDLE LJUSB_OpenSpecificDevice(libusb_device *dev, const struct libusb_d
return NULL;
}
}
#endif // _WIN32
r = libusb_claim_interface(devh, 0);
if (r < 0) {

View File

@ -17,6 +17,10 @@ pub fn build(b: *std.Build) !void {
.link_libc = true,
});
if (optimize == .Debug) {
libljacklm.defineCMacro("LJ_DEBUG", "1");
}
if (target.result.os.tag == .windows) {
libljacklm.defineCMacro("LJACKLM_USE_WINDOWS_MUTEX_SHIM", "1");
}

View File

@ -7220,6 +7220,17 @@ long GetU12Information( HANDLE hDevice,
temp = (unsigned long)LJUSB_GetDeviceDescriptorReleaseNumber(hDevice) * 65536; //upper two bytes of serial #
result = LJUSB_GetHIDReportDescriptor(hDevice, repDesc, 75);
#if defined(LJ_DEBUG)
fprintf(stderr, "U12 HID ReportDescriptor (hex, %lu B): ", result);
for (int idx = 0; idx < result; idx++) {
fprintf(stderr, "%02hhX", repDesc[idx]);
}
fprintf(stderr, "\n");
#endif // LJ_DEBUG
if(result < 75)
{
//Failed getting descriptor. First capability would of been input, so

View File

@ -18,7 +18,7 @@ static inline int pthread_mutex_unlock(pthread_mutex_t *mutex) {
}
static inline int pthread_mutex_trylock(pthread_mutex_t *mutex) {
return TryEnterCriticalSection(mutex) != 0;
return TryEnterCriticalSection(mutex) == 0;
}
static inline void pthread_mutex_destroy(pthread_mutex_t *mutex) {

View File

@ -6,15 +6,34 @@ const lj = @import("./labjack.zig");
const Config = @This();
var global_internal: Config = undefined;
pub const global: *const Config;
pub const global: *const Config = &global_internal;
pub fn load(allocator: std.mem.Allocator, reader: anytype) !void {
var jread = std.json.Reader(1024, @TypeOf(reader)).init(allocator, reader);
defer jread.deinit();
global_internal = try std.json.parseFromTokenSourceLeaky(allocator, &jread, .{});
global_internal = try std.json.parseFromTokenSourceLeaky(
Config,
allocator,
&jread,
.{},
);
}
pub fn loadDefault(allocator: std.mem.Allocator) void {
_ = allocator;
global_internal = .{};
}
pub fn destroy(allocator: std.mem.Allocator) void {
// TODO: implement this probably
_ = allocator;
}
rotctl: RotControlConfig = .{
.listen_address = "127.0.0.1",
.listen_port = 5432,
},
labjack: LabjackConfig = .{
.device = .autodetect,
.feedback_calibration = .{
@ -56,6 +75,11 @@ pub const MinMax = struct {
}
};
const RotControlConfig = struct {
listen_address: []const u8,
listen_port: u16,
};
const LabjackConfig = struct {
device: union(enum) {
autodetect,

View File

@ -4,6 +4,8 @@ const lj = @import("./labjack.zig");
const Config = @import("./Config.zig");
const config = Config.global;
const log = std.log.scoped(.labjack_yaesu);
const LabjackYaesu = @This();
control_thread: std.Thread,
@ -51,12 +53,14 @@ pub fn startCalibration(self: LabjackYaesu) void {
// The former is (fairly) trivial to automate, just run until stall
// (assuming there's no deadband in the feedback). The latter requires
// manual input as the human is the feedback hardware in the loop.
_ = self;
}
pub fn idle(self: LabjackYaesu) void {
self.lock.lock();
defer self.lock.unlock();
const controller = @constCast(self.controller);
controller.requested_state = .idle;
}
@ -64,11 +68,17 @@ pub fn stop(self: LabjackYaesu) void {
self.lock.lock();
defer self.lock.unlock();
const controller = @constCast(self.controller);
controller.requested_state = .stopped;
}
fn runController(controller: *Controller) void {
controller.run() catch {};
controller.run() catch {
log.err(
"the labjack control loop has terminated unexpectedly!!!!",
.{},
);
};
}
const Controller = struct {
@ -93,19 +103,19 @@ const Controller = struct {
self.* = .{
.target = .{ .azimuth = 0, .elevation = 0 },
.position = .{ .azimuth = 0, .elevation = 0 },
.state = .stopped,
.current_state = .stopped,
.requested_state = .idle,
.lock = lock,
.labjack = switch (config.labjack.device) {
.autodetect => try labjack.Labjack.autodetect(),
.serial_number => |sn| labjack.Labjack.with_serial_number(sn),
.autodetect => lj.Labjack.autodetect(),
.serial_number => |sn| lj.Labjack.with_serial_number(sn),
},
};
}
fn connectLabjack(self: *Controller) !void {
const info = try controller.labjack.connect();
controller.labjack.id = info.local_id;
const info = try self.labjack.connect();
self.labjack.id = info.local_id;
}
fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
@ -158,10 +168,10 @@ const Controller = struct {
config.controller.angle_tolerance.elevation,
);
drive_signal[config.azimuth_outputs.increase.io] = azsign == .positive;
drive_signal[config.azimuth_outputs.decrease.io] = azsign == .negative;
drive_signal[config.elevation_outputs.increase.io] = elsign == .positive;
drive_signal[config.elevation_outputs.decrease.io] = elsign == .negative;
drive_signal[config.controller.azimuth_outputs.increase.io] = azsign == .positive;
drive_signal[config.controller.azimuth_outputs.decrease.io] = azsign == .negative;
drive_signal[config.controller.elevation_outputs.increase.io] = elsign == .positive;
drive_signal[config.controller.elevation_outputs.decrease.io] = elsign == .negative;
const raw = try self.labjack.readAnalogWriteDigital(2, inputs, drive_signal, true);
@ -169,17 +179,17 @@ const Controller = struct {
}
fn run(self: *Controller) !void {
self.state = .initializing;
self.current_state = .initializing;
var timer: LoopTimer = .{ .interval_ns = config.controller.loop_interval_ns };
while (timer.mark()) : (timer.sleep()) switch (self.state) {
while (timer.mark()) : (timer.sleep()) switch (self.current_state) {
.initializing, .idle => {
const pos = self.updateAzEl() catch {
self.lock.lock();
defer self.lock.unlock();
self.state = .stopped;
self.current_state = .stopped;
continue;
};
@ -187,15 +197,15 @@ const Controller = struct {
defer self.lock.unlock();
self.position = pos;
self.state = self.requested_state;
self.current_state = self.requested_state;
},
.calibration => {
self.lock.lock();
defer self.lock.unlock();
// run calibration routine. psych, this does nothing. gottem
self.state = .idle;
self.requested_state = self.state;
self.current_state = .idle;
self.requested_state = self.current_state;
},
.running => {
const pos_error: AzEl = blk: {
@ -212,7 +222,7 @@ const Controller = struct {
self.lock.lock();
defer self.lock.unlock();
self.state = .stopped;
self.current_state = .stopped;
continue;
};
@ -220,7 +230,7 @@ const Controller = struct {
defer self.lock.unlock();
self.position = pos;
self.state = self.requested_state;
self.current_state = self.requested_state;
},
.stopped => {
// attempt to reset the drive outputs
@ -245,6 +255,6 @@ pub const LoopTimer = struct {
const now = std.time.nanoTimestamp();
const elapsed: u64 = @intCast(now - self.start);
std.time.sleep(interval_ns - elapsed);
std.time.sleep(self.interval_ns - elapsed);
}
};

View File

@ -1,5 +1,6 @@
const std = @import("std");
const config = @import("./Config.zig").global;
const LabjackYaesu = @import("./LabjackYaesu.zig");
const RotCtl = @This();
@ -10,15 +11,16 @@ writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
running: bool,
rotator: LabjackYaesu,
pub fn run() !void {
var server = std.net.StreamServer.init(.{ .reuse_address = true });
defer server.deinit();
pub fn run(allocator: std.mem.Allocator) !void {
// var server = std.net.StreamServer.init(.{ .reuse_address = true });
// defer server.deinit();
const listen_addr = try std.net.Address.parseIp(
config.gpredict_listen_address,
config.gpredict_listen_port,
config.rotctl.listen_address,
config.rotctl.listen_port,
);
server.listen(listen_addr) catch {
var server = listen_addr.listen(.{ .reuse_address = true }) catch {
log.err("Could not listen on {}. Is it already in use?", .{listen_addr});
return;
};
@ -27,7 +29,7 @@ pub fn run() !void {
var interface: RotCtl = .{
.writer = undefined,
.running = true,
.rotator = LabjackYaesu.init(),
.rotator = try LabjackYaesu.init(allocator),
};
while (true) {
@ -49,21 +51,22 @@ pub fn run() !void {
while (interface.running) : (fbs.reset()) {
reader.streamUntilDelimiter(fbs.writer(), '\n', readbuffer.len) catch break;
try radio.handleHamlibCommand(fbs.getWritten());
try interface.handleHamlibCommand(fbs.getWritten());
}
}
}
fn write(self: *const Hamlibber, buf: []const u8) !void {
try self.writer.writeAll(buf);
fn write(self: *RotCtl, buf: []const u8) !void {
try self.writer.writer().writeAll(buf);
try self.writer.flush();
}
fn replyStatus(self: *RadioProxy, comptime status: HamlibErrorCode) !void {
fn replyStatus(self: *RotCtl, comptime status: HamlibErrorCode) !void {
try self.write(comptime status.replyFrame() ++ "\n");
}
fn handleHamlibCommand(
self: *const Hamlibber,
self: *RotCtl,
command: []const u8,
) !void {
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
@ -71,43 +74,13 @@ fn handleHamlibCommand(
const first = tokens.next().?;
if (first.len == 1 or first[0] == '\\') {
switch (first[0]) {
'F' => {
const freqt = tokens.next() orelse
return self.replyStatus(.invalid_parameter);
const new = std.fmt.parseInt(i32, freqt, 10) catch
return self.replyStatus(.invalid_parameter);
self.setFrequency(new) catch
return self.replyStatus(.io_error);
try self.replyStatus(.okay);
},
'f' => {
const freq = self.getFrequency() catch
return self.replyStatus(.io_error);
try self.print("{d}", .{freq});
},
't' => {
try self.print("{d}", .{@intFromBool(self.ptt_state)});
},
'q' => {
self.running = false;
self.replyStatus(.okay) catch return;
},
'\\' => return self.parseLongCommand(first[1..], &tokens),
// zig fmt: off
'*', '1', '2', '4', '_', 'A', 'a', 'B', 'b', 'C', 'c', 'D',
'd', 'E', 'e', 'G', 'g', 'H', 'h', 'I', 'i', 'J', 'j', 'L',
'l', 'M', 'm', 'N', 'n', 'O', 'o', 'P', 'p', 'R', 'r', 'S',
's', 'T', 'U', 'u', 'V', 'v', 'X', 'x', 'Y', 'y', 'Z', 'z',
0x87, 0x88, 0x89, 0x8A, 0x8B, 0x90, 0x91, 0x92, 0x93, 0xF3, 0xF5
=> |cmd| {
log.warn("Unsupported command {c}", .{cmd});
try self.replyStatus(.not_supported);
'\\' => {
return try self.parseLongCommand(first[1..], &tokens);
},
// zig fmt: on
else => |cmd| {
log.err("unknown command {}", .{cmd});
try self.replyStatus(.not_implemented);
@ -116,30 +89,25 @@ fn handleHamlibCommand(
} else if (std.mem.eql(u8, first, "AOS")) {
// gpredict just kind of shoves this message in on top of the HamLib
// protocol.
log.info("Received AOS message from gpredict", .{});
self.setFrequency(self.base_freq) catch return self.replyStatus(.io_error);
try self.replyStatus(.okay);
} else if (std.mem.eql(u8, first, "LOS")) {
log.info("Received LOS message from gpredict", .{});
if (self.stop_on_los) {
self.running = false;
self.setFrequency(self.base_freq) catch return self.replyStatus(.io_error);
}
try self.replyStatus(.okay);
} else try self.replyStatus(.not_supported);
}
fn parseLongCommand(
self: *RadioProxy,
self: *RotCtl,
command: []const u8,
tokens: *std.mem.TokenIterator(u8, .scalar),
) !void {
_ = tokens;
for (hamlib_commands) |check| {
if (command.len >= check.long.len and std.mem.eql(u8, check.long, command[0..check.long.len])) {
log.warn("Unsupported command {s}", .{command});
break;
for (rotctl_commands) |check| {
if (check.long) |long| {
if (command.len >= long.len and std.mem.eql(u8, long, command)) {
log.warn("Unsupported command {s}", .{command});
break;
}
}
} else {
log.warn("Unknown command {s}", .{command});
@ -147,47 +115,6 @@ fn parseLongCommand(
return self.replyStatus(.not_supported);
}
fn resetWatchdog(self: *RadioProxy) !void {
const wd = spacecraft.shared.commands.watchdog;
const reqheader = csp.Header{
.priority = 1,
.source = Config.global.doppler_shift.gs_source_address,
.destination = Config.global.doppler_shift.gs_radio_address,
.destination_port = @intFromEnum(wd.Reset.port),
.source_port = self.reqPort(),
};
const request = wd.Reset{};
const packet = reqheader.packBig() ++ request.packLittle();
try self.nats.publish(self.nats_up, &packet);
while (true) {
const res = self.nats_down.nextMessage(reply_timeout) catch |err| {
log.err("Could not reset ground UHF radio watchdog. Is the radio on and bridged to NATS?", .{});
return err;
};
const data = res.getData() orelse continue;
if (data.len != csp.Header.pack_size + wd.Reset.Response.pack_size)
continue;
const resheader = csp.Header.unpackBig(data[0..csp.Header.pack_size].*);
if (!resheader.isReply(reqheader)) continue;
const reply = wd.Reset.Response.unpackLittle(
data[csp.Header.pack_size..][0..wd.Reset.Response.pack_size].*,
);
if (reply.err != .okay) {
log.err("Got error response for ground UHF watchdog reset: {}", .{reply.err.code()});
return error.Failure;
}
break;
}
log.info("Successfully reset the ground UHF radio watchdog timer.", .{});
}
const HamlibErrorCode = enum(u8) {
okay = 0,
invalid_parameter = 1,
@ -222,38 +149,33 @@ const HamlibErrorCode = enum(u8) {
const HamlibCommand = struct {
short: ?u8 = null,
long: ?[]const u8 = null,
mode: enum { rotator, radio, both },
};
const hamlib_commands = [_]HamlibCommand{
.{ .short = 'F', .long = "set_freq", .mode = .radio },
.{ .short = 'f', .long = "get_freq", .mode = .radio },
.{ .short = 'T', .long = "set_ptt", .mode = .radio },
.{ .short = 't', .long = "get_ptt", .mode = .radio },
.{ .short = 'q', .mode = .both }, // quit
.{ .short = 'Q', .mode = .both }, // quit
.{ .short = 'P', .long = "set_pos", .mode = .rotator }, // azimuth: f64, elevation: f64
.{ .short = 'p', .long = "get_pos", .mode = .rotator }, // return az: f64, el: f64
.{ .short = 'M', .long = "move", .mode = .rotator }, // direction: enum { up=2, down=4, left=8, right=16 }, speed: i8 (0-100 or -1)
.{ .short = 'S', .long = "stop", .mode = .rotator },
.{ .short = 'K', .long = "park", .mode = .rotator },
.{ .short = 'C', .long = "set_conf", .mode = .rotator }, // token: []const u8, value: []const u8
.{ .short = 'R', .long = "reset", .mode = .rotator }, // u1 (1 is reset all)
.{ .short = '_', .long = "get_info", .mode = .rotator }, // return Model name
.{ .short = 'K', .long = "park", .mode = .rotator },
.{ .long = "dump_state", .mode = .rotator }, // ???
.{ .short = '1', .long = "dump_caps", .mode = .rotator }, // ???
.{ .short = 'w', .long = "send_cmd", .mode = .rotator }, // []const u8, send serial command directly to the rotator
.{ .short = 'L', .long = "lonlat2loc", .mode = .rotator }, // return Maidenhead locator for given long: f64 and lat: f64, locator precision: u4 (2-12)
.{ .short = 'l', .long = "loc2lonlat", .mode = .rotator }, // the inverse of the above
.{ .short = 'D', .long = "dms2dec", .mode = .rotator }, // deg, min, sec, 0 (positive) or 1 (negative)
.{ .short = 'd', .long = "dec2dms", .mode = .rotator },
.{ .short = 'E', .long = "dmmm2dec", .mode = .rotator },
.{ .short = 'e', .long = "dec2dmmm", .mode = .rotator },
.{ .short = 'B', .long = "grb", .mode = .rotator },
.{ .short = 'A', .long = "a_sp2a_lp", .mode = .rotator },
.{ .short = 'a', .long = "d_sp2d_lp", .mode = .rotator },
.{ .long = "pause", .mode = .rotator },
const rotctl_commands = [_]HamlibCommand{
.{ .short = 'q' }, // quit
.{ .short = 'Q' }, // quit
.{ .short = 'P', .long = "set_pos" }, // azimuth: f64, elevation: f64
.{ .short = 'p', .long = "get_pos" }, // return az: f64, el: f64
.{ .short = 'M', .long = "move" }, // direction: enum { up=2, down=4, left=8, right=16 }, speed: i8 (0-100 or -1)
.{ .short = 'S', .long = "stop" },
.{ .short = 'K', .long = "park" },
.{ .short = 'C', .long = "set_conf" }, // token: []const u8, value: []const u8
.{ .short = 'R', .long = "reset" }, // u1 (1 is reset all)
.{ .short = '_', .long = "get_info" }, // return Model name
.{ .short = 'K', .long = "park" },
.{ .long = "dump_state" }, // ???
.{ .short = '1', .long = "dump_caps" }, // ???
.{ .short = 'w', .long = "send_cmd" }, // []const u8, send serial command directly to the rotator
.{ .short = 'L', .long = "lonlat2loc" }, // return Maidenhead locator for given long: f64 and lat: f64, locator precision: u4 (2-12)
.{ .short = 'l', .long = "loc2lonlat" }, // the inverse of the above
.{ .short = 'D', .long = "dms2dec" }, // deg, min, sec, 0 (positive) or 1 (negative)
.{ .short = 'd', .long = "dec2dms" },
.{ .short = 'E', .long = "dmmm2dec" },
.{ .short = 'e', .long = "dec2dmmm" },
.{ .short = 'B', .long = "grb" },
.{ .short = 'A', .long = "a_sp2a_lp" },
.{ .short = 'a', .long = "d_sp2d_lp" },
.{ .long = "pause" },
};
// D, dms2dec 'Degrees' 'Minutes' 'Seconds' 'S/W'

View File

@ -1,25 +1,38 @@
const std = @import("std");
const Config = @import("./Config.zig");
const lj = @import("./labjack.zig");
const RotCtl = @import("./RotCtl.zig");
const log = std.log.scoped(.main);
pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
blk: {
const conf_file = std.fs.cwd().openFile("yaes.json", .{}) catch {
log.warn("Could not load config file yaes.json. Using default config.", .{});
Config.loadDefault(allocator);
break :blk;
};
defer conf_file.close();
Config.load(allocator, conf_file.reader()) catch {
log.err("Could not parse config file yaes.json. Good luck figuring out why.", .{});
return 1;
};
}
defer Config.destroy(allocator);
pub fn main() !void {
const ver = lj.getDriverVersion();
std.debug.print("Driver version: {d}\n", .{ver});
const labjack = lj.Labjack.autodetect();
RotCtl.run(allocator) catch |err| {
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
return 1;
};
const in = try labjack.analogReadOne(.{ .channel = .diff_01, .gain_index = 2 });
std.debug.print("Read voltage: {d}. Overvolt: {}\n", .{ in.voltage, in.over_voltage });
try labjack.digitalWriteOne(.{ .channel = .{ .io = 0 }, .level = true });
const sample = try labjack.readAnalogWriteDigital(
2,
.{ .{ .channel = .diff_01, .gain_index = 2 }, .{ .channel = .diff_23, .gain_index = 2 } },
.{false} ** 4,
true,
);
for (sample, 0..) |input, idx| {
std.debug.print(" channel {d}: {d} V\n", .{ idx, input.voltage });
}
return 0;
}