Compare commits

..

2 Commits

Author SHA1 Message Date
49fce9c584
start writing control and config functionality
This is not hooked up and does nothing notable, yet. Soon there will
probably be something to test with hardware, though.
2024-07-01 23:55:56 -07:00
f1ad1c3c2f
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-01 19:28:30 -07:00
9 changed files with 165 additions and 155 deletions

View File

@ -17,10 +17,6 @@ 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,7 +505,6 @@ 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
@ -522,7 +521,6 @@ 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,10 +17,6 @@ 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,17 +7220,6 @@ 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,34 +6,15 @@ const lj = @import("./labjack.zig");
const Config = @This();
var global_internal: Config = undefined;
pub const global: *const Config = &global_internal;
pub const global: *const Config;
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(
Config,
allocator,
&jread,
.{},
);
global_internal = try std.json.parseFromTokenSourceLeaky(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 = .{
@ -75,11 +56,6 @@ pub const MinMax = struct {
}
};
const RotControlConfig = struct {
listen_address: []const u8,
listen_port: u16,
};
const LabjackConfig = struct {
device: union(enum) {
autodetect,

View File

@ -4,8 +4,6 @@ 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,
@ -53,14 +51,12 @@ 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;
}
@ -68,17 +64,11 @@ 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 {
log.err(
"the labjack control loop has terminated unexpectedly!!!!",
.{},
);
};
controller.run() catch {};
}
const Controller = struct {
@ -103,19 +93,19 @@ const Controller = struct {
self.* = .{
.target = .{ .azimuth = 0, .elevation = 0 },
.position = .{ .azimuth = 0, .elevation = 0 },
.current_state = .stopped,
.state = .stopped,
.requested_state = .idle,
.lock = lock,
.labjack = switch (config.labjack.device) {
.autodetect => lj.Labjack.autodetect(),
.serial_number => |sn| lj.Labjack.with_serial_number(sn),
.autodetect => try labjack.Labjack.autodetect(),
.serial_number => |sn| labjack.Labjack.with_serial_number(sn),
},
};
}
fn connectLabjack(self: *Controller) !void {
const info = try self.labjack.connect();
self.labjack.id = info.local_id;
const info = try controller.labjack.connect();
controller.labjack.id = info.local_id;
}
fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
@ -168,10 +158,10 @@ const Controller = struct {
config.controller.angle_tolerance.elevation,
);
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;
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;
const raw = try self.labjack.readAnalogWriteDigital(2, inputs, drive_signal, true);
@ -179,17 +169,17 @@ const Controller = struct {
}
fn run(self: *Controller) !void {
self.current_state = .initializing;
self.state = .initializing;
var timer: LoopTimer = .{ .interval_ns = config.controller.loop_interval_ns };
while (timer.mark()) : (timer.sleep()) switch (self.current_state) {
while (timer.mark()) : (timer.sleep()) switch (self.state) {
.initializing, .idle => {
const pos = self.updateAzEl() catch {
self.lock.lock();
defer self.lock.unlock();
self.current_state = .stopped;
self.state = .stopped;
continue;
};
@ -197,15 +187,15 @@ const Controller = struct {
defer self.lock.unlock();
self.position = pos;
self.current_state = self.requested_state;
self.state = self.requested_state;
},
.calibration => {
self.lock.lock();
defer self.lock.unlock();
// run calibration routine. psych, this does nothing. gottem
self.current_state = .idle;
self.requested_state = self.current_state;
self.state = .idle;
self.requested_state = self.state;
},
.running => {
const pos_error: AzEl = blk: {
@ -222,7 +212,7 @@ const Controller = struct {
self.lock.lock();
defer self.lock.unlock();
self.current_state = .stopped;
self.state = .stopped;
continue;
};
@ -230,7 +220,7 @@ const Controller = struct {
defer self.lock.unlock();
self.position = pos;
self.current_state = self.requested_state;
self.state = self.requested_state;
},
.stopped => {
// attempt to reset the drive outputs
@ -255,6 +245,6 @@ pub const LoopTimer = struct {
const now = std.time.nanoTimestamp();
const elapsed: u64 = @intCast(now - self.start);
std.time.sleep(self.interval_ns - elapsed);
std.time.sleep(interval_ns - elapsed);
}
};

View File

@ -1,6 +1,5 @@
const std = @import("std");
const config = @import("./Config.zig").global;
const LabjackYaesu = @import("./LabjackYaesu.zig");
const RotCtl = @This();
@ -11,16 +10,15 @@ writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
running: bool,
rotator: LabjackYaesu,
pub fn run(allocator: std.mem.Allocator) !void {
// var server = std.net.StreamServer.init(.{ .reuse_address = true });
// defer server.deinit();
pub fn run() !void {
var server = std.net.StreamServer.init(.{ .reuse_address = true });
defer server.deinit();
const listen_addr = try std.net.Address.parseIp(
config.rotctl.listen_address,
config.rotctl.listen_port,
config.gpredict_listen_address,
config.gpredict_listen_port,
);
var server = listen_addr.listen(.{ .reuse_address = true }) catch {
server.listen(listen_addr) catch {
log.err("Could not listen on {}. Is it already in use?", .{listen_addr});
return;
};
@ -29,7 +27,7 @@ pub fn run(allocator: std.mem.Allocator) !void {
var interface: RotCtl = .{
.writer = undefined,
.running = true,
.rotator = try LabjackYaesu.init(allocator),
.rotator = LabjackYaesu.init(),
};
while (true) {
@ -51,22 +49,21 @@ pub fn run(allocator: std.mem.Allocator) !void {
while (interface.running) : (fbs.reset()) {
reader.streamUntilDelimiter(fbs.writer(), '\n', readbuffer.len) catch break;
try interface.handleHamlibCommand(fbs.getWritten());
try radio.handleHamlibCommand(fbs.getWritten());
}
}
}
fn write(self: *RotCtl, buf: []const u8) !void {
try self.writer.writer().writeAll(buf);
try self.writer.flush();
fn write(self: *const Hamlibber, buf: []const u8) !void {
try self.writer.writeAll(buf);
}
fn replyStatus(self: *RotCtl, comptime status: HamlibErrorCode) !void {
fn replyStatus(self: *RadioProxy, comptime status: HamlibErrorCode) !void {
try self.write(comptime status.replyFrame() ++ "\n");
}
fn handleHamlibCommand(
self: *RotCtl,
self: *const Hamlibber,
command: []const u8,
) !void {
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
@ -74,13 +71,43 @@ 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 try self.parseLongCommand(first[1..], &tokens);
'\\' => 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);
},
// zig fmt: on
else => |cmd| {
log.err("unknown command {}", .{cmd});
try self.replyStatus(.not_implemented);
@ -89,32 +116,78 @@ 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: *RotCtl,
self: *RadioProxy,
command: []const u8,
tokens: *std.mem.TokenIterator(u8, .scalar),
) !void {
_ = tokens;
for (rotctl_commands) |check| {
if (check.long) |long| {
if (command.len >= long.len and std.mem.eql(u8, long, command)) {
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;
}
}
} else {
log.warn("Unknown command {s}", .{command});
}
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,
@ -149,33 +222,38 @@ const HamlibErrorCode = enum(u8) {
const HamlibCommand = struct {
short: ?u8 = null,
long: ?[]const u8 = null,
mode: enum { rotator, radio, both },
};
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" },
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 },
};
// D, dms2dec 'Degrees' 'Minutes' 'Seconds' 'S/W'

View File

@ -1,38 +1,25 @@
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});
RotCtl.run(allocator) catch |err| {
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
return 1;
};
const labjack = lj.Labjack.autodetect();
return 0;
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 });
}
}