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
8 changed files with 733 additions and 14 deletions

View File

@ -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");

View File

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

View 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
View 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
View 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
View 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 dont 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.

View File

@ -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,

View File

@ -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,
);