Compare commits
2 Commits
7275d1c30e
...
49fce9c584
Author | SHA1 | Date | |
---|---|---|---|
49fce9c584 | |||
f1ad1c3c2f |
4
deps/labjack/ljacklm/build.zig
vendored
4
deps/labjack/ljacklm/build.zig
vendored
@ -17,6 +17,10 @@ pub fn build(b: *std.Build) !void {
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
if (target.result.os.tag == .windows) {
|
||||
libljacklm.defineCMacro("LJACKLM_USE_WINDOWS_MUTEX_SHIM", "1");
|
||||
}
|
||||
|
||||
libljacklm.addCSourceFile(.{ .file = b.path("libljacklm/ljacklm.c") });
|
||||
libljacklm.installHeader(b.path("libljacklm/ljacklm.h"), "ljacklm.h");
|
||||
|
||||
|
13
deps/labjack/ljacklm/libljacklm/ljacklm.c
vendored
13
deps/labjack/ljacklm/libljacklm/ljacklm.c
vendored
@ -23,7 +23,14 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#if defined(LJACKLM_USE_WINDOWS_MUTEX_SHIM)
|
||||
#include <windows.h>
|
||||
#include "windows_mutex_shim.h"
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#endif // LJACKLM_USE_WINDOWS_MUTEX_SHIM
|
||||
|
||||
#include "labjackusb.h"
|
||||
|
||||
|
||||
@ -286,7 +293,9 @@ long GetU12Information( HANDLE hDevice,
|
||||
long *fcddMaxSize,
|
||||
long *hvcMaxSize);
|
||||
|
||||
#if !defined(LJACKLM_USE_WINDOWS_MUTEX_SHIM)
|
||||
unsigned long GetTickCount( void);
|
||||
#endif
|
||||
|
||||
|
||||
__attribute__((constructor))
|
||||
@ -7266,6 +7275,7 @@ long GetU12Information( HANDLE hDevice,
|
||||
}
|
||||
|
||||
|
||||
#if !defined(LJACKLM_USE_WINDOWS_MUTEX_SHIM)
|
||||
//======================================================================
|
||||
//GetTickCount: Implementation of GetTickCount() for Unix. Returns the
|
||||
// current time, expressed as millisconds since the Epoch
|
||||
@ -7276,3 +7286,4 @@ unsigned long GetTickCount( void)
|
||||
gettimeofday(&tv, NULL);
|
||||
return (tv.tv_sec * 1000) + (tv.tv_usec / 1000);
|
||||
}
|
||||
#endif
|
||||
|
28
deps/labjack/ljacklm/libljacklm/windows_mutex_shim.h
vendored
Normal file
28
deps/labjack/ljacklm/libljacklm/windows_mutex_shim.h
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
#ifndef LJACKLM_WIN_MUTEX
|
||||
#define LJACKLM_WIN_MUTEX
|
||||
|
||||
typedef CRITICAL_SECTION pthread_mutex_t;
|
||||
|
||||
static inline int pthread_mutex_init(pthread_mutex_t *mutex, void *attr) {
|
||||
InitializeCriticalSection(mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void pthread_mutex_lock(pthread_mutex_t *mutex) {
|
||||
EnterCriticalSection(mutex);
|
||||
}
|
||||
|
||||
static inline int pthread_mutex_unlock(pthread_mutex_t *mutex) {
|
||||
LeaveCriticalSection(mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int pthread_mutex_trylock(pthread_mutex_t *mutex) {
|
||||
return TryEnterCriticalSection(mutex) != 0;
|
||||
}
|
||||
|
||||
static inline void pthread_mutex_destroy(pthread_mutex_t *mutex) {
|
||||
DeleteCriticalSection(mutex);
|
||||
}
|
||||
|
||||
#endif // LJACKLM_WIN_MUTEX
|
88
src/Config.zig
Normal file
88
src/Config.zig
Normal file
@ -0,0 +1,88 @@
|
||||
const std = @import("std");
|
||||
|
||||
const AzEl = @import("./LabjackYaesu.zig").AzEl;
|
||||
const lj = @import("./labjack.zig");
|
||||
|
||||
const Config = @This();
|
||||
|
||||
var global_internal: Config = undefined;
|
||||
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(allocator, &jread, .{});
|
||||
}
|
||||
|
||||
labjack: LabjackConfig = .{
|
||||
.device = .autodetect,
|
||||
.feedback_calibration = .{
|
||||
.azimuth = .{
|
||||
.minimum = .{ .voltage = 0.0, .angle = 0.0 },
|
||||
.maximum = .{ .voltage = 5.0, .angle = 450.0 },
|
||||
},
|
||||
.elevation = .{
|
||||
.minimum = .{ .voltage = 0.0, .angle = 0.0 },
|
||||
.maximum = .{ .voltage = 5.0, .angle = 180.0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
controller: ControllerConfig = .{
|
||||
.azimuth_input = .{ .channel = .diff_01, .gain_index = 2 },
|
||||
.elevation_input = .{ .channel = .diff_23, .gain_index = 2 },
|
||||
.azimuth_outputs = .{ .increase = .{ .io = 0 }, .decrease = .{ .io = 1 } },
|
||||
.elevation_outputs = .{ .increase = .{ .io = 2 }, .decrease = .{ .io = 3 } },
|
||||
.loop_interval_ns = 50_000_000,
|
||||
.parking_posture = .{ .azimuth = 180, .elevation = 90 },
|
||||
.angle_tolerance = .{ .azimuth = 1, .elevation = 1 },
|
||||
},
|
||||
|
||||
pub const VoltAngle = struct { voltage: f64, angle: f64 };
|
||||
pub const MinMax = struct {
|
||||
minimum: VoltAngle,
|
||||
maximum: VoltAngle,
|
||||
|
||||
pub inline fn slope(self: MinMax) f64 {
|
||||
return self.angleDiff() / self.voltDiff();
|
||||
}
|
||||
|
||||
pub inline fn voltDiff(self: MinMax) f64 {
|
||||
return self.maximum.voltage - self.minimum.voltage;
|
||||
}
|
||||
|
||||
pub inline fn angleDiff(self: MinMax) f64 {
|
||||
return self.maximum.angle - self.minimum.angle;
|
||||
}
|
||||
};
|
||||
|
||||
const LabjackConfig = struct {
|
||||
device: union(enum) {
|
||||
autodetect,
|
||||
serial_number: i32,
|
||||
},
|
||||
// Very basic two-point calibration for each degree of freedom. All other angles are
|
||||
// linearly interpolated from these two points. This assumes the feedback is linear,
|
||||
// which seems to be a mostly reasonable assumption in practice.
|
||||
feedback_calibration: struct {
|
||||
azimuth: MinMax,
|
||||
elevation: MinMax,
|
||||
},
|
||||
};
|
||||
|
||||
const ControllerConfig = struct {
|
||||
azimuth_input: lj.AnalogInput,
|
||||
elevation_input: lj.AnalogInput,
|
||||
|
||||
azimuth_outputs: OutPair,
|
||||
elevation_outputs: OutPair,
|
||||
|
||||
loop_interval_ns: u64,
|
||||
parking_posture: AzEl,
|
||||
angle_tolerance: AzEl,
|
||||
|
||||
const OutPair = struct {
|
||||
increase: lj.DigitalOutputChannel,
|
||||
decrease: lj.DigitalOutputChannel,
|
||||
};
|
||||
};
|
250
src/LabjackYaesu.zig
Normal file
250
src/LabjackYaesu.zig
Normal file
@ -0,0 +1,250 @@
|
||||
const std = @import("std");
|
||||
|
||||
const lj = @import("./labjack.zig");
|
||||
const Config = @import("./Config.zig");
|
||||
const config = Config.global;
|
||||
|
||||
const LabjackYaesu = @This();
|
||||
|
||||
control_thread: std.Thread,
|
||||
lock: *std.Thread.Mutex,
|
||||
controller: *const Controller,
|
||||
|
||||
pub const AzEl = struct {
|
||||
azimuth: f64,
|
||||
elevation: f64,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !LabjackYaesu {
|
||||
const lock = try allocator.create(std.Thread.Mutex);
|
||||
errdefer allocator.destroy(lock);
|
||||
lock.* = .{};
|
||||
|
||||
const controller = try allocator.create(Controller);
|
||||
errdefer allocator.destroy(controller);
|
||||
controller.init(lock);
|
||||
// do this in the main thread so we can throw the error about it synchronously.
|
||||
try controller.connectLabjack();
|
||||
|
||||
return .{
|
||||
.control_thread = try std.Thread.spawn(.{}, runController, .{controller}),
|
||||
.lock = lock,
|
||||
.controller = controller,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setTarget(self: LabjackYaesu, target: AzEl) void {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
const controller = @constCast(self.controller);
|
||||
controller.target = target;
|
||||
controller.requested_state = .running;
|
||||
}
|
||||
|
||||
pub fn startCalibration(self: LabjackYaesu) void {
|
||||
// there are two different types of calibration:
|
||||
// 1. feedback calibration, running to the extents of the rotator
|
||||
// 2. sun calibration, which determines the azimuth and elevation angle
|
||||
// offset between the rotator's physical stops and geodetic north
|
||||
//
|
||||
// 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.
|
||||
}
|
||||
|
||||
pub fn idle(self: LabjackYaesu) void {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
controller.requested_state = .idle;
|
||||
}
|
||||
|
||||
pub fn stop(self: LabjackYaesu) void {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
controller.requested_state = .stopped;
|
||||
}
|
||||
|
||||
fn runController(controller: *Controller) void {
|
||||
controller.run() catch {};
|
||||
}
|
||||
|
||||
const Controller = struct {
|
||||
target: AzEl,
|
||||
position: AzEl,
|
||||
|
||||
current_state: ControllerState,
|
||||
requested_state: ControllerState,
|
||||
|
||||
lock: *std.Thread.Mutex,
|
||||
labjack: lj.Labjack,
|
||||
|
||||
const ControllerState = enum {
|
||||
initializing,
|
||||
idle,
|
||||
calibration,
|
||||
running,
|
||||
stopped,
|
||||
};
|
||||
|
||||
fn init(self: *Controller, lock: *std.Thread.Mutex) void {
|
||||
self.* = .{
|
||||
.target = .{ .azimuth = 0, .elevation = 0 },
|
||||
.position = .{ .azimuth = 0, .elevation = 0 },
|
||||
.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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn connectLabjack(self: *Controller) !void {
|
||||
const info = try controller.labjack.connect();
|
||||
controller.labjack.id = info.local_id;
|
||||
}
|
||||
|
||||
fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
|
||||
return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle;
|
||||
}
|
||||
|
||||
fn lerpAngles(input: [2]lj.AnalogReadResult) AzEl {
|
||||
return .{
|
||||
.azimuth = lerpOne(input[0].voltage, config.labjack.feedback_calibration.azimuth),
|
||||
.elevation = lerpOne(input[1].voltage, config.labjack.feedback_calibration.elevation),
|
||||
};
|
||||
}
|
||||
|
||||
fn signDeadzone(offset: f64, deadzone: f64) enum { negative, zero, positive } {
|
||||
return if (@abs(offset) < deadzone)
|
||||
.zero
|
||||
else if (offset < 0)
|
||||
.negative
|
||||
else
|
||||
.positive;
|
||||
}
|
||||
|
||||
fn updateAzEl(self: *const Controller) !AzEl {
|
||||
const inputs = .{ config.controller.azimuth_input, config.controller.elevation_input };
|
||||
|
||||
const raw = try self.labjack.readAnalogWriteDigital(
|
||||
2,
|
||||
inputs,
|
||||
.{false} ** 4,
|
||||
true,
|
||||
);
|
||||
|
||||
return lerpAngles(raw);
|
||||
}
|
||||
|
||||
fn drive(self: *const Controller, pos_error: AzEl) !AzEl {
|
||||
// NOTE: feedback will be roughly config.controller.loop_interval_ns out of
|
||||
// date. For high loop rates, this shouldn't be an issue.
|
||||
|
||||
const inputs = .{ config.controller.azimuth_input, config.controller.elevation_input };
|
||||
var drive_signal: [4]bool = .{false} ** 4;
|
||||
|
||||
const azsign = signDeadzone(
|
||||
pos_error.azimuth,
|
||||
config.controller.angle_tolerance.azimuth,
|
||||
);
|
||||
|
||||
const elsign = signDeadzone(
|
||||
pos_error.elevation,
|
||||
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;
|
||||
|
||||
const raw = try self.labjack.readAnalogWriteDigital(2, inputs, drive_signal, true);
|
||||
|
||||
return lerpAngles(raw);
|
||||
}
|
||||
|
||||
fn run(self: *Controller) !void {
|
||||
self.state = .initializing;
|
||||
|
||||
var timer: LoopTimer = .{ .interval_ns = config.controller.loop_interval_ns };
|
||||
|
||||
while (timer.mark()) : (timer.sleep()) switch (self.state) {
|
||||
.initializing, .idle => {
|
||||
const pos = self.updateAzEl() catch {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
self.state = .stopped;
|
||||
continue;
|
||||
};
|
||||
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
self.position = pos;
|
||||
self.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;
|
||||
},
|
||||
.running => {
|
||||
const pos_error: AzEl = blk: {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
break :blk .{
|
||||
.azimuth = self.target.azimuth - self.position.azimuth,
|
||||
.elevation = self.target.elevation - self.position.elevation,
|
||||
};
|
||||
};
|
||||
|
||||
const pos = self.drive(pos_error) catch {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
self.state = .stopped;
|
||||
continue;
|
||||
};
|
||||
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
self.position = pos;
|
||||
self.state = self.requested_state;
|
||||
},
|
||||
.stopped => {
|
||||
// attempt to reset the drive outputs
|
||||
_ = self.updateAzEl() catch {};
|
||||
break;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const LoopTimer = struct {
|
||||
interval_ns: u64,
|
||||
|
||||
start: i128 = 0,
|
||||
|
||||
pub fn mark(self: *LoopTimer) bool {
|
||||
self.start = std.time.nanoTimestamp();
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn sleep(self: *LoopTimer) void {
|
||||
const now = std.time.nanoTimestamp();
|
||||
const elapsed: u64 = @intCast(now - self.start);
|
||||
|
||||
std.time.sleep(interval_ns - elapsed);
|
||||
}
|
||||
};
|
288
src/RotCtl.zig
Normal file
288
src/RotCtl.zig
Normal file
@ -0,0 +1,288 @@
|
||||
const std = @import("std");
|
||||
|
||||
const LabjackYaesu = @import("./LabjackYaesu.zig");
|
||||
|
||||
const RotCtl = @This();
|
||||
|
||||
const log = std.log.scoped(.RotCtl);
|
||||
|
||||
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();
|
||||
|
||||
const listen_addr = try std.net.Address.parseIp(
|
||||
config.gpredict_listen_address,
|
||||
config.gpredict_listen_port,
|
||||
);
|
||||
server.listen(listen_addr) catch {
|
||||
log.err("Could not listen on {}. Is it already in use?", .{listen_addr});
|
||||
return;
|
||||
};
|
||||
log.info("Listening for client on: {}", .{listen_addr});
|
||||
|
||||
var interface: RotCtl = .{
|
||||
.writer = undefined,
|
||||
.running = true,
|
||||
.rotator = LabjackYaesu.init(),
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const client = try server.accept();
|
||||
defer {
|
||||
log.info("disconnecting client", .{});
|
||||
client.stream.close();
|
||||
}
|
||||
|
||||
interface.writer = .{ .unbuffered_writer = client.stream.writer() };
|
||||
interface.running = true;
|
||||
defer interface.running = false;
|
||||
|
||||
log.info("client connected from {}", .{client.address});
|
||||
|
||||
var readbuffer = [_]u8{0} ** 512;
|
||||
var fbs = std.io.fixedBufferStream(&readbuffer);
|
||||
const reader = client.stream.reader();
|
||||
|
||||
while (interface.running) : (fbs.reset()) {
|
||||
reader.streamUntilDelimiter(fbs.writer(), '\n', readbuffer.len) catch break;
|
||||
try radio.handleHamlibCommand(fbs.getWritten());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write(self: *const Hamlibber, buf: []const u8) !void {
|
||||
try self.writer.writeAll(buf);
|
||||
}
|
||||
|
||||
fn replyStatus(self: *RadioProxy, comptime status: HamlibErrorCode) !void {
|
||||
try self.write(comptime status.replyFrame() ++ "\n");
|
||||
}
|
||||
|
||||
fn handleHamlibCommand(
|
||||
self: *const Hamlibber,
|
||||
command: []const u8,
|
||||
) !void {
|
||||
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
|
||||
|
||||
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);
|
||||
},
|
||||
// zig fmt: on
|
||||
else => |cmd| {
|
||||
log.err("unknown command {}", .{cmd});
|
||||
try self.replyStatus(.not_implemented);
|
||||
},
|
||||
}
|
||||
} 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,
|
||||
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;
|
||||
}
|
||||
} 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,
|
||||
invalid_configuration = 2,
|
||||
out_of_memory = 3,
|
||||
not_implemented = 4,
|
||||
timeout = 5,
|
||||
io_error = 6,
|
||||
internal_error = 7,
|
||||
protocol_error = 8,
|
||||
command_rejected = 9,
|
||||
parameter_truncated = 10,
|
||||
not_supported = 11,
|
||||
not_targetable = 12,
|
||||
bus_error = 13,
|
||||
bus_busy = 14,
|
||||
invalid_arg = 15,
|
||||
invalid_vfo = 16,
|
||||
domain_error = 17,
|
||||
deprecated = 18,
|
||||
security = 19,
|
||||
power = 20,
|
||||
|
||||
fn replyFrame(comptime self: HamlibErrorCode) []const u8 {
|
||||
return std.fmt.comptimePrint(
|
||||
"RPRT {d}",
|
||||
.{-@as(i8, @intCast(@intFromEnum(self)))},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 },
|
||||
};
|
||||
|
||||
// D, dms2dec 'Degrees' 'Minutes' 'Seconds' 'S/W'
|
||||
// Returns 'Dec Degrees', a signed floating point value.
|
||||
// 'Degrees' and 'Minutes' are integer values.
|
||||
// 'Seconds' is a floating point value.
|
||||
// 'S/W' is a flag with ’1’ indicating South latitude or West longitude and ’0’ North or East (the flag is needed as computers don’t recognize a signed zero even though only the 'Degrees' value is typically signed in DMS notation).
|
||||
// d, dec2dms 'Dec Degrees'
|
||||
// Returns 'Degrees' 'Minutes' 'Seconds' 'S/W'.
|
||||
// Values are as in dms2dec above.
|
||||
// E, dmmm2dec 'Degrees' 'Dec Minutes' 'S/W'
|
||||
// Returns 'Dec Degrees', a signed floating point value.
|
||||
// 'Degrees' is an integer value.
|
||||
// 'Dec Minutes' is a floating point value.
|
||||
// 'S/W' is a flag as in dms2dec above.
|
||||
// e, dec2dmmm 'Dec Deg'
|
||||
// Returns 'Degrees' 'Minutes' 'S/W'.
|
||||
// Values are as in dmmm2dec above.
|
||||
// B, qrb 'Lon 1' 'Lat 1' 'Lon 2' 'Lat 2'
|
||||
// Returns 'Distance' and 'Azimuth'.
|
||||
// 'Distance' is in km.
|
||||
// 'Azimuth' is in degrees.
|
||||
// Supplied Lon/Lat values are signed floating point numbers.
|
||||
// A, a_sp2a_lp 'Short Path Deg'
|
||||
// Returns 'Long Path Deg'.
|
||||
// Both the supplied argument and returned value are floating point values within the range of 0.00 to 360.00.
|
||||
// Note: Supplying a negative value will return an error message.
|
||||
// a, d_sp2d_lp 'Short Path km'
|
||||
// Returns 'Long Path km'.
|
||||
// Both the supplied argument and returned value are floating point values.
|
||||
// pause 'Seconds'
|
||||
// Pause for the given whole (integer) number of 'Seconds' before sending the next command to the rotator.
|
@ -5,13 +5,27 @@ pub fn getDriverVersion() f32 {
|
||||
}
|
||||
|
||||
pub const Labjack = struct {
|
||||
id: ?u32 = null,
|
||||
id: ?i32 = null,
|
||||
demo: bool = false,
|
||||
|
||||
pub fn autodetect() Labjack {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn with_serial_number(sn: i32) Labjack {
|
||||
return .{ .id = sn };
|
||||
}
|
||||
|
||||
pub fn connect(self: Labjack) LabjackError!struct { local_id: i32, firmware_version: f32 } {
|
||||
var id = self.cId();
|
||||
const version = c_api.GetFirmwareVersion(&id);
|
||||
|
||||
return if (version == 0)
|
||||
@as(c_api.LabjackCError, @bitCast(id)).toError()
|
||||
else
|
||||
.{ .local_id = @intCast(id), .firmware_version = version };
|
||||
}
|
||||
|
||||
pub fn firmwareVersion(self: Labjack) LabjackError!f32 {
|
||||
var id = self.cId();
|
||||
const version = c_api.GetFirmwareVersion(&id);
|
||||
@ -24,7 +38,7 @@ pub const Labjack = struct {
|
||||
|
||||
/// Read one analog input channel, either single-ended or differential
|
||||
pub fn analogReadOne(self: Labjack, input: AnalogInput) LabjackError!AnalogReadResult {
|
||||
if (!input.channel.isDifferential() and input.gain != 0) {
|
||||
if (!input.channel.isDifferential() and input.gain_index != 0) {
|
||||
return error.InvalidGain;
|
||||
}
|
||||
|
||||
@ -36,7 +50,7 @@ pub const Labjack = struct {
|
||||
&id,
|
||||
@intFromBool(self.demo),
|
||||
input.channelNumber(),
|
||||
input.gain,
|
||||
input.gain_index,
|
||||
&over_v,
|
||||
&res.voltage,
|
||||
);
|
||||
@ -79,7 +93,7 @@ pub const Labjack = struct {
|
||||
var gains: [incount]c_long = undefined;
|
||||
for (inputs, &in_channels, &gains) |from, *inc, *gain| {
|
||||
inc.* = from.channelNumber();
|
||||
gain.* = from.gain;
|
||||
gain.* = from.gain_index;
|
||||
}
|
||||
var v_out: [4]f32 = .{0} ** 4;
|
||||
var over_v: c_long = 0;
|
||||
@ -119,7 +133,7 @@ pub const Labjack = struct {
|
||||
|
||||
pub const AnalogInput = struct {
|
||||
channel: AnalogInputChannel,
|
||||
gain: Gain = 0,
|
||||
gain_index: GainIndex = 0,
|
||||
|
||||
pub fn channelNumber(self: AnalogInput) u4 {
|
||||
return @intFromEnum(self.channel);
|
||||
@ -191,7 +205,7 @@ pub const DigitalOutputChannel = union(enum) {
|
||||
// 5 => G=10 ±2 volts
|
||||
// 6 => G=16 ±1.25 volts
|
||||
// 7 => G=20 ±1 volt
|
||||
pub const Gain = u3;
|
||||
pub const GainIndex = u3;
|
||||
|
||||
pub const PackedOutput = packed struct(u4) {
|
||||
io0: bool,
|
||||
@ -199,6 +213,18 @@ pub const PackedOutput = packed struct(u4) {
|
||||
io2: bool,
|
||||
io3: bool,
|
||||
|
||||
pub fn setOut(self: *PackedOutput, output: DigitalOutput) !void {
|
||||
switch (output) {
|
||||
.io => |num| switch (num) {
|
||||
0 => self.io0 = output.level,
|
||||
1 => self.io1 = output.level,
|
||||
2 => self.io2 = output.level,
|
||||
3 => self.io3 = output.level,
|
||||
},
|
||||
.d => return error.Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fromBoolArray(states: [4]bool) PackedOutput {
|
||||
return .{
|
||||
.io0 = states[0],
|
||||
@ -214,6 +240,30 @@ pub const PackedOutput = packed struct(u4) {
|
||||
};
|
||||
|
||||
pub const c_api = struct {
|
||||
pub const vendor_id: u16 = 0x0CD5;
|
||||
pub const u12_product_id: u16 = 0x0001;
|
||||
|
||||
pub extern fn OpenLabJack(
|
||||
errorcode: *LabjackCError,
|
||||
vendor_id: c_uint,
|
||||
product_id: c_uint,
|
||||
idnum: *c_long,
|
||||
serialnum: *c_long,
|
||||
caldata: *[20]c_long,
|
||||
) c_long;
|
||||
|
||||
pub extern fn CloseAll(local_id: c_long) c_long;
|
||||
|
||||
pub extern fn GetU12Information(
|
||||
handle: *anyopaque,
|
||||
serialnum: *c_long,
|
||||
local_id: *c_long,
|
||||
power: *c_long,
|
||||
cal_data: *[20]c_long,
|
||||
fcdd_max_size: *c_long,
|
||||
hvc_max_size: *c_long,
|
||||
) c_long;
|
||||
|
||||
pub extern fn EAnalogIn(
|
||||
idnum: *c_long,
|
||||
demo: c_long,
|
14
src/main.zig
14
src/main.zig
@ -1,20 +1,20 @@
|
||||
const std = @import("std");
|
||||
|
||||
const ljack = @import("./ljacklm.zig");
|
||||
const lj = @import("./labjack.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
const ver = ljack.getDriverVersion();
|
||||
const ver = lj.getDriverVersion();
|
||||
std.debug.print("Driver version: {d}\n", .{ver});
|
||||
|
||||
const device = ljack.Labjack.autodetect();
|
||||
const labjack = lj.Labjack.autodetect();
|
||||
|
||||
const in = try device.analogReadOne(.{ .channel = .diff_01, .gain = 2 });
|
||||
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 device.digitalWriteOne(.{ .channel = .{ .io = 0 }, .level = true });
|
||||
try labjack.digitalWriteOne(.{ .channel = .{ .io = 0 }, .level = true });
|
||||
|
||||
const sample = try device.readAnalogWriteDigital(
|
||||
const sample = try labjack.readAnalogWriteDigital(
|
||||
2,
|
||||
.{ .{ .channel = .diff_01, .gain = 2 }, .{ .channel = .diff_23, .gain = 2 } },
|
||||
.{ .{ .channel = .diff_01, .gain_index = 2 }, .{ .channel = .diff_23, .gain_index = 2 } },
|
||||
.{false} ** 4,
|
||||
true,
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user