diff --git a/src/app.zig b/src/app.zig index 541ab60..c416d44 100644 --- a/src/app.zig +++ b/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(); } diff --git a/src/graph.zig b/src/graph.zig index 6d4c40a..9e73fbc 100644 --- a/src/graph.zig +++ b/src/graph.zig @@ -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); + } } } diff --git a/src/main.zig b/src/main.zig index 666cd20..98d3de7 100644 --- a/src/main.zig +++ b/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; diff --git a/src/ni-daq.zig b/src/ni-daq.zig index 217cbec..116cb0f 100644 --- a/src/ni-daq.zig +++ b/src/ni-daq.zig @@ -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]; } \ No newline at end of file diff --git a/src/task-pool.zig b/src/task-pool.zig index fbbc9cd..e648c5a 100644 --- a/src/task-pool.zig +++ b/src/task-pool.zig @@ -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", .{}); - } } \ No newline at end of file diff --git a/src/ui.zig b/src/ui.zig index e389189..f81e7c8 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -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;