From 49fce9c584d0e57d45b8b83d2e5ee513110a17fb Mon Sep 17 00:00:00 2001 From: torque Date: Mon, 1 Jul 2024 23:55:56 -0700 Subject: [PATCH] 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. --- src/Config.zig | 88 ++++++++++ src/LabjackYaesu.zig | 250 +++++++++++++++++++++++++++ src/RotCtl.zig | 288 +++++++++++++++++++++++++++++++ src/{ljacklm.zig => labjack.zig} | 62 ++++++- src/main.zig | 14 +- 5 files changed, 689 insertions(+), 13 deletions(-) create mode 100644 src/Config.zig create mode 100644 src/LabjackYaesu.zig create mode 100644 src/RotCtl.zig rename src/{ljacklm.zig => labjack.zig} (92%) diff --git a/src/Config.zig b/src/Config.zig new file mode 100644 index 0000000..421f235 --- /dev/null +++ b/src/Config.zig @@ -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, + }; +}; diff --git a/src/LabjackYaesu.zig b/src/LabjackYaesu.zig new file mode 100644 index 0000000..8c0603a --- /dev/null +++ b/src/LabjackYaesu.zig @@ -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); + } +}; diff --git a/src/RotCtl.zig b/src/RotCtl.zig new file mode 100644 index 0000000..9b81a41 --- /dev/null +++ b/src/RotCtl.zig @@ -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. diff --git a/src/ljacklm.zig b/src/labjack.zig similarity index 92% rename from src/ljacklm.zig rename to src/labjack.zig index c25892e..950ae10 100644 --- a/src/ljacklm.zig +++ b/src/labjack.zig @@ -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, diff --git a/src/main.zig b/src/main.zig index f72cbc0..ba1928c 100644 --- a/src/main.zig +++ b/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, );