#include #include #include #include #include #include #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include #include #include #include "tinyfiledialogs.h" void generateRandomColors(unsigned char& r, unsigned char& g, unsigned char& b); void sendDMXData(HANDLE hSerial, unsigned char* data, int length); void setupImGui(GLFWwindow* window); void renderImGui(); // ui timeline stuff std::vector> markers; // Time, R, G, B float timeline_position = 0.0f; bool is_playing = false; float playback_start_time = 0.0f; float current_time = 0.0f; float timeline_duration = 10.0f; // 10 seconds by default float bpm = 120.0f; // Default BPM float zoom_level = 1.0f; // Zoom level float pan_offset = 0.0f; // Pan offset const int num_channels = 512; unsigned char buffer[num_channels] = { 0 }; GLFWwindow* initOpenGL(); HANDLE hSerial; std::atomic running(true); void dmxThreadFunc() { while (running) { // 25ms refresh rate sendDMXData(hSerial, buffer, num_channels); std::this_thread::sleep_for(std::chrono::milliseconds(25)); } } int main() { hSerial = CreateFileA("\\\\.\\COM3", GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if (hSerial == INVALID_HANDLE_VALUE) { std::cerr << "Error opening COM3 port" << std::endl; return 1; } DCB dcbSerialParams = { 0 }; dcbSerialParams.DCBlength = sizeof(dcbSerialParams); if (!GetCommState(hSerial, &dcbSerialParams)) { std::cerr << "Error getting COM3 state" << std::endl; CloseHandle(hSerial); return 1; } dcbSerialParams.BaudRate = 250000; dcbSerialParams.ByteSize = 8; dcbSerialParams.StopBits = TWOSTOPBITS; // <-- don't forget about this dcbSerialParams.Parity = NOPARITY; if (!SetCommState(hSerial, &dcbSerialParams)) { std::cerr << "Error setting COM3 state" << std::endl; CloseHandle(hSerial); return 1; } COMMTIMEOUTS timeouts = { 0 }; timeouts.ReadIntervalTimeout = 50; timeouts.ReadTotalTimeoutConstant = 50; timeouts.ReadTotalTimeoutMultiplier = 10; timeouts.WriteTotalTimeoutConstant = 50; timeouts.WriteTotalTimeoutMultiplier = 10; if (!SetCommTimeouts(hSerial, &timeouts)) { std::cerr << "Error setting COM3 timeouts" << std::endl; CloseHandle(hSerial); return 1; } GLFWwindow* window = initOpenGL(); if (!window) { CloseHandle(hSerial); return 1; } setupImGui(window); std::thread dmxThread(dmxThreadFunc); while (!glfwWindowShouldClose(window)) { glfwPollEvents(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); renderImGui(); ImGui::Render(); int display_w, display_h; glfwGetFramebufferSize(window, &display_w, &display_h); glViewport(0, 0, display_w, display_h); glClearColor(0.45f, 0.55f, 0.60f, 1.00f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); } running = false; dmxThread.join(); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); CloseHandle(hSerial); return 0; } void generateRandomColors(unsigned char& r, unsigned char& g, unsigned char& b) { r = rand() % 256; g = rand() % 256; b = rand() % 256; } void sendDMXData(HANDLE hSerial, unsigned char* data, int length) { // break condition EscapeCommFunction(hSerial, SETBREAK); std::this_thread::sleep_for(std::chrono::milliseconds(1)); EscapeCommFunction(hSerial, CLRBREAK); std::this_thread::sleep_for(std::chrono::milliseconds(1)); // this is the mab DWORD bytesWritten; unsigned char startCode = 0; WriteFile(hSerial, &startCode, 1, &bytesWritten, NULL); WriteFile(hSerial, data, length, &bytesWritten, NULL); } // Function to export markers to a JSON file void exportMarkersToFile(const std::string& filename) { nlohmann::json j; for (const auto& marker : markers) { j.push_back({ {"time", std::get<0>(marker)}, {"r", std::get<1>(marker)}, {"g", std::get<2>(marker)}, {"b", std::get<3>(marker)} }); } std::ofstream file(filename); if (file.is_open()) { file << j.dump(4); // Pretty print with indentation of 4 spaces file.close(); } else { std::cerr << "Could not open file for writing: " << filename << std::endl; } } // Function to load markers from a JSON file void loadMarkersFromFile(const std::string& filename) { nlohmann::json j; std::ifstream file(filename); if (file.is_open()) { file >> j; file.close(); markers.clear(); for (const auto& element : j) { markers.emplace_back( element["time"].get(), element["r"].get(), element["g"].get(), element["b"].get() ); } } else { std::cerr << "Could not open file for reading: " << filename << std::endl; } } GLFWwindow* initOpenGL() { if (!glfwInit()) { std::cerr << "Failed to initialize GLFW" << std::endl; return nullptr; } GLFWwindow* window = glfwCreateWindow(1280, 720, "DMX Controller", NULL, NULL); if (!window) { std::cerr << "Failed to create GLFW window" << std::endl; glfwTerminate(); return nullptr; } glfwMakeContextCurrent(window); glfwSwapInterval(1); // I think this is vsync?? if (glewInit() != GLEW_OK) { std::cerr << "Failed to initialize GLEW" << std::endl; glfwDestroyWindow(window); glfwTerminate(); return nullptr; } return window; } void setupImGui(GLFWwindow* window) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 130"); } void renderImGui() { // Calculate time per beat based on BPM float beat_interval = 60.0f / bpm; // Temporary variables to hold the slider values int channel1 = buffer[0]; int channel2 = buffer[1]; int channel3 = buffer[2]; // Create a window ImGui::Begin("DMX Controller"); // Create buttons to export and load markers if (ImGui::Button("Export Lightshow")) { const char* filterPatterns[2] = { "*.ls", "*.json" }; const char* saveFileName = tinyfd_saveFileDialog( "Save As", // Dialog title "lightshow.ls", // Default file name 2, // Number of filter patterns filterPatterns, // Filter patterns NULL // Single filter description (optional) ); if (saveFileName) { std::cout << "File saved as: " << saveFileName << std::endl; exportMarkersToFile(saveFileName); } else { std::cout << "Save operation was canceled." << std::endl; } } ImGui::SameLine(); if (ImGui::Button("Load Lightshow")) { const char* filterPatterns[2] = { "*.ls", "*.json" }; std::string filename = tinyfd_openFileDialog("Select a file", "", 2, filterPatterns, NULL, 0); if (!filename.empty()) { loadMarkersFromFile(filename); } } // Create sliders for the first three DMX channels ImGui::SliderInt("Channel 1", &channel1, 0, 255); ImGui::SliderInt("Channel 2", &channel2, 0, 255); ImGui::SliderInt("Channel 3", &channel3, 0, 255); // Assign the slider values back to the DMX buffer buffer[0] = static_cast(channel1); buffer[1] = static_cast(channel2); buffer[2] = static_cast(channel3); // Allow user to set the total duration of the timeline and BPM ImGui::SliderFloat("Timeline Duration (seconds)", &timeline_duration, 1.0f, 300.0f); ImGui::SliderFloat("BPM", &bpm, 30.0f, 240.0f); // Play/Pause button if (ImGui::Button(is_playing ? "Pause" : "Play")) { is_playing = !is_playing; if (is_playing) { playback_start_time = static_cast(glfwGetTime()) - timeline_position; } } // Time Slider (Timeline Position) ImGui::SliderFloat("Timeline Position", &timeline_position, 0.0f, timeline_duration, "%.2f sec"); // Custom timeline rendering with grid lines ImGui::Text("Timeline"); ImGui::PushID("CustomTimeline"); ImGui::BeginChild("##Timeline", ImVec2(ImGui::GetContentRegionAvail().x, 100), true); // Draw grid lines for beats ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); float timeline_width = ImGui::GetContentRegionAvail().x; for (int i = 0; i <= static_cast(timeline_duration / beat_interval); ++i) { float beat_position = i * beat_interval * zoom_level - pan_offset; float x_position = cursor_pos.x + (beat_position / timeline_duration) * timeline_width; if (x_position >= cursor_pos.x && x_position <= cursor_pos.x + timeline_width) { // Draw major beat line if (i % 4 == 0) { draw_list->AddLine(ImVec2(x_position, cursor_pos.y), ImVec2(x_position, cursor_pos.y + 100), IM_COL32(255, 0, 0, 255), 2.0f); } else { // Draw minor beat line draw_list->AddLine(ImVec2(x_position, cursor_pos.y), ImVec2(x_position, cursor_pos.y + 100), IM_COL32(255, 255, 255, 255), 1.0f); } } } // Draw the current timeline position marker float position_marker = (timeline_position * zoom_level - pan_offset) / timeline_duration * timeline_width; draw_list->AddLine(ImVec2(cursor_pos.x + position_marker, cursor_pos.y), ImVec2(cursor_pos.x + position_marker, cursor_pos.y + 100), IM_COL32(0, 0, 255, 255), 2.0f); // Allow adding markers with snapping to grid if (ImGui::Button("Add Marker")) { float snap_position = std::round(timeline_position / beat_interval) * beat_interval; markers.emplace_back(snap_position / timeline_duration, buffer[0], buffer[1], buffer[2]); } // Draw markers on the timeline for (const auto& marker : markers) { float marker_position = std::get<0>(marker) * timeline_duration * zoom_level - pan_offset; float x_position = cursor_pos.x + (marker_position / timeline_duration) * timeline_width; if (x_position >= cursor_pos.x && x_position <= cursor_pos.x + timeline_width) { draw_list->AddRectFilled(ImVec2(x_position - 2.0f, cursor_pos.y), ImVec2(x_position + 2.0f, cursor_pos.y + 100), IM_COL32(0, 255, 0, 255)); } } ImGui::EndChild(); ImGui::PopID(); // Playback logic: Update timeline position based on playback time if (is_playing) { current_time = static_cast(glfwGetTime()) - playback_start_time; timeline_position = current_time; // Trigger markers during playback for (const auto& marker : markers) { float marker_time = std::get<0>(marker) * timeline_duration; if (current_time >= marker_time && current_time < marker_time + 0.025f) { // Slight tolerance for timing precision buffer[0] = std::get<1>(marker); buffer[1] = std::get<2>(marker); buffer[2] = std::get<3>(marker); } } // Stop playback when the timeline reaches the end if (timeline_position >= timeline_duration) { is_playing = false; timeline_position = timeline_duration; } } // Display added markers with their RGB values and time for (const auto& marker : markers) { float marker_time = std::get<0>(marker) * timeline_duration; ImGui::Text("Marker at %.2f sec: R=%d, G=%d, B=%d", marker_time, std::get<1>(marker), std::get<2>(marker), std::get<3>(marker)); } // Place Zoom and Pan sliders below the timeline to avoid overlap ImGui::SliderFloat("Zoom", &zoom_level, 0.1f, 10.0f); ImGui::SliderFloat("Pan", &pan_offset, 0.0f, timeline_duration * zoom_level); ImGui::End(); }