add button to start recording samples
This commit is contained in:
parent
65ad8d7786
commit
785355509e
613
src/app.zig
613
src/app.zig
@ -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,37 +107,57 @@ 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{
|
||||
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,
|
||||
.max_counter_outputs = 8,
|
||||
.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 = try NIDaq.init(allocator, .{
|
||||
.max_devices = 4,
|
||||
.max_analog_inputs = 32,
|
||||
.max_analog_outputs = 8,
|
||||
.max_counter_outputs = 8,
|
||||
.max_counter_inputs = 8,
|
||||
.max_analog_input_voltage_ranges = 4,
|
||||
.max_analog_output_voltage_ranges = 4
|
||||
})
|
||||
.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,161 +215,331 @@ 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);
|
||||
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);
|
||||
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];
|
||||
|
||||
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 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");
|
||||
minimap_box.background = rl.Color.dark_purple;
|
||||
minimap_box.layout_axis = .X;
|
||||
minimap_box.size.x = UI.Size.percent(1, 0);
|
||||
minimap_box.size.y = UI.Size.pixels(32, 1);
|
||||
self.ui.pushParent(minimap_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
const minimap_rect = minimap_box.computedRect();
|
||||
|
||||
const middle_box = self.ui.newBoxFromString("Middle knob");
|
||||
{
|
||||
middle_box.flags.insert(.clickable);
|
||||
middle_box.flags.insert(.draggable_x);
|
||||
middle_box.background = rl.Color.black.alpha(0.5);
|
||||
middle_box.size.y = UI.Size.pixels(32, 1);
|
||||
}
|
||||
|
||||
const left_knob_box = self.ui.newBoxFromString("Left knob");
|
||||
{
|
||||
left_knob_box.flags.insert(.clickable);
|
||||
left_knob_box.flags.insert(.draggable_x);
|
||||
left_knob_box.background = rl.Color.black.alpha(0.5);
|
||||
left_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||
left_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||
}
|
||||
|
||||
const right_knob_box = self.ui.newBoxFromString("Right knob");
|
||||
{
|
||||
right_knob_box.flags.insert(.clickable);
|
||||
right_knob_box.flags.insert(.draggable_x);
|
||||
right_knob_box.background = rl.Color.black.alpha(0.5);
|
||||
right_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||
right_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||
}
|
||||
|
||||
const left_knob_size = left_knob_box.persistent.size.x;
|
||||
const right_knob_size = right_knob_box.persistent.size.x;
|
||||
|
||||
const left_signal = self.ui.signalFromBox(left_knob_box);
|
||||
if (left_signal.dragged()) {
|
||||
view_rect.from += remap(
|
||||
f32,
|
||||
0, minimap_rect.width,
|
||||
0, sample_count,
|
||||
left_signal.drag.x
|
||||
);
|
||||
|
||||
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()) {
|
||||
view_rect.to += remap(
|
||||
f32,
|
||||
0, minimap_rect.width,
|
||||
0, sample_count,
|
||||
right_signal.drag.x
|
||||
);
|
||||
|
||||
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, -view_rect.from, sample_count - view_rect.to);
|
||||
|
||||
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,
|
||||
view_rect.from
|
||||
));
|
||||
|
||||
right_knob_box.setFixedX(remap(f32,
|
||||
0, sample_count,
|
||||
left_knob_size, minimap_rect.width - right_knob_size,
|
||||
view_rect.to
|
||||
));
|
||||
|
||||
middle_box.setFixedX(remap(f32,
|
||||
0, sample_count,
|
||||
left_knob_size, minimap_rect.width - right_knob_size,
|
||||
view_rect.from
|
||||
));
|
||||
middle_box.setFixedWidth(remap(f32,
|
||||
0, sample_count,
|
||||
0, minimap_rect.width - right_knob_size - left_knob_size,
|
||||
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.layout_axis = .Y;
|
||||
graph_box.size.x = UI.Size.percent(1, 0);
|
||||
graph_box.size.y = UI.Size.pixels(256, 1);
|
||||
graph_box.size.y = UI.Size.pixels(channel_view.height, 1);
|
||||
|
||||
Graph.drawCached(&channel.view_cache, graph_box.persistent.size, channel.view_rect, channel.samples.owned);
|
||||
if (channel.view_cache.texture) |texture| {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const sample_count: f32 = @floatFromInt(channel.samples.owned.len);
|
||||
const min_visible_samples = 1; // sample_count*0.02;
|
||||
self.showChannelViewSlider(
|
||||
&channel_view.view_rect,
|
||||
@floatFromInt(samples.len)
|
||||
);
|
||||
}
|
||||
|
||||
const minimap_box = self.ui.newBoxFromString("Minimap");
|
||||
minimap_box.background = rl.Color.dark_purple;
|
||||
minimap_box.layout_axis = .X;
|
||||
minimap_box.size.x = UI.Size.percent(1, 0);
|
||||
minimap_box.size.y = UI.Size.pixels(32, 1);
|
||||
self.ui.pushParent(minimap_box);
|
||||
defer self.ui.popParent();
|
||||
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;
|
||||
|
||||
const minimap_rect = minimap_box.computedRect();
|
||||
|
||||
|
||||
const middle_box = self.ui.newBoxFromString("Middle knob");
|
||||
{
|
||||
middle_box.flags.insert(.clickable);
|
||||
middle_box.flags.insert(.draggable_x);
|
||||
middle_box.background = rl.Color.black.alpha(0.5);
|
||||
middle_box.size.y = UI.Size.pixels(32, 1);
|
||||
}
|
||||
|
||||
const left_knob_box = self.ui.newBoxFromString("Left knob");
|
||||
{
|
||||
left_knob_box.flags.insert(.clickable);
|
||||
left_knob_box.flags.insert(.draggable_x);
|
||||
left_knob_box.background = rl.Color.black.alpha(0.5);
|
||||
left_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||
left_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||
}
|
||||
|
||||
const right_knob_box = self.ui.newBoxFromString("Right knob");
|
||||
{
|
||||
right_knob_box.flags.insert(.clickable);
|
||||
right_knob_box.flags.insert(.draggable_x);
|
||||
right_knob_box.background = rl.Color.black.alpha(0.5);
|
||||
right_knob_box.size.x = UI.Size.pixels(8, 1);
|
||||
right_knob_box.size.y = UI.Size.pixels(32, 1);
|
||||
}
|
||||
|
||||
const left_knob_size = left_knob_box.persistent.size.x;
|
||||
const right_knob_size = right_knob_box.persistent.size.x;
|
||||
|
||||
const left_signal = self.ui.signalFromBox(left_knob_box);
|
||||
if (left_signal.dragged()) {
|
||||
channel.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);
|
||||
}
|
||||
|
||||
const right_signal = self.ui.signalFromBox(right_knob_box);
|
||||
if (right_signal.dragged()) {
|
||||
channel.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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
channel.view_rect.from += samples_moved;
|
||||
channel.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
|
||||
));
|
||||
|
||||
right_knob_box.setFixedX(remap(f32,
|
||||
0, sample_count,
|
||||
left_knob_size, minimap_rect.width - right_knob_size,
|
||||
channel.view_rect.to
|
||||
));
|
||||
|
||||
middle_box.setFixedX(remap(f32,
|
||||
0, sample_count,
|
||||
left_knob_size, minimap_rect.width - right_knob_size,
|
||||
channel.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
|
||||
));
|
||||
|
||||
}
|
||||
for (self.channel_views.slice()) |*channel_view| {
|
||||
try self.showChannelView(channel_view);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
@ -165,10 +165,12 @@ 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);
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
39
src/main.zig
39
src/main.zig
@ -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;
|
||||
@memset(&buffer, 0);
|
||||
const text_length = raylib_h.vsnprintf(&buffer, buffer.len, text, args);
|
||||
|
||||
const formatted_text = buffer[0..@intCast(text_length)];
|
||||
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;
|
||||
|
@ -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 {
|
||||
@ -824,4 +821,10 @@ 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];
|
||||
}
|
@ -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;
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
const unused_capacity = entry.samples.unusedCapacitySlice();
|
||||
if (unused_capacity.len == 0) return;
|
||||
|
||||
const read_amount = try entry.task.readAnalog(.{
|
||||
.timeout = timeout,
|
||||
.read_array = unused_capacity,
|
||||
});
|
||||
|
||||
if (read_amount == 0) return;
|
||||
|
||||
entry.samples.items.len += read_amount;
|
||||
}
|
||||
|
||||
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)) {
|
||||
fn readThreadCallback(task_pool: *TaskPool) void {
|
||||
const timeout = 0.05;
|
||||
|
||||
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.*);
|
||||
}
|
||||
|
||||
entry.stop() catch log.err("failed to stop collecting", .{});
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (self.entries.items.len == self.entries.capacity) {
|
||||
return error.TaskLimitReached;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const task = try ni_daq.createTask(null);
|
||||
errdefer task.clear() catch {};
|
||||
samples.clearRetainingCapacity();
|
||||
|
||||
var entry = self.entries.addOneAssumeCapacity();
|
||||
entry.channel_type = channel_type;
|
||||
entry.device = try NIDaq.BoundedDeviceName.fromSlice(device);
|
||||
entry.task = task;
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
if (samples_per_channel == 0) continue;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isDone(self: TaskPool) !bool {
|
||||
for (self.entries.items) |entry| {
|
||||
const is_done = try entry.task.isDone();
|
||||
if (!is_done) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
if (max_error_count == error_count) {
|
||||
log.err("Stopping read thread, too many errors occured", .{});
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user