From 3838293c9d879f15e504580c654476b0088f42bd Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Wed, 30 Oct 2024 00:26:03 +0200 Subject: [PATCH] add visualization of voltage channels --- build.zig | 13 ++- build.zig.zon | 4 - src/font-face.zig | 140 +++++++++++++++++++++++ src/main.zig | 279 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 426 insertions(+), 10 deletions(-) create mode 100644 src/font-face.zig diff --git a/build.zig b/build.zig index 4ee24fe..354a939 100644 --- a/build.zig +++ b/build.zig @@ -1,13 +1,10 @@ const std = @import("std"); const Module = std.Build.Module; -pub fn build(b: *std.Build) void { +pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const libdaq_dep = b.dependency("libdaq", .{}); - _ = libdaq_dep; // TODO: Build libdaq - var lib: *Module = undefined; { lib = b.createModule(.{ @@ -42,6 +39,14 @@ pub fn build(b: *std.Build) void { exe.linkLibrary(raylib_dep.artifact("raylib")); exe.root_module.addImport("raylib", raylib_dep.module("raylib")); + const external_compiler_support_dir = try std.process.getEnvVarOwned(b.allocator, "NIEXTCCOMPILERSUPP"); + exe.addSystemIncludePath(.{ .cwd_relative = try std.fs.path.join(b.allocator, &.{ external_compiler_support_dir, "include" }) }); + exe.addLibraryPath(.{ .cwd_relative = try std.fs.path.join(b.allocator, &.{ external_compiler_support_dir, "lib64", "msvc" }) }); + + exe.linkSystemLibrary("nidaqmx"); + exe.linkSystemLibrary("odbccp32"); + exe.linkSystemLibrary("odbc32"); + b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); diff --git a/build.zig.zon b/build.zig.zon index 072f4b0..a12248e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,10 +3,6 @@ .version = "0.1.0", .dependencies = .{ - .libdaq = .{ - .url = "https://github.com/snort3/libdaq/archive/refs/tags/v3.0.17.tar.gz", - .hash = "1220338b42823b08d2f848549d03958b19836b4794c12f9a7875bca37a8f9477a5c0", - }, .@"raylib-zig" = .{ .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" diff --git a/src/font-face.zig b/src/font-face.zig new file mode 100644 index 0000000..0fb5d95 --- /dev/null +++ b/src/font-face.zig @@ -0,0 +1,140 @@ +const std = @import("std"); +const rl = @import("raylib"); +const Allocator = std.mem.Allocator; + +font: rl.Font, +spacing: ?f32 = null, +line_height: f32 = 1.4, + +pub fn getSpacing(self: @This()) f32 { + if (self.spacing) |spacing| { + return spacing; + } else { + return self.getSize() / 10; + } +} + +pub fn getSize(self: @This()) f32 { + return @floatFromInt(self.font.baseSize); +} + +pub fn drawTextLines(self: @This(), lines: []const []const u8, position: rl.Vector2, tint: rl.Color) void { + var offset_y: f32 = 0; + + const font_size = self.getSize(); + + for (lines) |line| { + self.drawText(line, position.add(.{ .x = 0, .y = offset_y }), tint); + + const line_size = self.measureText(line); + offset_y += line_size.y + font_size * (self.line_height - 1); + } +} + +pub fn measureTextLines(self: @This(), lines: []const []const u8) rl.Vector2 { + var text_size = rl.Vector2.zero(); + + const font_size = self.getSize(); + + for (lines) |line| { + const line_size = self.measureText(line); + + text_size.x = @max(text_size.x, line_size.x); + text_size.y += line_size.y; + } + + text_size.y += (self.line_height - 1) * font_size * @as(f32, @floatFromInt(@max(lines.len - 1, 0))); + + return text_size; +} + +pub fn drawTextEx(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color, offset: *rl.Vector2) void { + if (self.font.texture.id == 0) return; + + const font_size = self.getSize(); + const spacing = self.getSpacing(); + + var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; + while (iter.nextCodepoint()) |codepoint| { + + if (codepoint == '\n') { + offset.x = 0; + offset.y += font_size * self.line_height; + } else { + if (!(codepoint <= 255 and std.ascii.isWhitespace(@intCast(codepoint)))) { + var codepoint_position = position.add(offset.*); + codepoint_position.x = @round(codepoint_position.x); + codepoint_position.y = @round(codepoint_position.y); + rl.drawTextCodepoint(self.font, codepoint, codepoint_position, font_size, tint); + } + + const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint)); + if (self.font.glyphs[index].advanceX != 0) { + offset.x += @floatFromInt(self.font.glyphs[index].advanceX); + } else { + offset.x += self.font.recs[index].width; + offset.x += @floatFromInt(self.font.glyphs[index].offsetX); + } + offset.x += spacing; + } + } +} + +pub fn drawText(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void { + var offset = rl.Vector2.init(0, 0); + self.drawTextEx(text, position, tint, &offset); +} + +pub fn drawTextAlloc(self: @This(), allocator: Allocator, comptime fmt: []const u8, args: anytype, position: rl.Vector2, tint: rl.Color) !void { + const text = try std.fmt.allocPrint(allocator, fmt, args); + defer allocator.free(text); + + self.drawText(text, position, tint); +} + +pub fn measureText(self: @This(), text: []const u8) rl.Vector2 { + var text_size = rl.Vector2.zero(); + + if (self.font.texture.id == 0) return text_size; // Security check + if (text.len == 0) return text_size; + + const font_size = self.getSize(); + const spacing = self.getSpacing(); + + var line_width: f32 = 0; + text_size.y = font_size; + + var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; + while (iter.nextCodepoint()) |codepoint| { + if (codepoint == '\n') { + text_size.y += font_size * self.line_height; + + line_width = 0; + } else { + if (line_width > 0) { + line_width += spacing; + } + + const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint)); + if (self.font.glyphs[index].advanceX != 0) { + line_width += @floatFromInt(self.font.glyphs[index].advanceX); + } else { + line_width += self.font.recs[index].width; + line_width += @floatFromInt(self.font.glyphs[index].offsetX); + } + + text_size.x = @max(text_size.x, line_width); + } + } + + return text_size; +} + +pub fn drawTextCenter(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void { + const text_size = self.measureText(text); + const adjusted_position = rl.Vector2{ + .x = position.x - text_size.x/2, + .y = position.y - text_size.y/2, + }; + self.drawText(text, adjusted_position, tint); +} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index ba029f9..08abbd4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,15 @@ const std = @import("std"); const rl = @import("raylib"); +const c = @cImport({ + @cInclude("stdint.h"); + @cDefine("__int64", "long long"); + @cInclude("NIDAQmx.h"); +}); + +const Allocator = std.mem.Allocator; +const FontFace = @import("font-face.zig"); +const assert = std.debug.assert; +const Vec2 = rl.Vector2; fn toTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel { return switch (log_level) { @@ -10,6 +20,100 @@ fn toTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel { }; } +fn checkDAQmxError(error_code: i32) !void { + if (error_code != 0) { + var error_msg: [512:0]u8 = .{ 0 } ** 512; + if (c.DAQmxGetErrorString(error_code, &error_msg, error_msg.len) == 0) { + std.debug.print("DAQmx error ({}): {s}\n", .{error_code, error_msg}); + } else { + std.debug.print("DAQmx error ({}): Unknown (Buffer too small for error message)\n", .{error_code}); + } + } + + if (error_code < 0) { + return error.DAQError; + } +} + +fn splitCommaDelimitedList(allocator: Allocator, list: []u8) ![][:0]u8 { + const name_count = std.mem.count(u8, list, ",") + 1; + var names = try std.ArrayList([:0]u8).initCapacity(allocator, name_count); + errdefer names.deinit(); + + var name_iter = std.mem.tokenizeSequence(u8, list, ", "); + while (name_iter.next()) |name| { + names.appendAssumeCapacity(try allocator.dupeZ(u8, name)); + } + + return try names.toOwnedSlice(); +} + +fn listDeviceNames(allocator: Allocator) ![][:0]u8 { + const required_size = c.DAQmxGetSysDevNames(null, 0); + if (required_size == 0) { + return try allocator.alloc([:0]u8, 0); + } + + const device_names_list = try allocator.alloc(u8, @intCast(required_size)); + defer allocator.free(device_names_list); + + try checkDAQmxError( c.DAQmxGetSysDevNames(device_names_list.ptr, @intCast(device_names_list.len)) ); + + const nullbyte = std.mem.indexOfScalar(u8, device_names_list, 0) orelse unreachable; + assert(nullbyte == required_size - 1); + + return try splitCommaDelimitedList(allocator, device_names_list[0..nullbyte]); +} + +fn listDeviceAIPhysicalChannels(allocator: Allocator, device: [:0]u8) ![][:0]u8 { + const required_size = c.DAQmxGetDevAIPhysicalChans(device, null, 0); + if (required_size == 0) { + return try allocator.alloc([:0]u8, 0); + } + + const device_names_list = try allocator.alloc(u8, @intCast(required_size)); + defer allocator.free(device_names_list); + + try checkDAQmxError( c.DAQmxGetDevAIPhysicalChans(device, device_names_list.ptr, @intCast(device_names_list.len)) ); + + const nullbyte = std.mem.indexOfScalar(u8, device_names_list, 0) orelse unreachable; + assert(nullbyte == required_size - 1); + + return try splitCommaDelimitedList(allocator, device_names_list[0..nullbyte]); +} + +fn freeNameList(allocator: Allocator, names: [][:0]u8) void { + for (names) |name| { + allocator.free(name); + } + allocator.free(names); +} + +fn remap(from_min: f32, from_max: f32, to_min: f32, to_max: f32, value: f32) f32 { + const t = (value - from_min) / (from_max - from_min); + return std.math.lerp(to_min, to_max, t); +} + +const Channel = struct { + color: rl.Color, + min_sample: f64, + max_sample: f64, + samples: std.ArrayList(f64), + + fn init(allocator: Allocator) Channel { + return Channel{ + .color = rl.Color.red, + .min_sample = 0, + .max_sample = 0, + .samples = std.ArrayList(f64).init(allocator) + }; + } + + fn deinit(self: Channel) void { + self.samples.deinit(); + } +}; + pub fn main() !void { rl.setTraceLogLevel(toTraceLogLevel(std.log.default_level)); @@ -17,7 +121,68 @@ pub fn main() !void { const allocator = gpa.allocator(); defer _ = gpa.deinit(); - _ = allocator; + + + const devices = try listDeviceNames(allocator); + defer freeNameList(allocator, devices); + + std.debug.print("Devices ({}):\n", .{devices.len}); + for (devices) |device| { + std.debug.print(" * '{s}'\n", .{device}); + + const channel_names = try listDeviceAIPhysicalChannels(allocator, device); + defer freeNameList(allocator, channel_names); + for (channel_names) |channel_name| { + std.debug.print(" * '{s}'\n", .{channel_name}); + } + } + + const sample_rate: f64 = 5000; + + var task_handle: c.TaskHandle = null; + try checkDAQmxError(c.DAQmxCreateTask("", &task_handle)); + defer checkDAQmxError(c.DAQmxClearTask(task_handle)) catch unreachable; + + var channels = std.ArrayList(Channel).init(allocator); + defer channels.deinit(); + defer { + for (channels.items) |channel| { + channel.deinit(); + } + } + + var channel1 = Channel.init(allocator); + channel1.color = rl.Color.red; + channel1.min_sample = -10.0; + channel1.max_sample = 10.0; + try checkDAQmxError(c.DAQmxCreateAIVoltageChan(task_handle, "Dev1/ai1", "", c.DAQmx_Val_Cfg_Default, channel1.min_sample, channel1.max_sample, c.DAQmx_Val_Volts, null)); + try channels.append(channel1); + + var channel2 = Channel.init(allocator); + channel2.color = rl.Color.green; + channel2.min_sample = -10.0; + channel2.max_sample = 10.0; + try checkDAQmxError(c.DAQmxCreateAIVoltageChan(task_handle, "Dev1/ai2", "", c.DAQmx_Val_Cfg_Default, channel2.min_sample, channel2.max_sample, c.DAQmx_Val_Volts, null)); + try channels.append(channel2); + + var channel3 = Channel.init(allocator); + channel3.color = rl.Color.blue; + channel3.min_sample = -10.0; + channel3.max_sample = 10.0; + try checkDAQmxError(c.DAQmxCreateAIVoltageChan(task_handle, "Dev1/ai3", "", c.DAQmx_Val_Cfg_Default, channel3.min_sample, channel3.max_sample, c.DAQmx_Val_Volts, null)); + try channels.append(channel3); + + var channel4 = Channel.init(allocator); + channel4.color = rl.Color.yellow; + channel4.min_sample = -10.0; + channel4.max_sample = 10.0; + try checkDAQmxError(c.DAQmxCreateAIVoltageChan(task_handle, "Dev1/ai4", "", c.DAQmx_Val_Cfg_Default, channel4.min_sample, channel4.max_sample, c.DAQmx_Val_Volts, null)); + try channels.append(channel4); + + try checkDAQmxError(c.DAQmxCfgSampClkTiming(task_handle, null, sample_rate, c.DAQmx_Val_Rising, c.DAQmx_Val_ContSamps, 0)); + + try checkDAQmxError(c.DAQmxStartTask(task_handle)); + defer checkDAQmxError(c.DAQmxStopTask(task_handle)) catch unreachable; rl.initWindow(800, 450, "DAQ view"); defer rl.closeWindow(); @@ -25,12 +190,122 @@ pub fn main() !void { rl.setTargetFPS(60); + var font_face = FontFace{ + .font = rl.getFontDefault() + }; + + var last_read_size: u32 = 0; + var last_read_at: f64 = 0; + while (!rl.windowShouldClose()) { rl.beginDrawing(); defer rl.endDrawing(); rl.clearBackground(rl.Color.white); - rl.drawText("Congrats! You created your first window!", 190, 200, 20, rl.Color.light_gray); + const window_width: f32 = @floatFromInt(rl.getScreenWidth()); + const window_height: f32 = @floatFromInt(rl.getScreenHeight()); + + rl.drawLineV( + Vec2.init(0, window_height/2), + Vec2.init(window_width, window_height/2), + rl.Color.gray + ); + + for (channels.items) |channel| { + const samples = channel.samples; + const min_sample: f32 = @floatCast(channel.min_sample); + const max_sample: f32 = @floatCast(channel.max_sample); + + const max_visible_samples: u32 = @intFromFloat(sample_rate * 5); + var shown_samples = samples.items; + if (shown_samples.len > max_visible_samples) { + shown_samples = samples.items[(samples.items.len-max_visible_samples-1)..(samples.items.len-1)]; + } + + if (shown_samples.len >= 2) { + const color = channel.color; // rl.Color.alpha(channel.color, 1.5 / @as(f32, @floatFromInt(channels.items.len))); + + const samples_per_pixel = max_visible_samples / window_width; + + var i: f32 = 0; + while (i < @as(f32, @floatFromInt(shown_samples.len)) - samples_per_pixel) : (i += samples_per_pixel) { + const next_i = i + samples_per_pixel; + + var min_slice_sample = shown_samples[@intFromFloat(i)]; + var max_slice_sample = shown_samples[@intFromFloat(i)]; + + for (@intFromFloat(i)..@intFromFloat(next_i)) |sub_i| { + min_slice_sample = @min(min_slice_sample, shown_samples[sub_i]); + max_slice_sample = @max(max_slice_sample, shown_samples[sub_i]); + } + + const offset_i: f32 = @floatFromInt(max_visible_samples - shown_samples.len); + + const start_pos = Vec2.init( + (offset_i + i) / max_visible_samples * window_width, + remap(min_sample, max_sample, 0, window_height, @floatCast(min_slice_sample)) + ); + + const end_pos = Vec2.init( + (offset_i + i) / max_visible_samples * window_width, + remap(min_sample, max_sample, 0, window_height, @floatCast(max_slice_sample)) + ); + rl.drawLineV(start_pos, end_pos, color); + } + } + } + + const now = rl.getTime(); + + { + var y: f32 = 10; + try font_face.drawTextAlloc(allocator, "Time: {d:.03}", .{now}, Vec2.init(10, y), rl.Color.black); + y += 10; + + try font_face.drawTextAlloc(allocator, "Last read size (bytes): {d}", .{last_read_size * @as(u32, @intCast(channels.items.len)) * @sizeOf(f64)}, Vec2.init(20, y), rl.Color.black); + y += 10; + + try font_face.drawTextAlloc(allocator, "Last read count per channel: {d}", .{last_read_size}, Vec2.init(20, y), rl.Color.black); + y += 10; + + try font_face.drawTextAlloc(allocator, "Time since last read: {d:.05}", .{now - last_read_at}, Vec2.init(20, y), rl.Color.black); + y += 10; + + for (1.., channels.items) |i, channel| { + const sample_count = channel.samples.items.len; + y += 10; + + try font_face.drawTextAlloc(allocator, "Channel {}:", .{i}, Vec2.init(10, y), rl.Color.black); + y += 10; + + try font_face.drawTextAlloc(allocator, "Sample count: {}", .{sample_count}, Vec2.init(20, y), rl.Color.black); + y += 10; + + try font_face.drawTextAlloc(allocator, "Sample rate: {d:.03}", .{@as(f64, @floatFromInt(sample_count)) / now}, Vec2.init(20, y), rl.Color.black); + y += 10; + } + } + + rl.drawFPS(@as(i32, @intFromFloat(window_width)) - 100, 10); + + var read_buffer: [1024]f64 = undefined; + var read: i32 = 0; + const err = c.DAQmxReadAnalogF64(task_handle, -1, 0, c.DAQmx_Val_GroupByChannel, &read_buffer, @intCast(read_buffer.len), &read, null); + if (err == 0 and read > 0) { + const read_u32 = @as(u32, @intCast(read)); + + for (0.., channels.items) |i, *channel| { + const channel_samples = read_buffer[(i*read_u32)..((i+1)*read_u32)]; + try channel.samples.appendSlice(channel_samples); + } + + last_read_size = read_u32; + last_read_at = now; + } else if (err != 0 and err != c.DAQmxErrorSamplesNotYetAvailable) { + // TODO: Handle error c.DAQmxErrorSamplesNoLongerAvailable + // This error occurs, when application is not reading data fast enough and the DAQmx internal buffer fills up. + try checkDAQmxError(err); + } } }