Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
20 changes: 14 additions & 6 deletions gui/view/panels/serial_panel.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
// Serial panel — see serial_panel.hpp.
#include "gui/view/panels/serial_panel.hpp"

#include "QuarkWidgets/UartTerminalView.hpp"

#include <QLineEdit>
#include <QString>
#include <QTextEdit>
#include <QVBoxLayout>

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;
Expand All @@ -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<int>(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<std::size_t>(sv.size()) - shownBytes_;
view_->appendText(QString::fromUtf8(sv.data() + shownBytes_,
static_cast<int>(len)));
shownBytes_ = sv.size();
}
}

} // namespace micro_forge::gui::panels
17 changes: 15 additions & 2 deletions gui/view/panels/serial_panel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cstddef>

#include <QString>
#include <QWidget>

class QLineEdit;
class QTextEdit;

namespace quark { class UartTerminalView; }

namespace micro_forge::gui::panels {

Expand All @@ -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
96 changes: 52 additions & 44 deletions gui/view/widgets/stm32_board_widget.cpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
// 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 <QColor>
#include <QFont>
#include <QPainter>
#include <QPaintEvent>
#include <QPen>
#include <QPoint>
#include <QRect>
#include <QResizeEvent>
#include <QSize>
#include <QSizePolicy>
#include <QString>

#include <cstdint>

namespace micro_forge::gui::view {

namespace {
Expand All @@ -21,22 +24,58 @@ 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(
const introspection::IntrospectionSnapshot& snap) {
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<bool>((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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 ──
Expand Down
27 changes: 19 additions & 8 deletions gui/view/widgets/stm32_board_widget.hpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -16,6 +18,8 @@

#include <QWidget>

namespace quark { class LedPanel; class QuarkBulb; } // hosted children

namespace micro_forge::gui::view {

class Stm32BoardWidget : public QWidget {
Expand All @@ -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<std::uint16_t, 3> 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
Loading