add addition of channels from device screen

This commit is contained in:
Rokas Puzonas 2025-03-19 01:58:11 +02:00
parent 25377c8b4f
commit 8ba3d0c914
5 changed files with 275 additions and 80 deletions

View File

@ -12,6 +12,7 @@ const RangeF64 = @import("./range.zig").RangeF64;
const P = @import("profiler"); const P = @import("profiler");
const MainScreen = @import("./screens/main_screen.zig"); const MainScreen = @import("./screens/main_screen.zig");
const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig");
const log = std.log.scoped(.app); const log = std.log.scoped(.app);
const clamp = std.math.clamp; const clamp = std.math.clamp;
@ -107,28 +108,28 @@ channel_mutex: std.Thread.Mutex = .{},
// UI Fields // UI Fields
ui: UI, ui: UI,
screen: MainScreen, current_screen: enum {
graph_controls: struct { main_menu,
drag_start: ?struct { channel_from_device
index: f64, } = .main_menu,
value: f64 main_screen: MainScreen,
} = null, channel_from_device: ChannelFromDeviceScreen,
} = .{},
fullscreen_channel: ?*ChannelView = null,
pub fn init(self: *App, allocator: std.mem.Allocator) !void { pub fn init(self: *App, allocator: std.mem.Allocator) !void {
self.* = App{ self.* = App{
.allocator = allocator, .allocator = allocator,
.ui = UI.init(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, .task_pool = undefined,
.screen = undefined
}; };
errdefer self.deinit(); errdefer self.deinit();
self.screen = MainScreen{
.app = self
};
if (NIDaq.Api.init()) |ni_daq_api| { if (NIDaq.Api.init()) |ni_daq_api| {
self.ni_daq_api = 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 { pub fn deinit(self: *App) void {
self.task_pool.deinit();
for (self.channel_views.slice()) |*channel| { for (self.channel_views.slice()) |*channel| {
channel.view_cache.deinit(); channel.view_cache.deinit();
} }
@ -192,8 +195,8 @@ pub fn deinit(self: *App) void {
} }
self.ui.deinit(); 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_api) |*ni_daq_api| ni_daq_api.deinit();
if (self.ni_daq) |*ni_daq| ni_daq.deinit(self.allocator); if (self.ni_daq) |*ni_daq| ni_daq.deinit(self.allocator);
} }
@ -254,7 +257,10 @@ pub fn tick(self: *App) !void {
ui.begin(); ui.begin();
defer ui.end(); 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 -------------------------------- // // ------------------------ 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 { pub fn getChannelSamples(self: *App, channel_view: *ChannelView) []const f64 {
return switch (channel_view.source) { return switch (channel_view.source) {
.file => |index| { .file => |index| {

View File

@ -82,67 +82,11 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
defer _ = gpa.deinit(); 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"); const icon_png = @embedFile("./assets/icon.png");
var icon_image = rl.loadImageFromMemory(".png", icon_png); var icon_image = rl.loadImageFromMemory(".png", icon_png);
defer icon_image.unload(); defer icon_image.unload();
rl.initWindow(800, 450, "DAQ view"); rl.initWindow(800, 600, "DAQ view");
defer rl.closeWindow(); defer rl.closeWindow();
rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true }); rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true });
rl.setWindowMinSize(256, 256); rl.setWindowMinSize(256, 256);

View File

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

View File

@ -550,7 +550,7 @@ fn showChannelView(self: *MainScreen, channel_view: *ChannelView, height: UI.Siz
if (self.app.getChannelSourceDevice(channel_view)) |device_channel| { if (self.app.getChannelSourceDevice(channel_view)) |device_channel| {
_ = device_channel; _ = device_channel;
const follow = ui.button("Follow"); const follow = ui.textButton("Follow");
follow.background = srcery.hard_black; follow.background = srcery.hard_black;
if (channel_view.follow) { if (channel_view.follow) {
follow.borders = UI.Borders.bottom(.{ follow.borders = UI.Borders.bottom(.{
@ -659,7 +659,7 @@ pub fn tick(self: *MainScreen) !void {
toolbar.beginChildren(); toolbar.beginChildren();
defer toolbar.endChildren(); 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.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red });
start_all.background = srcery.black; start_all.background = srcery.black;
start_all.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 }); start_all.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 });
@ -725,9 +725,12 @@ pub fn tick(self: *MainScreen) !void {
add_channel_view.beginChildren(); add_channel_view.beginChildren();
defer add_channel_view.endChildren(); 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 }); add_from_file.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green });
if (ui.signal(add_from_file).clicked()) { 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| { if (Platform.openFilePicker(self.app.allocator)) |filename| {
defer self.app.allocator.free(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 }); add_from_device.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green });
if (ui.signal(add_from_device).clicked()) { if (ui.signal(add_from_device).clicked()) {
self.app.current_screen = .channel_from_device;
} }
} }
} }

View File

@ -1881,9 +1881,9 @@ pub fn mouseTooltip(self: *UI) *Box {
return tooltip; return tooltip;
} }
pub fn button(self: *UI, text: []const u8) *Box { pub fn button(self: *UI, key: UI.Key) *Box {
return self.createBox(.{ return self.createBox(.{
.key = self.keyFromString(text), .key = key,
.size_x = Sizing.initFixed(.text), .size_x = Sizing.initFixed(.text),
.size_y = Sizing.initFixed(.text), .size_y = Sizing.initFixed(.text),
.flags = &.{ .draw_hot, .draw_active, .clickable }, .flags = &.{ .draw_hot, .draw_active, .clickable },
@ -1894,10 +1894,17 @@ pub fn button(self: *UI, text: []const u8) *Box {
.right = self.rem(1) .right = self.rem(1)
}, },
.hot_cursor = .mouse_cursor_pointing_hand, .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 { pub fn label(self: *UI, comptime fmt: []const u8, args: anytype) *Box {
const box = self.createBox(.{ const box = self.createBox(.{
.size_x = Sizing.initFixed(.text), .size_x = Sizing.initFixed(.text),