diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 672efb6..b5dfc14 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -7,6 +7,32 @@ find_package(Qt6 REQUIRED COMPONENTS Widgets) set(CMAKE_AUTOMOC ON) +# QuarkWidgets — org-level Qt control library. The repo now holds two +# independent sub-libraries: the MD3 widget library at the root (SHARED + aex, +# for CFDesktop) and the embedded-debug controls under embedded/ (STATIC + +# QTest, zero deps — these). micro-forge consumes ONLY the embedded/ controls +# (QuarkBulb / LedPanel / UartTerminalView); pointing at the subdir avoids +# pulling in the root's aex dependency. Dev: MCUUTILS_DIR → ~/MCUUtils; +# release/CI: third_party/QuarkWidgets submodule. Wiring lives in gui/ (not the +# root CMakeLists) so the parallel-worktree collision hotspot is untouched. +set(MCUUTILS_DIR "$ENV{MCUUTILS_DIR}" CACHE PATH + "MCUUtils monorepo root (holds QuarkWidgets/); needed for MICRO_FORGE_GUI=ON") +set(quarkwidgets_dir "${MCUUTILS_DIR}/QuarkWidgets") +if(NOT EXISTS "${quarkwidgets_dir}/embedded/CMakeLists.txt") + # Release/CI fallback: in-repo submodule (init via git submodule update). + set(quarkwidgets_dir "${CMAKE_SOURCE_DIR}/third_party/QuarkWidgets") +endif() +if(NOT EXISTS "${quarkwidgets_dir}/embedded/CMakeLists.txt") + message(FATAL_ERROR + "QuarkWidgets embedded controls not found under " + "'${MCUUTILS_DIR}/QuarkWidgets/embedded' nor " + "'${CMAKE_SOURCE_DIR}/third_party/QuarkWidgets/embedded'. " + "Set MCUUTILS_DIR (e.g. export MCUUTILS_DIR=$HOME/MCUUtils) or init " + "the third_party/QuarkWidgets submodule. Required for MICRO_FORGE_GUI=ON.") +endif() +add_subdirectory("${quarkwidgets_dir}/embedded" + "${CMAKE_BINARY_DIR}/quarkwidgets-embedded-build") + add_executable(micro-forge-gui main.cpp main_window.cpp @@ -24,4 +50,4 @@ add_executable(micro-forge-gui # main.cpp / main_window.cpp include "gui/main_window.hpp" (header sits next to # the .cpp here), so the include root is the repo top, not src/. target_include_directories(micro-forge-gui PRIVATE ${CMAKE_SOURCE_DIR}) -target_link_libraries(micro-forge-gui PRIVATE micro_forge Qt6::Widgets) +target_link_libraries(micro-forge-gui PRIVATE micro_forge Qt6::Widgets QuarkWidgets::Embedded) diff --git a/gui/view/panels/serial_panel.cpp b/gui/view/panels/serial_panel.cpp index 7b256f1..df42f7c 100644 --- a/gui/view/panels/serial_panel.cpp +++ b/gui/view/panels/serial_panel.cpp @@ -1,9 +1,10 @@ // Serial panel — see serial_panel.hpp. #include "gui/view/panels/serial_panel.hpp" +#include "QuarkWidgets/UartTerminalView.hpp" + #include #include -#include #include namespace micro_forge::gui::panels { @@ -11,9 +12,7 @@ namespace micro_forge::gui::panels { SerialPanel::SerialPanel(QWidget* parent) : QWidget(parent) { auto* lay = new QVBoxLayout(this); - view_ = new QTextEdit; - view_->setReadOnly(true); - view_->setStyleSheet("font-family: monospace;"); + view_ = new quark::UartTerminalView; lay->addWidget(view_); input_ = new QLineEdit; @@ -28,8 +27,17 @@ SerialPanel::SerialPanel(QWidget* parent) : QWidget(parent) { void SerialPanel::refresh(const introspection::IntrospectionSnapshot& snap) { const auto sv = snap.peripherals.usart_output; - view_->setPlainText( - QString::fromUtf8(sv.data(), static_cast(sv.size()))); + // Buffer shrank → session rebuilt: old output is stale, resync from zero. + if (sv.size() < shownBytes_) { + view_->clear(); + shownBytes_ = 0; + } + if (sv.size() > shownBytes_) { + const auto len = static_cast(sv.size()) - shownBytes_; + view_->appendText(QString::fromUtf8(sv.data() + shownBytes_, + static_cast(len))); + shownBytes_ = sv.size(); + } } } // namespace micro_forge::gui::panels diff --git a/gui/view/panels/serial_panel.hpp b/gui/view/panels/serial_panel.hpp index ff75e88..0273db9 100644 --- a/gui/view/panels/serial_panel.hpp +++ b/gui/view/panels/serial_panel.hpp @@ -2,15 +2,24 @@ // plus a text input to inject bytes into USART RX (A2). The input only emits // a signal; MainWindow forwards it to model::Session so the panel itself stays // free of simulator coupling. +// +// The terminal body is a quark::UartTerminalView (org-level control). The +// snapshot carries the host's whole accumulated USART buffer each tick, so this +// adapter tracks how many bytes it has already appended and feeds only the new +// tail — keeping the terminal append-only (its RX counter + autoscroll stay +// meaningful) instead of replacing the whole text every tick. #pragma once #include "introspection/introspection.hpp" +#include + #include #include class QLineEdit; -class QTextEdit; + +namespace quark { class UartTerminalView; } namespace micro_forge::gui::panels { @@ -26,8 +35,12 @@ class SerialPanel : public QWidget { void inputSubmitted(const QString& text); private: - QTextEdit* view_; + quark::UartTerminalView* view_; QLineEdit* input_; + // Bytes of the snapshot's USART buffer already shown in the terminal. Used + // to append only the delta each tick. Reset when the buffer shrinks (the + // session rebuilt → old output is stale → terminal cleared). + std::size_t shownBytes_ = 0; }; } // namespace micro_forge::gui::panels diff --git a/gui/view/widgets/stm32_board_widget.cpp b/gui/view/widgets/stm32_board_widget.cpp index 7f941cb..4b51a5a 100644 --- a/gui/view/widgets/stm32_board_widget.cpp +++ b/gui/view/widgets/stm32_board_widget.cpp @@ -1,6 +1,9 @@ // STM32F103 board widget — see stm32_board_widget.hpp. #include "gui/view/widgets/stm32_board_widget.hpp" +#include "QuarkWidgets/LedPanel.hpp" +#include "QuarkWidgets/QuarkBulb.hpp" + #include #include #include @@ -8,11 +11,11 @@ #include #include #include +#include +#include #include #include -#include - namespace micro_forge::gui::view { namespace { @@ -21,12 +24,25 @@ constexpr int kChipH = 250; constexpr int kPinGap = 22; constexpr int kPinStart = 30; // first pin offset from chip top (room for label) constexpr int kWireLen = 70; // pin wire length out to the LED -constexpr int kLedR = 10; // LED radius +constexpr int kPc13Bulb = 24; // PC13 on-board bulb side, px } // namespace Stm32BoardWidget::Stm32BoardWidget(QWidget* parent) : QWidget(parent) { - setMinimumSize(480, 420); + setMinimumSize(600, 440); // widened from 480×420 to fit the LED panel setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + // Port-A LED row (PA0..PA7 + ODR hex) — a LedPanel child. Green tint keeps + // the board's "lit = pin drives high" semantics; small bulbs fit a dense + // pin column beside the chip. + ledPanel_ = new quark::LedPanel(8, Qt::Vertical, this); + ledPanel_->setLabelPrefix(QStringLiteral("PA")); + ledPanel_->setColor(QColor(90, 220, 100)); + ledPanel_->setBulbSize(QSize(24, 24)); + + // On-board PC13 LED — same green "lit" semantics as the port-A row. + pc13Bulb_ = new quark::QuarkBulb(this); + pc13Bulb_->setColor(QColor(90, 220, 100)); + pc13Bulb_->setFixedSize(kPc13Bulb, kPc13Bulb); } void Stm32BoardWidget::refresh( @@ -34,9 +50,32 @@ void Stm32BoardWidget::refresh( odr_[0] = snap.peripherals.gpio[0].odr; // A odr_[1] = snap.peripherals.gpio[1].odr; // B (rendered pins use A/C) odr_[2] = snap.peripherals.gpio[2].odr; // C + if (ledPanel_ != nullptr) ledPanel_->setLevels(odr_[0]); // PA0..PA7 + if (pc13Bulb_ != nullptr) // PC13 = port C bit 13 + pc13Bulb_->setState(static_cast((odr_[2] >> 13) & 1u)); update(); // async — never block the tick loop on a repaint. } +void Stm32BoardWidget::resizeEvent(QResizeEvent*) { + const int chip_x = (width() - kChipW) / 2; + const int chip_y = (height() - kChipH) / 2; + + if (ledPanel_ != nullptr) { + // Place the LED panel just past the UART wire stubs, aligned to the chip. + const int x = chip_x + kChipW + kWireLen + 20; + const QSize hint = ledPanel_->sizeHint(); + ledPanel_->setGeometry(x, chip_y, qMax(96, hint.width()), + qMax(kChipH, hint.height())); + } + if (pc13Bulb_ != nullptr) { + // Below the chip, on centre: the pin pad + wire are painted in + // paintEvent; the bulb self-draws here. + const int cx = chip_x + kChipW / 2; + const int y1 = chip_y + kChipH + 38; // wire length, matches paintEvent + pc13Bulb_->setGeometry(cx - kPc13Bulb / 2, y1, kPc13Bulb, kPc13Bulb); + } +} + void Stm32BoardWidget::paintEvent(QPaintEvent*) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); @@ -65,38 +104,11 @@ void Stm32BoardWidget::paintEvent(QPaintEvent*) { p.setFont(small); p.drawText(QPoint(chip.x() + kChipW / 2 - 24, chip.y() + 26), "Cortex-M3"); - const auto bit = [](std::uint16_t odr, int pin) -> bool { - return (odr >> pin) & 1u; - }; - - // ── right side: PA0..PA7 LED row ── any port-A pin a firmware drives shows. - const auto rightLed = [&](int row, int pin) { - const int y = chip_y + kPinStart + row * kPinGap; - const int x0 = chip_x + kChipW; - const int x1 = x0 + kWireLen; - // pin stub - p.setPen(QPen(QColor(200, 200, 200), 1)); - p.setBrush(QColor(210, 210, 210)); - p.drawRect(x0 - 4, y - 3, 8, 6); - // wire (red when driven high, grey when low) - const bool on = bit(odr_[0], pin); - p.setPen(QPen(on ? QColor(200, 40, 40) : QColor(180, 180, 180), 2)); - p.drawLine(x0, y, x1, y); - // LED - const QRect led_rect(x1, y - kLedR, kLedR * 2, kLedR * 2); - p.setPen(QPen(QColor(60, 60, 60), 1)); - p.setBrush(on ? QColor(90, 220, 100) : QColor(70, 70, 70)); - p.drawEllipse(led_rect); - // pin name - p.setPen(QColor(40, 40, 40)); - p.setFont(small); - p.drawText(x0 + 6, y - 6, QString("PA%1").arg(pin)); - }; - for (int i = 0; i < 8; ++i) { - rightLed(i, i); // row i ↔ pin i (PA0..PA7) - } + // ── PA0..PA7 LEDs: hosted by the ledPanel_ child (see refresh / resizeEvent). + // Only the UART markers (PA9/PA10) + SWD + the PC13 pad/wire/label are + // painted below — PC13's LED itself is the pc13Bulb_ child. - // Below the LED row: PA9/PA10 UART markers (wire + label, no LED). + // PA9/PA10 UART markers (wire + label, no LED). const auto rightTag = [&](int row, const QString& name, const QColor& wire, const QString& tag) { const int y = chip_y + kPinStart + (8 + row) * kPinGap; @@ -136,24 +148,20 @@ void Stm32BoardWidget::paintEvent(QPaintEvent*) { p.drawText(QPoint(chip_x - kWireLen, chip_y + kPinStart + 2 * kPinGap + 14), "SWD"); - // ── bottom: PC13 board LED ── + // ── bottom: PC13 — pin pad + wire + label painted here; the LED itself + // is the pc13Bulb_ child (positioned in resizeEvent, lit in refresh) ── { const int x = chip_x + kChipW / 2; const int y0 = chip_y + kChipH; const int y1 = y0 + 38; p.setPen(QPen(QColor(200, 200, 200), 1)); p.setBrush(QColor(210, 210, 210)); - p.drawRect(x - 3, y0 - 4, 6, 8); + p.drawRect(x - 3, y0 - 4, 6, 8); // pin pad on the chip edge p.setPen(QPen(QColor(40, 160, 200), 2)); - p.drawLine(x, y0, x, y1); - const QRect led_rect(x - kLedR, y1, kLedR * 2, kLedR * 2); - const bool on = bit(odr_[2], 13); - p.setPen(QPen(QColor(60, 60, 60), 1)); - p.setBrush(on ? QColor(90, 220, 100) : QColor(70, 70, 70)); - p.drawEllipse(led_rect); + p.drawLine(x, y0, x, y1); // wire down to the bulb p.setPen(QColor(40, 40, 40)); p.setFont(small); - p.drawText(led_rect.x() - 10, led_rect.bottom() + 12, "PC13 / LED"); + p.drawText(x - 30, y1 + kPc13Bulb + 14, "PC13 / LED"); } // ── legend ── diff --git a/gui/view/widgets/stm32_board_widget.hpp b/gui/view/widgets/stm32_board_widget.hpp index 4496378..29192d0 100644 --- a/gui/view/widgets/stm32_board_widget.hpp +++ b/gui/view/widgets/stm32_board_widget.hpp @@ -1,12 +1,14 @@ -// STM32F103 board widget — paints the chip + pins wired to external LEDs / -// UART / SWD. The LEDs reflect GPIO ODR bits in real time: micro-forge's -// distinctive advantage over a real-hardware debugger (which can't show "the -// light is on" without staring at a physical board). Read-only output; -// pin-level input injection is handled in the gpio_panel (A2 buttons). +// STM32F103 board widget — paints the chip + pins wired to UART / SWD / PC13, +// and hosts a LedPanel child for the Port-A LED row (PA0..PA7). The LEDs reflect +// GPIO ODR bits in real time: micro-forge's distinctive advantage over a +// real-hardware debugger (which can't show "the light is on" without staring at +// a physical board). Read-only output; pin-level input injection is handled in +// the gpio_panel (A2 buttons). // -// Port A is drawn as a row of 8 LEDs (PA0..PA7) so ANY demo firmware driving a -// port-A pin shows up — gpio_blink uses PA5, the F103 CubeMX example uses PA1. -// PC13 (board LED) + USART1 TX/RX + SWD are also drawn. +// Port A (PA0..PA7) is shown via a quark::LedPanel child — any demo firmware +// driving a port-A pin lights up (gpio_blink uses PA5, the F103 CubeMX example +// uses PA1). PC13 is a quark::QuarkBulb child. PA9/PA10 UART markers + SWD + +// the PC13 pin pad/wire/label are still custom-painted here. #pragma once #include "introspection/introspection.hpp" @@ -16,6 +18,8 @@ #include +namespace quark { class LedPanel; class QuarkBulb; } // hosted children + namespace micro_forge::gui::view { class Stm32BoardWidget : public QWidget { @@ -28,11 +32,18 @@ class Stm32BoardWidget : public QWidget { protected: void paintEvent(QPaintEvent* event) override; + void resizeEvent(QResizeEvent* event) override; private: // GPIO ODR per port A/B/C — bit i = pin i driven high. Saved by refresh(), // read by paintEvent() (Qt may repaint at any time, independent of ticks). std::array odr_{}; + + // Port-A LED row (PA0..PA7 + ODR hex), driven from odr_[0]. + quark::LedPanel* ledPanel_{}; + + // On-board PC13 LED, driven from odr_[2] bit 13. + quark::QuarkBulb* pc13Bulb_{}; }; } // namespace micro_forge::gui::view