From 8ba3d0c9149d0e49276325d8975db484b0f2c381 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Wed, 19 Mar 2025 01:58:11 +0200 Subject: [PATCH] add addition of channels from device screen --- src/app.zig | 46 ++++-- src/main.zig | 58 +------ src/screens/channel_from_device.zig | 225 ++++++++++++++++++++++++++++ src/screens/main_screen.zig | 13 +- src/ui.zig | 13 +- 5 files changed, 275 insertions(+), 80 deletions(-) create mode 100644 src/screens/channel_from_device.zig diff --git a/src/app.zig b/src/app.zig index f321a90..a85c74e 100644 --- a/src/app.zig +++ b/src/app.zig @@ -12,6 +12,7 @@ const RangeF64 = @import("./range.zig").RangeF64; const P = @import("profiler"); const MainScreen = @import("./screens/main_screen.zig"); +const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig"); const log = std.log.scoped(.app); const clamp = std.math.clamp; @@ -107,28 +108,28 @@ channel_mutex: std.Thread.Mutex = .{}, // UI Fields ui: UI, -screen: MainScreen, -graph_controls: struct { - drag_start: ?struct { - index: f64, - value: f64 - } = null, -} = .{}, -fullscreen_channel: ?*ChannelView = null, +current_screen: enum { + main_menu, + channel_from_device +} = .main_menu, +main_screen: MainScreen, +channel_from_device: ChannelFromDeviceScreen, pub fn init(self: *App, allocator: std.mem.Allocator) !void { self.* = App{ .allocator = allocator, .ui = UI.init(allocator), + .main_screen = MainScreen{ + .app = self + }, + .channel_from_device = ChannelFromDeviceScreen{ + .app = self, + .channel_names = std.heap.ArenaAllocator.init(allocator) + }, .task_pool = undefined, - .screen = undefined }; errdefer self.deinit(); - self.screen = MainScreen{ - .app = self - }; - if (NIDaq.Api.init()) |ni_daq_api| { self.ni_daq_api = ni_daq_api; @@ -173,6 +174,8 @@ pub fn init(self: *App, allocator: std.mem.Allocator) !void { } pub fn deinit(self: *App) void { + self.task_pool.deinit(); + for (self.channel_views.slice()) |*channel| { channel.view_cache.deinit(); } @@ -192,8 +195,8 @@ pub fn deinit(self: *App) void { } self.ui.deinit(); + self.channel_from_device.deinit(); - self.task_pool.deinit(); if (self.ni_daq_api) |*ni_daq_api| ni_daq_api.deinit(); if (self.ni_daq) |*ni_daq| ni_daq.deinit(self.allocator); } @@ -254,7 +257,10 @@ pub fn tick(self: *App) !void { ui.begin(); defer ui.end(); - try self.screen.tick(); + switch (self.current_screen) { + .main_menu => try self.main_screen.tick(), + .channel_from_device => try self.channel_from_device.tick() + } } } @@ -263,6 +269,16 @@ pub fn tick(self: *App) !void { // ------------------------ Channel management -------------------------------- // +pub fn getChannelDeviceByName(self: *App, name: []const u8) ?*DeviceChannel { + for (&self.device_channels) |*maybe_channel| { + var channel: *DeviceChannel = &(maybe_channel.* orelse continue); + if (std.mem.eql(u8, channel.name.slice(), name)) { + return channel; + } + } + return null; +} + pub fn getChannelSamples(self: *App, channel_view: *ChannelView) []const f64 { return switch (channel_view.source) { .file => |index| { diff --git a/src/main.zig b/src/main.zig index 1e332e4..8843f71 100644 --- a/src/main.zig +++ b/src/main.zig @@ -82,67 +82,11 @@ pub fn main() !void { const allocator = gpa.allocator(); defer _ = gpa.deinit(); - // const devices = try ni_daq.listDeviceNames(); - - // for (devices) |device| { - // if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) { - // const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device); - // assert(voltage_ranges.len > 0); - - // const min_sample = voltage_ranges[0].low; - // const max_sample = voltage_ranges[0].high; - - // for (try ni_daq.listDeviceAIPhysicalChannels(device)) |channel_name| { - // var channel = try app.appendChannel(); - // channel.min_sample = min_sample; - // channel.max_sample = max_sample; - // try app.task_pool.createAIVoltageChannel(ni_daq, .{ - // .channel = channel_name, - // .min_value = min_sample, - // .max_value = max_sample, - // }); - // break; - // } - // } - - // if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) { - // const voltage_ranges = try ni_daq.listDeviceAOVoltageRanges(device); - // assert(voltage_ranges.len > 0); - - // const min_sample = voltage_ranges[0].low; - // const max_sample = voltage_ranges[0].high; - - // for (try ni_daq.listDeviceAOPhysicalChannels(device)) |channel_name| { - // var channel = try app.appendChannel(); - // channel.min_sample = min_sample; - // channel.max_sample = max_sample; - // try app.task_pool.createAOVoltageChannel(ni_daq, .{ - // .channel = channel_name, - // .min_value = min_sample, - // .max_value = max_sample, - // }); - // } - // } - // } - - // for (0.., app.channels.items) |i, *channel| { - // channel.color = rl.Color.fromHSV(@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(app.channels.items.len)) * 360, 0.75, 0.8); - // } - - // const sample_rate: f64 = 5000; - // try app.task_pool.setContinousSampleRate(sample_rate); - - // var channel_samples = try app.task_pool.start(0.01, allocator); - // defer channel_samples.deinit(); - // defer app.task_pool.stop() catch @panic("stop task failed"); - - // app.channel_samples = channel_samples; - const icon_png = @embedFile("./assets/icon.png"); var icon_image = rl.loadImageFromMemory(".png", icon_png); defer icon_image.unload(); - rl.initWindow(800, 450, "DAQ view"); + rl.initWindow(800, 600, "DAQ view"); defer rl.closeWindow(); rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true }); rl.setWindowMinSize(256, 256); diff --git a/src/screens/channel_from_device.zig b/src/screens/channel_from_device.zig new file mode 100644 index 0000000..41f2d67 --- /dev/null +++ b/src/screens/channel_from_device.zig @@ -0,0 +1,225 @@ +const std = @import("std"); +const App = @import("../app.zig"); +const UI = @import("../ui.zig"); +const srcery = @import("../srcery.zig"); +const NIDaq = @import("../ni-daq/root.zig"); + +const assert = std.debug.assert; +const log = std.log.scoped(.channel_from_device_screen); + +const Screen = @This(); + +app: *App, + +hot_channel: ?[:0]const u8 = null, + +// TODO: 32 limit +selected_channels: std.BoundedArray([:0]u8, 32) = .{}, +// TODO: Don't use arena +channel_names: std.heap.ArenaAllocator, + +pub fn deinit(self: *Screen) void { + _ = self.channel_names.reset(.free_all); +} + +fn isChannelSelected(self: *Screen, channel: []const u8) bool { + for (self.selected_channels.slice()) |selected_channel| { + if (std.mem.eql(u8, selected_channel, channel)) { + return true; + } + } + return false; +} + +fn selectChannel(self: *Screen, channel: []const u8) void { + if (self.selected_channels.unusedCapacitySlice().len == 0) { + log.warn("Maximum number of selected channels reached", .{}); + return; + } + + if (self.isChannelSelected(channel)) { + return; + } + + const allocator = self.channel_names.allocator(); + const channel_dupe = allocator.dupeZ(u8, channel) catch |e| { + log.err("Failed to duplicate channel name: {}", .{e}); + return; + }; + + self.selected_channels.appendAssumeCapacity(channel_dupe); +} + +fn deselectChannel(self: *Screen, channel: []const u8) void { + for (0.., self.selected_channels.slice()) |i, selected_channel| { + if (std.mem.eql(u8, selected_channel, channel)) { + _ = self.selected_channels.swapRemove(i); + return; + } + } +} + +fn toggleChannel(self: *Screen, channel: []const u8) void { + if (self.isChannelSelected(channel)) { + self.deselectChannel(channel); + } else { + self.selectChannel(channel); + } +} + +pub fn tick(self: *Screen) !void { + var ni_daq = self.app.ni_daq orelse return; + var ui = &self.app.ui; + + if (ui.isKeyboardPressed(.key_escape)) { + self.app.current_screen = .main_menu; + } + + const root = ui.parentBox().?; + root.layout_direction = .left_to_right; + + { + const panel = ui.beginScrollbar(ui.keyFromString("Channels")); + defer ui.endScrollbar(); + panel.layout_direction = .top_to_bottom; + + const devices = try ni_daq.listDeviceNames(); + for (devices) |device| { + var ai_voltage_physical_channels: []const [:0]const u8 = &.{}; + if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) { + ai_voltage_physical_channels = try ni_daq.listDeviceAIPhysicalChannels(device); + } + + var ao_physical_channels: []const [:0]const u8 = &.{}; + if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) { + ao_physical_channels = try ni_daq.listDeviceAOPhysicalChannels(device); + } + + inline for (.{ ai_voltage_physical_channels, ao_physical_channels }) |channels| { + for (channels) |channel| { + const channel_button = ui.textButton(channel); + channel_button.background = srcery.black; + if (self.isChannelSelected(channel)) { + channel_button.background = srcery.bright_white; + channel_button.text_color = srcery.black; + } + + if (self.app.getChannelDeviceByName(channel) != null) { + channel_button.text_color = srcery.white; + channel_button.background = srcery.hard_black; + } else { + const signal = ui.signal(channel_button); + if (signal.clicked()) { + self.toggleChannel(channel); + } + + if (signal.hot) { + self.hot_channel = channel; + } + } + + } + } + } + } + + { + const panel = ui.createBox(.{ + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull(), + .borders = .{ + .left = .{ .color = srcery.hard_black, .size = 4 } + }, + .layout_direction = .top_to_bottom, + .padding = UI.Padding.all(ui.rem(2)) + }); + panel.beginChildren(); + defer panel.endChildren(); + + const info_container = ui.createBox(.{ + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initGrowFull(), + .layout_direction = .top_to_bottom, + }); + + if (self.hot_channel) |hot_channel| { + info_container.beginChildren(); + defer info_container.endChildren(); + + var maybe_hot_device: ?[:0]const u8 = null; + var device_buff: NIDaq.BoundedDeviceName = .{}; + if (NIDaq.getDeviceNameFromChannel(hot_channel)) |device| { + device_buff.appendSliceAssumeCapacity(device); + device_buff.buffer[device_buff.len] = 0; + maybe_hot_device = device_buff.buffer[0..device_buff.len :0]; + } + + var channel_type_name: []const u8 = "unknown"; + if (NIDaq.getChannelType(hot_channel)) |channel_type| { + channel_type_name = channel_type.name(); + } + + { + const channel_info = ui.createBox(.{ + .size_x = UI.Sizing.initGrowFull(), + .size_y = UI.Sizing.initFitChildren(), + .padding = .{ + .bottom = ui.rem(2) + }, + .layout_direction = .top_to_bottom + }); + channel_info.beginChildren(); + defer channel_info.endChildren(); + + _ = ui.label("Channel properties", .{}); + _ = ui.label("Name: {s}", .{hot_channel}); + _ = ui.label("Type: {s}", .{channel_type_name}); + } + + if (maybe_hot_device) |hot_device| { + _ = ui.label("Device properties", .{}); + + if (ni_daq.listDeviceAIMeasurementTypes(hot_device)) |measurement_types| { + _ = ui.label("Measurement types: {} types", .{measurement_types.len}); + } else |e| { + log.err("ni_daq.listDeviceAIMeasurementTypes(): {}", .{ e }); + } + } + } else { + info_container.alignment.x = .center; + info_container.alignment.y = .center; + info_container.setText("Hover on a channel"); + info_container.flags.insert(.wrap_text); + info_container.text_color = srcery.hard_black; + info_container.font = .{ + .variant = .bold_italic, + .size = ui.rem(3) + }; + } + + const add_button = ui.button(ui.keyFromString("Add channels")); + add_button.setFmtText("Add {} selected channels", .{self.selected_channels.len}); + add_button.size.x = UI.Sizing.initGrowFull(); + add_button.alignment.x = .center; + if (self.selected_channels.len > 0) { + add_button.borders = UI.Borders.all(.{ + .color = srcery.green, + .size = 2 + }); + + const signal = ui.signal(add_button); + if (signal.clicked()) { + self.app.current_screen = .main_menu; + for (self.selected_channels.slice()) |channel| { + try self.app.appendChannelFromDevice(channel); + } + } + + } else { + add_button.borders = UI.Borders.all(.{ + .color = srcery.hard_black, + .size = 2 + }); + } + } +} \ No newline at end of file diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index f1550a3..14cf901 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -550,7 +550,7 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz if (self.app.getChannelSourceDevice(channel_view)) |device_channel| { _ = device_channel; - const follow = ui.button("Follow"); + const follow = ui.textButton("Follow"); follow.background = srcery.hard_black; if (channel_view.follow) { follow.borders = UI.Borders.bottom(.{ @@ -659,7 +659,7 @@ pub fn tick(self: *MainScreen) !void { toolbar.beginChildren(); defer toolbar.endChildren(); - var start_all = ui.button("Start/Stop button"); + var start_all = ui.textButton("Start/Stop button"); start_all.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red }); start_all.background = srcery.black; start_all.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); @@ -725,9 +725,12 @@ pub fn tick(self: *MainScreen) !void { add_channel_view.beginChildren(); defer add_channel_view.endChildren(); - const add_from_file = ui.button("Add from file"); + const add_from_file = ui.textButton("Add from file"); add_from_file.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); if (ui.signal(add_from_file).clicked()) { + self.app.channel_mutex.unlock(); + defer self.app.channel_mutex.lock(); + if (Platform.openFilePicker(self.app.allocator)) |filename| { defer self.app.allocator.free(filename); @@ -739,10 +742,10 @@ pub fn tick(self: *MainScreen) !void { } } - const add_from_device = ui.button("Add from device"); + const add_from_device = ui.textButton("Add from device"); add_from_device.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green }); if (ui.signal(add_from_device).clicked()) { - + self.app.current_screen = .channel_from_device; } } } diff --git a/src/ui.zig b/src/ui.zig index 3a1b8bb..ec2fe35 100644 --- a/src/ui.zig +++ b/src/ui.zig @@ -1881,9 +1881,9 @@ pub fn mouseTooltip(self: *UI) *Box { return tooltip; } -pub fn button(self: *UI, text: []const u8) *Box { +pub fn button(self: *UI, key: UI.Key) *Box { return self.createBox(.{ - .key = self.keyFromString(text), + .key = key, .size_x = Sizing.initFixed(.text), .size_y = Sizing.initFixed(.text), .flags = &.{ .draw_hot, .draw_active, .clickable }, @@ -1894,10 +1894,17 @@ pub fn button(self: *UI, text: []const u8) *Box { .right = self.rem(1) }, .hot_cursor = .mouse_cursor_pointing_hand, - .text = text }); } +pub fn textButton(self: *UI, text: []const u8) *Box { + var box = self.button(self.keyFromString(text)); + box.setText(text); + box.alignment.x = .center; + box.alignment.y = .center; + return box; +} + pub fn label(self: *UI, comptime fmt: []const u8, args: anytype) *Box { const box = self.createBox(.{ .size_x = Sizing.initFixed(.text),