add button to start recording samples

This commit is contained in:
Rokas Puzonas 2025-02-06 02:13:00 +02:00
parent 65ad8d7786
commit 785355509e
6 changed files with 590 additions and 526 deletions

View File

@ -8,6 +8,7 @@ const Graph = @import("./graph.zig");
const NIDaq = @import("ni-daq.zig");
const rect_utils = @import("./rect-utils.zig");
const remap = @import("./utils.zig").remap;
const TaskPool = @import("./task-pool.zig");
const log = std.log.scoped(.app);
const assert = std.debug.assert;
@ -16,25 +17,85 @@ const clamp = std.math.clamp;
const App = @This();
const max_channels = 64;
const max_files = 32;
const Channel = struct {
const FileChannel = struct {
path: []u8,
min_value: f64,
max_value: f64,
samples: []f64,
fn deinit(self: FileChannel, allocator: std.mem.Allocator) void {
allocator.free(self.path);
allocator.free(self.samples);
}
};
const DeviceChannel = struct {
const Name = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); // +1 for null byte
name: Name = .{},
mutex: std.Thread.Mutex = .{},
samples: std.ArrayList(f64),
units: i32 = NIDaq.c.DAQmx_Val_Volts,
min_sample_rate: f64,
max_sample_rate: f64,
min_value: f64,
max_value: f64,
active_task: ?*TaskPool.Entry = null,
fn deinit(self: DeviceChannel) void {
self.samples.deinit();
}
};
const ChannelView = struct {
view_cache: Graph.Cache = .{},
view_rect: Graph.ViewOptions,
follow: bool = false,
height: f32 = 150,
min_value: f64,
max_value: f64,
samples: union(enum) {
owned: []f64,
source: union(enum) {
file: usize,
device: usize
},
const SourceObject = union(enum) {
file: *FileChannel,
device: *DeviceChannel,
fn samples(self: SourceObject) []const f64 {
return switch (self) {
.file => |file| file.samples,
.device => |device| device.samples.items,
};
}
fn lockSamples(self: SourceObject) void {
if (self == .device) {
self.device.mutex.lock();
}
}
fn unlockSamples(self: SourceObject) void {
if (self == .device) {
self.device.mutex.unlock();
}
}
};
};
allocator: std.mem.Allocator,
ui: UI,
channels: std.BoundedArray(Channel, max_channels) = .{},
channel_views: std.BoundedArray(ChannelView, max_channels) = .{},
ni_daq: NIDaq,
task_pool: TaskPool,
loaded_files: [max_channels]?FileChannel = .{ null } ** max_channels,
device_channels: [max_channels]?DeviceChannel = .{ null } ** max_channels,
shown_window: enum {
channels,
@ -46,11 +107,8 @@ show_voltage_analog_inputs: bool = true,
show_voltage_analog_outputs: bool = true,
selected_channels: std.BoundedArray([:0]u8, max_channels) = .{},
pub fn init(allocator: std.mem.Allocator) !App {
return App{
.allocator = allocator,
.ui = UI.init(allocator),
.ni_daq = try NIDaq.init(allocator, .{
pub fn init(self: *App, allocator: std.mem.Allocator) !void {
var ni_daq = try NIDaq.init(allocator, .{
.max_devices = 4,
.max_analog_inputs = 32,
.max_analog_outputs = 8,
@ -58,25 +116,48 @@ pub fn init(allocator: std.mem.Allocator) !App {
.max_counter_inputs = 8,
.max_analog_input_voltage_ranges = 4,
.max_analog_output_voltage_ranges = 4
})
});
errdefer ni_daq.deinit(allocator);
self.* = App{
.allocator = allocator,
.ui = UI.init(allocator),
.ni_daq = ni_daq,
.task_pool = undefined
};
try TaskPool.init(&self.task_pool, allocator, &self.ni_daq);
errdefer self.task_pool.deinit();
}
pub fn deinit(self: *App) void {
self.ni_daq.deinit(self.allocator);
for (self.channels.slice()) |*channel| {
switch (channel.samples) {
.owned => |owned| self.allocator.free(owned)
}
self.task_pool.deinit();
for (self.channel_views.slice()) |*channel| {
channel.view_cache.deinit();
}
for (&self.loaded_files) |*loaded_file| {
if (loaded_file.*) |*f| {
f.deinit(self.allocator);
loaded_file.* = null;
}
}
for (&self.device_channels) |*device_channel| {
if (device_channel.*) |*c| {
c.deinit();
device_channel.* = null;
}
}
for (self.selected_channels.constSlice()) |channel| {
self.allocator.free(channel);
}
self.selected_channels.len = 0;
self.ui.deinit();
self.ni_daq.deinit(self.allocator);
}
fn showButton(self: *App, text: []const u8) UI.Interaction {
@ -134,61 +215,126 @@ fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 {
return samples;
}
pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void {
fn findFreeSlot(T: type, slice: []const ?T) ?usize {
for (0.., slice) |i, loaded_file| {
if (loaded_file == null) {
return i;
}
}
return null;
}
pub fn appendChannelFromFile(self: *App, path: []const u8) !void {
const path_dupe = try self.allocator.dupe(u8, path);
errdefer self.allocator.free(path_dupe);
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const samples = try readSamplesFromFile(self.allocator, file);
errdefer self.allocator.free(samples);
var min_value = samples[0];
var max_value = samples[0];
var min_value: f64 = 0;
var max_value: f64 = 0;
if (samples.len > 0) {
min_value = samples[0];
max_value = samples[0];
for (samples) |sample| {
min_value = @min(min_value, sample);
max_value = @max(max_value, sample);
}
}
const margin = 0.1;
try self.channels.append(Channel{
const loaded_file_index = findFreeSlot(FileChannel, &self.loaded_files) orelse return error.FileLimitReached;
self.loaded_files[loaded_file_index] = FileChannel{
.min_value = min_value,
.max_value = max_value,
.path = path_dupe,
.samples = samples
};
errdefer self.loaded_files[loaded_file_index] = null;
const margin = 0.1;
const sample_range = max_value - min_value;
self.channel_views.appendAssumeCapacity(ChannelView{
.view_rect = .{
.from = 0,
.to = @floatFromInt(samples.len),
.min_value = min_value + (min_value - max_value) * margin,
.max_value = max_value + (max_value - min_value) * margin
.min_value = min_value - sample_range * margin,
.max_value = max_value + sample_range * margin
},
.samples = .{ .owned = samples }
.source = .{ .file = loaded_file_index }
});
errdefer _ = self.channel_views.pop();
}
fn showChannelsWindow(self: *App) void {
const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels"));
scroll_area.layout_axis = .Y;
defer self.ui.popScrollbar();
pub fn appendChannelFromDevice(self: *App, channel_name: []const u8) !void {
const device_channel_index = findFreeSlot(DeviceChannel, &self.device_channels) orelse return error.DeviceChannelLimitReached;
for (self.channels.slice()) |*_channel| {
const channel: *Channel = _channel;
const name_buff = try DeviceChannel.Name.fromSlice(channel_name);
const channel_name_z = name_buff.buffer[0..name_buff.len :0];
const channel_box = self.ui.newBoxFromPtr(channel);
channel_box.background = rl.Color.blue;
channel_box.layout_axis = .Y;
channel_box.size.x = UI.Size.percent(1, 0);
channel_box.size.y = UI.Size.childrenSum(1);
self.ui.pushParent(channel_box);
defer self.ui.popParent();
const device = NIDaq.getDeviceNameFromChannel(channel_name) orelse return error.InvalidChannelName;
const device_buff = try NIDaq.BoundedDeviceName.fromSlice(device);
const device_z = device_buff.buffer[0..device_buff.len :0];
const graph_box = self.ui.newBoxFromString("Graph");
graph_box.background = rl.Color.blue;
graph_box.layout_axis = .Y;
graph_box.size.x = UI.Size.percent(1, 0);
graph_box.size.y = UI.Size.pixels(256, 1);
Graph.drawCached(&channel.view_cache, graph_box.persistent.size, channel.view_rect, channel.samples.owned);
if (channel.view_cache.texture) |texture| {
graph_box.texture = texture.texture;
var min_value: f64 = 0;
var max_value: f64 = 1;
const voltage_ranges = try self.ni_daq.listDeviceAOVoltageRanges(device_z);
if (voltage_ranges.len > 0) {
min_value = voltage_ranges[0].low;
max_value = voltage_ranges[0].high;
}
{
const sample_count: f32 = @floatFromInt(channel.samples.owned.len);
const max_sample_rate = try self.ni_daq.getMaxSampleRate(channel_name_z);
self.device_channels[device_channel_index] = DeviceChannel{
.name = name_buff,
.min_sample_rate = self.ni_daq.getMinSampleRate(channel_name_z) catch max_sample_rate,
.max_sample_rate = max_sample_rate,
.min_value = min_value,
.max_value = max_value,
.samples = std.ArrayList(f64).init(self.allocator)
};
errdefer self.device_channels[device_channel_index] = null;
self.channel_views.appendAssumeCapacity(ChannelView{
.view_rect = .{
.from = 0,
.to = 0,
.min_value = min_value,
.max_value = max_value
},
.source = .{ .device = device_channel_index }
});
errdefer _ = self.channel_views.pop();
}
fn getChannelSource(self: *App, channel_view: *ChannelView) ?ChannelView.SourceObject {
switch (channel_view.source) {
.file => |index| {
if (self.loaded_files[index]) |*loaded_file| {
return ChannelView.SourceObject{
.file = loaded_file
};
}
},
.device => |index| {
if (self.device_channels[index]) |*device_channel| {
return ChannelView.SourceObject{
.device = device_channel
};
}
}
}
return null;
}
fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count: f32) void {
const min_visible_samples = 1; // sample_count*0.02;
const minimap_box = self.ui.newBoxFromString("Minimap");
@ -201,7 +347,6 @@ fn showChannelsWindow(self: *App) void {
const minimap_rect = minimap_box.computedRect();
const middle_box = self.ui.newBoxFromString("Middle knob");
{
middle_box.flags.insert(.clickable);
@ -233,63 +378,169 @@ fn showChannelsWindow(self: *App) void {
const left_signal = self.ui.signalFromBox(left_knob_box);
if (left_signal.dragged()) {
channel.view_rect.from += remap(
view_rect.from += remap(
f32,
0, minimap_rect.width,
0, sample_count,
left_signal.drag.x
);
channel.view_rect.from = clamp(channel.view_rect.from, 0, channel.view_rect.to-min_visible_samples);
view_rect.from = clamp(view_rect.from, 0, view_rect.to-min_visible_samples);
}
const right_signal = self.ui.signalFromBox(right_knob_box);
if (right_signal.dragged()) {
channel.view_rect.to += remap(
view_rect.to += remap(
f32,
0, minimap_rect.width,
0, sample_count,
right_signal.drag.x
);
channel.view_rect.to = clamp(channel.view_rect.to, channel.view_rect.from+min_visible_samples, sample_count);
view_rect.to = clamp(view_rect.to, view_rect.from + min_visible_samples, sample_count);
}
const middle_signal = self.ui.signalFromBox(middle_box);
if (middle_signal.dragged()) {
var samples_moved = middle_signal.drag.x / minimap_rect.width * sample_count;
samples_moved = clamp(samples_moved, -channel.view_rect.from, sample_count - channel.view_rect.to);
samples_moved = clamp(samples_moved, -view_rect.from, sample_count - view_rect.to);
channel.view_rect.from += samples_moved;
channel.view_rect.to += samples_moved;
view_rect.from += samples_moved;
view_rect.to += samples_moved;
}
left_knob_box.setFixedX(remap(f32,
0, sample_count,
0, minimap_rect.width - left_knob_size - right_knob_size,
channel.view_rect.from
view_rect.from
));
right_knob_box.setFixedX(remap(f32,
0, sample_count,
left_knob_size, minimap_rect.width - right_knob_size,
channel.view_rect.to
view_rect.to
));
middle_box.setFixedX(remap(f32,
0, sample_count,
left_knob_size, minimap_rect.width - right_knob_size,
channel.view_rect.from
view_rect.from
));
middle_box.setFixedWidth(remap(f32,
0, sample_count,
0, minimap_rect.width - right_knob_size - left_knob_size,
channel.view_rect.to - channel.view_rect.from
view_rect.to - view_rect.from
));
}
fn showChannelView(self: *App, channel_view: *ChannelView) !void {
const source = self.getChannelSource(channel_view) orelse return;
const samples = source.samples();
source.lockSamples();
defer source.unlockSamples();
const channel_box = self.ui.newBoxFromPtr(channel_view);
channel_box.background = rl.Color.blue;
channel_box.layout_axis = .Y;
channel_box.size.x = UI.Size.percent(1, 0);
channel_box.size.y = UI.Size.childrenSum(1);
self.ui.pushParent(channel_box);
defer self.ui.popParent();
{
const tools_box = self.ui.newBoxFromString("Graph tools");
tools_box.background = rl.Color.gray;
tools_box.layout_axis = .X;
tools_box.size.x = UI.Size.percent(1, 0);
tools_box.size.y = UI.Size.pixels(32, 1);
self.ui.pushParent(tools_box);
defer self.ui.popParent();
if (source == .device) {
const device_channel = source.device;
{
const record_button = self.ui.newBoxFromString("Record");
record_button.flags.insert(.clickable);
record_button.size.x = UI.Size.text(1, 0);
record_button.size.y = UI.Size.percent(1, 0);
if (device_channel.active_task == null) {
record_button.setText(.text, "Record");
} else {
record_button.setText(.text, "Stop");
}
const signal = self.ui.signalFromBox(record_button);
if (signal.clicked()) {
if (device_channel.active_task) |task| {
try task.stop();
device_channel.active_task = null;
} else {
const channel_name = device_channel.name.buffer[0..device_channel.name.len :0];
device_channel.active_task = try self.task_pool.launchAIVoltageChannel(
&device_channel.mutex,
&device_channel.samples,
.{
.continous = .{ .sample_rate = device_channel.max_sample_rate }
},
.{
.min_value = device_channel.min_value,
.max_value = device_channel.max_value,
.units = device_channel.units,
.channel = channel_name
}
);
channel_view.follow = true;
}
}
}
{
const follow_button = self.ui.newBoxFromString("Follow");
follow_button.flags.insert(.clickable);
follow_button.size.x = UI.Size.text(1, 0);
follow_button.size.y = UI.Size.percent(1, 0);
follow_button.setText(.text, if (channel_view.follow) "Unfollow" else "Follow");
const signal = self.ui.signalFromBox(follow_button);
if (signal.clicked()) {
channel_view.follow = !channel_view.follow;
}
}
}
}
{
const graph_box = self.ui.newBoxFromString("Graph");
graph_box.background = rl.Color.blue;
graph_box.size.x = UI.Size.percent(1, 0);
graph_box.size.y = UI.Size.pixels(channel_view.height, 1);
Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, channel_view.view_rect, samples);
if (channel_view.view_cache.texture) |texture| {
graph_box.texture = texture.texture;
}
}
self.showChannelViewSlider(
&channel_view.view_rect,
@floatFromInt(samples.len)
);
}
fn showChannelsWindow(self: *App) !void {
const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels"));
defer self.ui.popScrollbar();
scroll_area.layout_axis = .Y;
//scroll_area.layout_gap = 16;
for (self.channel_views.slice()) |*channel_view| {
try self.showChannelView(channel_view);
}
}
fn findChannelIndexByName(haystack: []const [:0]const u8, needle: [:0]const u8) ?usize {
@ -363,9 +614,12 @@ fn showAddFromDeviceWindow(self: *App) !void {
const signal = self.ui.signalFromBox(add_button);
if (signal.clicked()) {
const selected_devices = self.selected_channels.constSlice();
std.debug.print("{s}\n", .{selected_devices});
for (self.selected_channels.constSlice()) |channel| {
for (selected_devices) |channel| {
try self.appendChannelFromDevice(channel);
}
for (selected_devices) |channel| {
self.allocator.free(channel);
}
self.selected_channels.len = 0;
@ -432,42 +686,6 @@ fn showAddFromDeviceWindow(self: *App) !void {
}
}
}
// if (self.selected_device.len > 0) {
// const device: [:0]u8 = self.selected_device.buffer[0..self.selected_device.len :0];
// var ai_voltage_physical_channels: []const [:0]const u8 = &.{};
// if (try self.ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
// ai_voltage_physical_channels = try self.ni_daq.listDeviceAIPhysicalChannels(device);
// }
// var ao_physical_channels: []const [:0]const u8 = &.{};
// if (try self.ni_daq.checkDeviceAOOutputType(device, .Voltage)) {
// ao_physical_channels = try self.ni_daq.listDeviceAOPhysicalChannels(device);
// }
// const channels_box = self.ui.newBoxFromString("Channel names");
// channels_box.size.x = UI.Size.percent(0.5, 0);
// channels_box.size.y = UI.Size.percent(1, 0);
// channels_box.layout_axis = .Y;
// self.ui.pushParent(channels_box);
// defer self.ui.popParent();
// for (ai_voltage_physical_channels) |channel_name| {
// const channel_box = self.ui.newBoxFromString(channel_name);
// channel_box.flags.insert(.clickable);
// channel_box.size.x = UI.Size.text(1, 0);
// channel_box.size.y = UI.Size.text(1, 0);
// channel_box.setText(.text, channel_name);
// const signal = self.ui.signalFromBox(channel_box);
// if (signal.clicked()) {
// self.selected_device = try NIDaq.BoundedDeviceName.fromSlice(device);
// }
// }
// }
}
fn showToolbar(self: *App) void {
@ -498,7 +716,7 @@ fn showToolbar(self: *App) void {
defer file.close();
// TODO: Handle error
self.appendChannelFromFile(file) catch @panic("Failed to append channel from file");
// self.appendChannelFromFile(file) catch @panic("Failed to append channel from file");
} else |err| {
// TODO: Show error message to user;
log.err("Failed to pick file: {}", .{ err });
@ -528,6 +746,23 @@ fn showToolbar(self: *App) void {
}
pub fn tick(self: *App) !void {
for (self.channel_views.slice()) |*_view| {
const view: *ChannelView = _view;
const source = self.getChannelSource(view) orelse continue;
if (source == .device) {
if (view.follow) {
source.lockSamples();
defer source.unlockSamples();
const sample_count: f32 = @floatFromInt(source.samples().len);
const view_size = view.view_rect.to - view.view_rect.from;
view.view_rect.from = sample_count - view_size;
view.view_rect.to = sample_count;
}
}
}
rl.clearBackground(srcery.black);
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
@ -544,7 +779,7 @@ pub fn tick(self: *App) !void {
self.showToolbar();
if (self.shown_window == .channels) {
self.showChannelsWindow();
try self.showChannelsWindow();
} else if (self.shown_window == .add_from_device) {
try self.showAddFromDeviceWindow();
}

View File

@ -165,12 +165,14 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
const from_index = clampIndexUsize(@floor(options.from), samples.len);
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
if (to_index - from_index > 0) {
for (from_index..(to_index-1)) |i| {
const from_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
const to_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i + 1), samples[i + 1]);
rl.drawLineV(from_point, to_point, options.color);
}
}
}
{
const from_index = clampIndexUsize(@ceil(options.from), samples.len);

View File

@ -44,20 +44,22 @@ fn toZigLogLevel(log_type: c_int) ?log.Level {
fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_list) callconv(.C) void {
const log_level = toZigLogLevel(logType) orelse return;
// TODO: Skip formatting buffer, if logging is not enabled for that level.
const scope = .raylib;
const raylib_log = std.log.scoped(scope);
const max_tracelog_msg_length = 256; // from utils.c in raylib
var buffer: [max_tracelog_msg_length:0]u8 = undefined;
inline for (std.meta.fields(std.log.Level)) |field| {
const message_level: std.log.Level = @enumFromInt(field.value);
if (std.log.logEnabled(message_level, scope) and log_level == message_level) {
@memset(&buffer, 0);
const text_length = raylib_h.vsnprintf(&buffer, buffer.len, text, args);
const formatted_text = buffer[0..@intCast(text_length)];
const raylib_log = log.scoped(.raylib);
switch (log_level) {
.debug => raylib_log.debug("{s}", .{ formatted_text }),
.info => raylib_log.info("{s}", .{ formatted_text }),
.warn => raylib_log.warn("{s}", .{ formatted_text }),
.err => raylib_log.err("{s}", .{ formatted_text })
const log_function = @field(raylib_log, field.name);
@call(.auto, log_function, .{ "{s}", .{formatted_text} });
}
}
}
@ -146,21 +148,14 @@ pub fn main() !void {
try Assets.init(allocator);
defer Assets.deinit(allocator);
var app = try Application.init(allocator);
var app: Application = undefined;
try Application.init(&app, allocator);
defer app.deinit();
if (builtin.mode == .Debug) {
{
const sample_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin", .{});
defer sample_file.close();
try app.appendChannelFromFile(sample_file);
}
{
const sample_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin", .{});
defer sample_file.close();
try app.appendChannelFromFile(sample_file);
}
try app.appendChannelFromDevice("Dev1/ai0");
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
}
var profiler: ?Profiler = null;

View File

@ -11,7 +11,7 @@ const log = std.log.scoped(.ni_daq);
const max_device_name_size = 255;
const max_task_name_size = 255;
const max_channel_name_size = count: {
pub const max_channel_name_size = count: {
var count: u32 = 0;
count += max_device_name_size;
count += 1; // '/'
@ -44,11 +44,8 @@ pub const Task = struct {
dropped_samples: u32 = 0,
pub fn clear(self: Task) !void {
try checkDAQmxError(
c.DAQmxClearTask(self.handle),
error.DAQmxClearTask
);
pub fn clear(self: Task) void {
logDAQmxError(c.DAQmxClearTask(self.handle));
}
pub fn name(self: *Task) ![]const u8 {
@ -825,3 +822,9 @@ pub fn getDeviceProductCategory(self: NIDaq, device: [:0]const u8) !ProductCateg
return product_category;
}
pub fn getDeviceNameFromChannel(channel_name: []const u8) ?[]const u8 {
const slash = std.mem.indexOfScalar(u8, channel_name, '/') orelse return null;
return channel_name[0..slash];
}

View File

@ -1,351 +1,172 @@
const std = @import("std");
const NIDaq = @import("./ni-daq.zig");
const TaskPool = @This();
const assert = std.debug.assert;
const log = std.log.scoped(.task_pool);
const ChannelType = enum { analog_input, analog_output };
const TaskPool = @This();
const max_tasks = 32;
const Entry = struct {
device: NIDaq.BoundedDeviceName,
channel_type: ChannelType,
task: NIDaq.Task,
channel_order: std.ArrayListUnmanaged(usize)
};
channel_count: usize = 0,
max_channel_count: usize,
entries: std.ArrayListUnmanaged(Entry),
read_thread: ?std.Thread = null,
thread_running: bool = false,
sampling: ?union(enum) {
pub const Sampling = union(enum) {
finite: struct {
sample_rate: f64,
samples_per_channel: u64
sample_count: u64
},
continous: struct {
sample_rate: f64
}
} = null,
};
pub const ChannelSamples = struct {
allocator: std.mem.Allocator,
mutex: std.Thread.Mutex = .{},
read_arrays_by_task: [][]f64,
samples: []std.ArrayList(f64),
started_sampling_ns: ?i128 = null,
pub const Entry = struct {
task: NIDaq.Task,
in_use: bool = false,
running: bool = false,
started_sampling_ns: i128,
stopped_sampling_ns: ?i128 = null,
dropped_samples: u32 = 0,
pub fn deinit(self: *ChannelSamples) void {
for (self.read_arrays_by_task) |read_arrays| {
self.allocator.free(read_arrays);
sampling: Sampling,
mutex: *std.Thread.Mutex,
samples: *std.ArrayList(f64),
pub fn stop(self: *Entry) !void {
self.running = false;
if (self.in_use) {
try self.task.stop();
self.task.clear();
}
self.allocator.free(self.read_arrays_by_task);
for (self.samples) |samples_per_channel| {
samples_per_channel.deinit();
}
self.allocator.free(self.samples);
self.allocator.destroy(self);
self.in_use = false;
}
};
pub const Options = struct {
max_tasks: usize,
max_channels: usize
};
running: bool = false,
read_thread: std.Thread,
ni_daq: *NIDaq,
entries: [max_tasks]Entry = undefined,
pub fn init(allocator: std.mem.Allocator, options: Options) !TaskPool {
var entries = try std.ArrayListUnmanaged(Entry).initCapacity(allocator, options.max_tasks);
errdefer entries.deinit(allocator);
for (entries.allocatedSlice()) |*entry| {
// TODO: .deinit() on failure
entry.channel_order = try std.ArrayListUnmanaged(usize).initCapacity(allocator, options.max_channels);
}
return TaskPool{
.entries = entries,
.max_channel_count = options.max_channels
pub fn init(self: *TaskPool, allocator: std.mem.Allocator, ni_daq: *NIDaq) !void {
self.* = TaskPool{
.ni_daq = ni_daq,
.read_thread = undefined
};
}
pub fn deinit(self: *TaskPool, allocator: std.mem.Allocator) void {
if (self.read_thread != null) {
self.stop() catch @panic("Failed to stop task");
}
for (self.entries.items) |e| {
e.task.clear() catch @panic("Failed to clear task");
}
for (self.entries.allocatedSlice()) |*e| {
e.channel_order.deinit(allocator);
}
self.entries.deinit(allocator);
}
pub fn setContinousSampleRate(self: *TaskPool, sample_rate: f64) !void {
assert(self.read_thread == null);
for (self.entries.items) |e| {
try e.task.setContinousSampleRate(sample_rate);
}
self.sampling = .{
.continous = .{
.sample_rate = sample_rate
}
};
}
pub fn setFiniteSampleRate(self: *TaskPool, sample_rate: f64, samples_per_channel: u64) !void {
assert(self.read_thread == null);
for (self.entries.items) |e| {
try e.task.setFiniteSampleRate(sample_rate, samples_per_channel);
}
self.sampling = .{
.finite = .{
.sample_rate = sample_rate,
.samples_per_channel = samples_per_channel
}
};
}
pub fn start(self: *TaskPool, read_timeout: f64, allocator: std.mem.Allocator) !*ChannelSamples {
assert(self.read_thread == null);
var channel_samples = try self.createChannelSamples(allocator);
errdefer channel_samples.deinit();
for (self.entries.items) |e| {
try e.task.start();
}
self.thread_running = true;
var read_thread = try std.Thread.spawn(
self.running = true;
self.read_thread = try std.Thread.spawn(
.{ .allocator = allocator },
readThreadCallback,
.{ self, read_timeout, channel_samples }
.{ self }
);
errdefer read_thread.join();
self.read_thread = read_thread;
return channel_samples;
for (&self.entries) |*entry| {
entry.in_use = false;
}
}
pub fn stop(self: *TaskPool) !void {
assert(self.read_thread != null);
for (self.entries.items) |e| {
try e.task.stop();
pub fn deinit(self: *TaskPool) void {
for (&self.entries) |*entry| {
entry.stop() catch log.err("Failed to stop entry", .{});
}
self.thread_running = false;
self.read_thread.?.join();
self.read_thread = null;
self.running = false;
self.read_thread.join();
}
fn getDeviceFromChannel(channel: [:0]const u8) ?[]const u8 {
const slash = std.mem.indexOfScalar(u8, channel, '/') orelse return null;
return channel[0..slash];
}
fn readAnalog(entry: *Entry, timeout: f64) !void {
if (!entry.in_use) return;
if (!entry.running) return;
fn getOrPutTask(self: *TaskPool, ni_daq: NIDaq, device: []const u8, channel_type: ChannelType) !*Entry {
for (self.entries.items) |*entry| {
const entry_device = entry.device.slice();
if (entry.channel_type == channel_type and std.mem.eql(u8, entry_device, device)) {
return entry;
entry.mutex.lock();
defer entry.mutex.unlock();
switch (entry.sampling) {
.finite => |args| {
try entry.samples.ensureTotalCapacity(args.sample_count);
},
.continous => |args| {
try entry.samples.ensureUnusedCapacity(@intFromFloat(@ceil(args.sample_rate)));
}
}
if (self.entries.items.len == self.entries.capacity) {
return error.TaskLimitReached;
}
const unused_capacity = entry.samples.unusedCapacitySlice();
if (unused_capacity.len == 0) return;
const task = try ni_daq.createTask(null);
errdefer task.clear() catch {};
var entry = self.entries.addOneAssumeCapacity();
entry.channel_type = channel_type;
entry.device = try NIDaq.BoundedDeviceName.fromSlice(device);
entry.task = task;
return entry;
}
pub fn createAIVoltageChannel(self: *TaskPool, ni_daq: NIDaq, options: NIDaq.Task.AIVoltageChannelOptions) !void {
assert(self.read_thread == null);
const device = getDeviceFromChannel(options.channel) orelse return error.UnknownDevice;
var entry = try self.getOrPutTask(ni_daq, device, .analog_input);
if (entry.channel_order.items.len == entry.channel_order.capacity) {
return error.MaxChannelsLimitReached;
}
try entry.task.createAIVoltageChannel(options);
entry.channel_order.appendAssumeCapacity(self.channel_count);
self.channel_count += 1;
}
pub fn createAOVoltageChannel(self: *TaskPool, ni_daq: NIDaq, options: NIDaq.Task.AOVoltageChannelOptions) !void {
assert(self.read_thread == null);
const device = getDeviceFromChannel(options.channel) orelse return error.UnknownDevice;
var entry = try self.getOrPutTask(ni_daq, device, .analog_output);
if (entry.channel_order.items.len == entry.channel_order.capacity) {
return error.MaxChannelsLimitReached;
}
try entry.task.createAOVoltageChannel(options);
entry.channel_order.appendAssumeCapacity(self.channel_count);
self.channel_count += 1;
}
pub fn createChannelSamples(self: TaskPool, allocator: std.mem.Allocator) !*ChannelSamples {
assert(self.channel_count > 0);
assert(self.sampling != null);
var read_arrays_by_task = try allocator.alloc([]f64, self.entries.items.len);
errdefer allocator.free(read_arrays_by_task);
const sampling = self.sampling.?;
const array_size_per_channel: usize = switch (sampling) {
// TODO: For now reserve 1s worth of samples per channel, maybe this should be configurable?
// Maybe it should be proportional to timeout?
.continous => |args| @intFromFloat(@ceil(args.sample_rate)),
.finite => |args| args.samples_per_channel,
};
for (0.., self.entries.items) |i, entry| {
const channel_count = entry.channel_order.items.len;
// TODO: Add allocator.free on failure
read_arrays_by_task[i] = try allocator.alloc(f64, array_size_per_channel * channel_count);
}
const samples = try allocator.alloc(std.ArrayList(f64), self.channel_count);
errdefer allocator.free(samples);
if (sampling == .finite) {
for (samples) |*samples_per_channel| {
// TODO: Add .deinit() on failure
samples_per_channel.* = try std.ArrayList(f64).initCapacity(allocator, sampling.finite.samples_per_channel);
}
} else {
for (samples) |*samples_per_channel| {
// TODO: Maybe it would be good to reserve a large amount of space for samples?
// Even if it is continous. Maybe use ringbuffer?
// TODO: Add .deinit() on failure
samples_per_channel.* = std.ArrayList(f64).init(allocator);
}
}
const channel_samples = try allocator.create(ChannelSamples);
errdefer allocator.destroy(channel_samples);
channel_samples.* = ChannelSamples{
.allocator = allocator,
.read_arrays_by_task = read_arrays_by_task,
.samples = samples
};
return channel_samples;
}
pub fn readAnalog(self: *TaskPool, timeout: f64, samples: *ChannelSamples) !void {
assert(self.read_thread != null);
assert(self.channel_count > 0);
samples.mutex.lock();
defer samples.mutex.unlock();
for (0.., self.entries.items) |i, *entry| {
const read_array = samples.read_arrays_by_task[i];
const samples_per_channel = try entry.task.readAnalog(.{
.read_array = read_array,
.timeout = timeout
const read_amount = try entry.task.readAnalog(.{
.timeout = timeout,
.read_array = unused_capacity,
});
if (samples_per_channel == 0) continue;
if (read_amount == 0) return;
const channel_count = entry.channel_order.items.len;
const read_array_used = samples_per_channel * channel_count;
var channel_index_of_task: usize = 0;
var samples_window = std.mem.window(f64, read_array[0..read_array_used], samples_per_channel, samples_per_channel);
while (samples_window.next()) |channel_samples| : (channel_index_of_task += 1) {
const channel_index: usize = entry.channel_order.items[channel_index_of_task];
// TODO: Maybe use .appendSliceAssumeCapacity(), when doing finite sampling?
try samples.samples[channel_index].appendSlice(channel_samples);
}
}
entry.samples.items.len += read_amount;
}
pub fn isDone(self: TaskPool) !bool {
for (self.entries.items) |entry| {
const is_done = try entry.task.isDone();
if (!is_done) {
return false;
}
}
fn readThreadCallback(task_pool: *TaskPool) void {
const timeout = 0.05;
return true;
}
pub fn droppedSamples(self: TaskPool) u32 {
var sum: u32 = 0;
for (self.entries.items) |entry| {
sum += entry.task.dropped_samples;
}
return sum;
}
fn readThreadCallback(task_pool: *TaskPool, timeout: f64, channel_samples: *ChannelSamples) void {
defer task_pool.thread_running = false;
channel_samples.started_sampling_ns = std.time.nanoTimestamp();
defer channel_samples.stopped_sampling_ns = std.time.nanoTimestamp();
var error_count: u32 = 0;
const max_error_count = 3;
while (error_count < max_error_count and task_pool.thread_running) {
const is_done = task_pool.isDone() catch |e| {
error_count += 1;
log.err(".isDone() failed in thread: {}", .{e});
while (task_pool.running) {
for (&task_pool.entries) |*entry| {
readAnalog(entry, timeout) catch |e| {
log.err("readAnalog() failed in thread: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
continue;
};
if (is_done) {
break;
}
task_pool.readAnalog(timeout, channel_samples) catch |e| {
error_count += 1;
log.err(".readAnalog() failed in thread: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
continue;
entry.stop() catch log.err("failed to stop collecting", .{});
};
}
if (max_error_count == error_count) {
log.err("Stopping read thread, too many errors occured", .{});
std.time.sleep(0.05 * std.time.ns_per_s);
}
}
fn findFreeEntry(self: *TaskPool) ?*Entry {
for (&self.entries) |*entry| {
if (!entry.in_use) {
return entry;
}
}
return null;
}
pub fn launchAIVoltageChannel(
self: *TaskPool,
mutex: *std.Thread.Mutex,
samples: *std.ArrayList(f64),
sampling: Sampling,
options: NIDaq.Task.AIVoltageChannelOptions
) !*Entry {
const task = try self.ni_daq.createTask(null);
errdefer task.clear();
const entry = self.findFreeEntry() orelse return error.NotEnoughSpace;
errdefer entry.in_use = false;
try task.createAIVoltageChannel(options);
switch (sampling) {
.continous => |args| {
try task.setContinousSampleRate(args.sample_rate);
},
.finite => |args| {
try task.setFiniteSampleRate(args.sample_rate, args.sample_count);
}
}
samples.clearRetainingCapacity();
try task.start();
const started_at = std.time.nanoTimestamp();
entry.* = Entry{
.task = task,
.started_sampling_ns = started_at,
.in_use = true,
.running = true,
.mutex = mutex,
.samples = samples,
.sampling = sampling,
};
return entry;
}

View File

@ -254,6 +254,7 @@ pub const Box = struct {
background: ?rl.Color = null,
rounded: bool = false,
layout_axis: Axis = .X,
layout_gap: f32 = 0,
last_used_frame: u64 = 0,
text: ?struct {
content: []u8,
@ -806,6 +807,7 @@ fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void {
if (box.layout_axis == axis) {
child_axis_position.* += layout_position;
layout_position += child_axis_size.*;
layout_position += box.layout_gap;
}
}
@ -815,6 +817,7 @@ fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void {
if (box.layout_axis == axis) {
var child_size_sum: f32 = 0;
var child_count: f32 = 0;
var child_iter = self.iterChildrenByParent(box);
while (child_iter.next()) |child| {
@ -822,6 +825,11 @@ fn calcLayoutPositions(self: *UI, box: *Box, axis: Axis) void {
const child_size = getVec2Axis(&child.persistent.size, axis);
child_size_sum += child_size.*;
child_count += 1;
}
if (child_count > 1) {
child_size_sum += (child_count - 1) * box.layout_gap;
}
getVec2Axis(&box.persistent.children_size, axis).* = child_size_sum;