add visualization of voltage channels

This commit is contained in:
Rokas Puzonas 2024-10-30 00:26:03 +02:00
parent d0e7445421
commit 3838293c9d
4 changed files with 426 additions and 10 deletions

View File

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

View File

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

140
src/font-face.zig Normal file
View File

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

View File

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