From b4ffa3d750e1cb89eb37aef796ed555080818cd7 Mon Sep 17 00:00:00 2001 From: Lukas Kettner Date: Fri, 19 Jun 2026 23:50:35 +0200 Subject: [PATCH 1/5] feat: added new service that enables generating fake readouts for testing/debug purposes --- quickevent/app/quickevent/CMakeLists.txt | 3 + .../plugins/Event/src/eventplugin.cpp | 4 + .../punchingtest/punchingtestservice.cpp | 273 ++++++++++++++++++ .../punchingtest/punchingtestservice.h | 42 +++ .../punchingtestservicewidget.cpp | 57 ++++ .../punchingtest/punchingtestservicewidget.h | 31 ++ .../punchingtest/punchingtestservicewidget.ui | 50 ++++ 7 files changed, 460 insertions(+) create mode 100644 quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp create mode 100644 quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h create mode 100644 quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp create mode 100644 quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.h create mode 100644 quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui diff --git a/quickevent/app/quickevent/CMakeLists.txt b/quickevent/app/quickevent/CMakeLists.txt index e7a89050e..5368f5fe7 100644 --- a/quickevent/app/quickevent/CMakeLists.txt +++ b/quickevent/app/quickevent/CMakeLists.txt @@ -109,6 +109,9 @@ add_executable(quickevent plugins/Event/src/services/qx/qxlateregistrationswidget.ui plugins/Event/src/services/qx/runchangedialog.h plugins/Event/src/services/qx/runchangedialog.cpp plugins/Event/src/services/qx/runchangedialog.ui plugins/Event/src/services/qx/runchange.h plugins/Event/src/services/qx/runchange.cpp + plugins/Event/src/services/punchingtest/punchingtestservice.h plugins/Event/src/services/punchingtest/punchingtestservice.cpp + plugins/Event/src/services/punchingtest/punchingtestservicewidget.h plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp + plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui plugins/Oris/src/chooseoriseventdialog.cpp plugins/Oris/src/chooseoriseventdialog.ui diff --git a/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp b/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp index 08f74624c..32e500ba3 100644 --- a/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/eventplugin.cpp @@ -13,6 +13,7 @@ #include "services/serviceswidget.h" #include "services/emmaclient.h" #include "services/qx/qxclientservice.h" +#include "services/punchingtest/punchingtestservice.h" #include #include @@ -437,6 +438,9 @@ void EventPlugin::onInstalled() auto shvapi_client = new services::qx::QxClientService(this); services::Service::addService(shvapi_client); + auto *punching_test = new services::PunchingTestService(this); + services::Service::addService(punching_test); + { m_servicesDockWidget = new qff::DockWidget(nullptr); m_servicesDockWidget->setObjectName("servicesDockWidget"); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp new file mode 100644 index 000000000..d8c6a4e7e --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp @@ -0,0 +1,273 @@ +#include "punchingtestservice.h" +#include "punchingtestservicewidget.h" + +#include "../../eventplugin.h" + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include + +using qf::gui::framework::getPlugin; + +namespace Event::services { + +PunchingTestService::PunchingTestService(QObject *parent) + : Super(serviceName(), parent) +{ +} + +QString PunchingTestService::serviceName() +{ + return QStringLiteral("PunchingTest"); +} + +QString PunchingTestService::serviceDisplayName() const +{ + return tr("Punching Test"); +} + +void PunchingTestService::run() +{ + PunchingTestServiceSettings ss = settings(); + int interval_sec = ss.punchInterval(); + if (interval_sec <= 0) + interval_sec = 10; + + if (!m_timer) { + m_timer = new QTimer(this); + connect(m_timer, &QTimer::timeout, this, &PunchingTestService::onTimerTick); + } + m_timer->start(interval_sec * 1000); + setStatusMessage(tr("Running, interval: %1 s").arg(interval_sec)); + Super::run(); +} + +void PunchingTestService::stop() +{ + if (m_timer) + m_timer->stop(); + Super::stop(); +} + +void PunchingTestService::onTimerTick() +{ + auto *event_plugin = getPlugin(); + if (!event_plugin->isEventOpen()) { + setStatusMessage(tr("No event open")); + return; + } + int stage_id = event_plugin->currentStageId(); + + qf::core::sql::Query q; + if (!q.exec(QStringLiteral( + "SELECT id, siId, startTimeMs FROM runs" + " WHERE stageId=%1" + " AND isRunning" + " AND siId>0" + " AND (finishTimeMs IS NULL OR finishTimeMs=0)").arg(stage_id))) { + qfWarning() << "PunchingTestService: cannot query runs"; + return; + } + + QList candidates; + while (q.next()) + candidates << QVariantList{q.value(0), q.value(1), q.value(2)}; + + if (candidates.isEmpty()) { + setStatusMessage(tr("No eligible runners left")); + return; + } + + int ix = static_cast(QRandomGenerator::global()->bounded(static_cast(candidates.size()))); + const auto &cand = candidates[ix]; + int run_id = cand[0].toInt(); + int si_id = cand[1].toInt(); + int start_time_ms = cand[2].toInt(); // ms relative to stage start + + auto *runs_plugin = getPlugin(); + quickevent::core::CourseDef course = runs_plugin->courseCodesForRunId(run_id); + QVariantList codes = course.codes(); + + int stage_start_ms = event_plugin->stageStartMsec(stage_id); + + if (start_time_ms <= 0) { + // Runner has no drawn start — place randomly within the past 60 minutes + start_time_ms = static_cast(QRandomGenerator::global()->bounded(60u * 60u * 1000u)); + } + + int abs_start_ms = stage_start_ms + start_time_ms; + // Finish 20..30 minutes after start + int running_ms = (20 * 60 + static_cast(QRandomGenerator::global()->bounded(10u * 60u))) * 1000; + int abs_finish_ms = abs_start_ms + running_ms; + + // SI card times are in seconds within 12-hour AM window (0..43199) + static constexpr int SI_HALF_DAY_SEC = 12 * 3600; + auto toSiSec = [SI_HALF_DAY_SEC](int abs_ms) { + // Use positive modulo to handle pre-midnight edge cases + return ((abs_ms / 1000) % SI_HALF_DAY_SEC + SI_HALF_DAY_SEC) % SI_HALF_DAY_SEC; + }; + + int si_start_sec = toSiSec(abs_start_ms); + int si_finish_sec = toSiSec(abs_finish_ms); + + auto &rng = *QRandomGenerator::global(); + + // 1/100: simulate unknown card — use a random number outside normal SI range + if (rng.bounded(100u) == 0) + si_id = 1000000 + static_cast(rng.bounded(8000000u)); + + // 1/80: missing start punch — runner didn't use start unit + if (rng.bounded(80u) == 0) + si_start_sec = siut::SICard::INVALID_SI_TIME; + + // 1/250: missing finish punch + if (rng.bounded(250u) == 0) + si_finish_sec = siut::SICard::INVALID_SI_TIME; + + // Check time: respect the event's "Card check" max-advance setting. + // When enabled: 1/20 chance the runner checked too early (offset > max, triggers bad-check). + // When disabled: natural 1–2 minute window, no bad-check possible. + int check_offset_sec; + if (auto cfg = event_plugin->eventConfig()->maximumCardCheckAdvanceSec(); cfg.has_value()) { + int max_sec = cfg.value(); + if (rng.bounded(1800u) == 0) { + // Bad check: checked too early — [max+1, max*3] seconds before start + check_offset_sec = max_sec + 1 + static_cast(rng.bounded(static_cast(max_sec * 2))); + } else { + check_offset_sec = 1 + static_cast(rng.bounded(static_cast(max_sec))); + } + } else { + check_offset_sec = 60 + static_cast(rng.bounded(60u)); // 1–2 minutes + } + int si_check_sec = toSiSec(abs_start_ms - check_offset_sec * 1000); + + // Straight-line distance in metres between two WGS-84 points (Haversine) + auto haversine_m = [](double lat1, double lon1, double lat2, double lon2) -> double { + constexpr double R = 6371000.0; + constexpr double DEG = M_PI / 180.0; + double phi1 = lat1 * DEG, phi2 = lat2 * DEG; + double dphi = (lat2 - lat1) * DEG; + double dlam = (lon2 - lon1) * DEG; + double a = std::sin(dphi / 2) * std::sin(dphi / 2) + + std::cos(phi1) * std::cos(phi2) * std::sin(dlam / 2) * std::sin(dlam / 2); + return 2.0 * R * std::atan2(std::sqrt(a), std::sqrt(1.0 - a)); + }; + + quickevent::core::CodeDef start_cd = course.startCode(); + quickevent::core::CodeDef finish_cd = course.finishCode(); + int n_controls = codes.size(); + + // Build per-leg distances: [start→ctrl0, ctrl0→ctrl1, …, ctrl[n-1]→finish] + // Fall back to unit weights when GPS coordinates are absent. + double prev_lat = start_cd.latitude(), prev_lon = start_cd.longitude(); + bool has_coords = (prev_lat != 0.0 || prev_lon != 0.0); + + QVector leg_dist; + leg_dist.reserve(n_controls + 1); + for (int k = 0; k < n_controls; ++k) { + quickevent::core::CodeDef cd(codes[k].toMap()); + if (has_coords) { + double clat = cd.latitude(), clon = cd.longitude(); + leg_dist << haversine_m(prev_lat, prev_lon, clat, clon); + prev_lat = clat; + prev_lon = clon; + } else { + leg_dist << 1.0; + } + } + if (has_coords) + leg_dist << haversine_m(prev_lat, prev_lon, finish_cd.latitude(), finish_cd.longitude()); + else + leg_dist << 1.0; + + // Noisy weights: leg_distance × U[0.8, 1.2] — simulates uneven terrain/navigation + QVector weights; + weights.reserve(leg_dist.size()); + double total_weight = 0.0; + for (double d : leg_dist) { + double w = (d > 0.0 ? d : 1.0) * (0.8 + rng.generateDouble() * 0.4); + weights << w; + total_weight += w; + } + + QVariantList punches; + double cumulative_w = 0.0; + for (int k = 0; k < n_controls; ++k) { + cumulative_w += weights[k]; + // 1/930: mispunch — skip this control entirely + if (rng.bounded(930u) == 0) + continue; + quickevent::core::CodeDef cd(codes[k].toMap()); + double t = cumulative_w / total_weight; + int ctrl_ms = abs_start_ms + static_cast(t * (abs_finish_ms - abs_start_ms)); + int ctrl_sec = (ctrl_ms / 1000) % SI_HALF_DAY_SEC; + siut::SIPunch punch(cd.code(), ctrl_sec); + punches << QVariant(static_cast(punch)); + } + + // 1/30: extra punch — a wrong control inserted at a chronologically correct position + if (rng.bounded(30u) == 0) { + QSet course_codes; + for (const auto &v : punches) + course_codes.insert(siut::SIPunch(v.toMap()).code()); + int extra_code = 0; + const int code_range = quickevent::core::CodeDef::PUNCH_CODE_MAX + - quickevent::core::CodeDef::PUNCH_CODE_MIN + 1; + for (int attempt = 0; attempt < 20 && extra_code == 0; ++attempt) { + int candidate = quickevent::core::CodeDef::PUNCH_CODE_MIN + + static_cast(rng.bounded(static_cast(code_range))); + if (!course_codes.contains(candidate)) + extra_code = candidate; + } + if (extra_code > 0) { + double t = rng.generateDouble(); + int extra_ms = abs_start_ms + static_cast(t * (abs_finish_ms - abs_start_ms)); + int extra_sec = (extra_ms / 1000) % SI_HALF_DAY_SEC; + siut::SIPunch extra_punch(extra_code, extra_sec); + // Insert at the position that keeps the list in chronological order + int insert_at = punches.size(); + for (int i = 0; i < punches.size(); ++i) { + if (extra_sec < siut::SIPunch(punches[i].toMap()).time()) { + insert_at = i; + break; + } + } + punches.insert(insert_at, QVariant(static_cast(extra_punch))); + } + } + + siut::SICard card; + card.setCardNumber(si_id); + card.setCheckTime(si_check_sec); + card.setStartTime(si_start_sec); + card.setFinishTime(si_finish_sec); + card.setPunches(punches); + + setStatusMessage(tr("Card SI %1, %2 controls").arg(si_id).arg(punches.size())); + + getPlugin()->emitSiTaskFinished( + static_cast(siut::SiTask::Type::CardRead), + QVariant(static_cast(card))); +} + +qf::gui::framework::DialogWidget *PunchingTestService::createDetailWidget() +{ + return new PunchingTestServiceWidget(); +} + +} // namespace Event::services diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h new file mode 100644 index 000000000..296522cb2 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h @@ -0,0 +1,42 @@ +#pragma once + +#include "../service.h" + +class QTimer; + +namespace Event { +namespace services { + +class PunchingTestServiceSettings : public ServiceSettings +{ + using Super = ServiceSettings; + + QF_VARIANTMAP_FIELD2(int, p, setP, unchInterval, 10) + +public: + PunchingTestServiceSettings(const QVariantMap &o = QVariantMap()) : Super(o) {} +}; + +class PunchingTestService : public Service +{ + Q_OBJECT + using Super = Service; +public: + explicit PunchingTestService(QObject *parent); + + void run() override; + void stop() override; + + PunchingTestServiceSettings settings() const { return PunchingTestServiceSettings(m_settings); } + static QString serviceName(); + QString serviceDisplayName() const override; + +private: + void onTimerTick(); + qf::gui::framework::DialogWidget *createDetailWidget() override; + +private: + QTimer *m_timer = nullptr; +}; + +}} // namespace Event::services diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp new file mode 100644 index 000000000..91ebb4dcd --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp @@ -0,0 +1,57 @@ +#include "punchingtestservicewidget.h" +#include "ui_punchingtestservicewidget.h" +#include "punchingtestservice.h" + +#include "../service.h" + +#include + +#include + +namespace Event::services { + +PunchingTestServiceWidget::PunchingTestServiceWidget(QWidget *parent) + : Super(parent) + , ui(new Ui::PunchingTestServiceWidget) +{ + setPersistentSettingsId("PunchingTestServiceWidget"); + ui->setupUi(this); + + PunchingTestService *svc = service(); + if (svc) { + PunchingTestServiceSettings ss = svc->settings(); + ui->edPunchInterval->setValue(ss.punchInterval()); + } +} + +PunchingTestServiceWidget::~PunchingTestServiceWidget() +{ + delete ui; +} + +bool PunchingTestServiceWidget::acceptDialogDone(int result) +{ + if (result == QDialog::Accepted) + saveSettings(); + return true; +} + +PunchingTestService *PunchingTestServiceWidget::service() +{ + auto *svc = qobject_cast( + Service::serviceByName(PunchingTestService::serviceName())); + QF_ASSERT(svc, PunchingTestService::serviceName() + " doesn't exist", return nullptr); + return svc; +} + +void PunchingTestServiceWidget::saveSettings() +{ + PunchingTestService *svc = service(); + if (svc) { + PunchingTestServiceSettings ss = svc->settings(); + ss.setPunchInterval(ui->edPunchInterval->value()); + svc->setSettings(ss); + } +} + +} // namespace Event::services diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.h b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.h new file mode 100644 index 000000000..9c2386cb0 --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace Event { +namespace services { + +namespace Ui { + class PunchingTestServiceWidget; +} + +class PunchingTestService; + +class PunchingTestServiceWidget : public qf::gui::framework::DialogWidget +{ + Q_OBJECT + using Super = qf::gui::framework::DialogWidget; +public: + explicit PunchingTestServiceWidget(QWidget *parent = nullptr); + ~PunchingTestServiceWidget() override; + +private: + bool acceptDialogDone(int result) override; + PunchingTestService *service(); + void saveSettings(); + +private: + Ui::PunchingTestServiceWidget *ui; +}; + +}} // namespace Event::services diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui new file mode 100644 index 000000000..6b288274e --- /dev/null +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui @@ -0,0 +1,50 @@ + + + Event::services::PunchingTestServiceWidget + + + + 0 + 0 + 300 + 80 + + + + Punching Test + + + + + + Punch interval + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + s + + + 1 + + + 3600 + + + 10 + + + + + + + edPunchInterval + + + + From a5acf18524a962fb973af6ab8ed7d80fb49dba02 Mon Sep 17 00:00:00 2001 From: Lukas Kettner Date: Thu, 25 Jun 2026 23:09:27 +0200 Subject: [PATCH 2/5] feat: enable setting own probability and add the service description to ui --- .../punchingtest/punchingtestservice.cpp | 21 +- .../punchingtest/punchingtestservice.h | 12 +- .../punchingtestservicewidget.cpp | 12 + .../punchingtest/punchingtestservicewidget.ui | 237 ++++++++++++++++-- 4 files changed, 250 insertions(+), 32 deletions(-) diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp index d8c6a4e7e..3f5399e6f 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp @@ -126,26 +126,24 @@ void PunchingTestService::onTimerTick() int si_finish_sec = toSiSec(abs_finish_ms); auto &rng = *QRandomGenerator::global(); + const PunchingTestServiceSettings ss = settings(); - // 1/100: simulate unknown card — use a random number outside normal SI range - if (rng.bounded(100u) == 0) + if (rng.bounded(static_cast(ss.unknownCardRate())) == 0) si_id = 1000000 + static_cast(rng.bounded(8000000u)); - // 1/80: missing start punch — runner didn't use start unit - if (rng.bounded(80u) == 0) + if (rng.bounded(static_cast(ss.missingStartRate())) == 0) si_start_sec = siut::SICard::INVALID_SI_TIME; - // 1/250: missing finish punch - if (rng.bounded(250u) == 0) + if (rng.bounded(static_cast(ss.missingFinishRate())) == 0) si_finish_sec = siut::SICard::INVALID_SI_TIME; // Check time: respect the event's "Card check" max-advance setting. - // When enabled: 1/20 chance the runner checked too early (offset > max, triggers bad-check). + // When enabled: 1/badCheckRate chance the runner checked too early (triggers bad-check). // When disabled: natural 1–2 minute window, no bad-check possible. int check_offset_sec; if (auto cfg = event_plugin->eventConfig()->maximumCardCheckAdvanceSec(); cfg.has_value()) { int max_sec = cfg.value(); - if (rng.bounded(1800u) == 0) { + if (rng.bounded(static_cast(ss.badCheckRate())) == 0) { // Bad check: checked too early — [max+1, max*3] seconds before start check_offset_sec = max_sec + 1 + static_cast(rng.bounded(static_cast(max_sec * 2))); } else { @@ -209,8 +207,7 @@ void PunchingTestService::onTimerTick() double cumulative_w = 0.0; for (int k = 0; k < n_controls; ++k) { cumulative_w += weights[k]; - // 1/930: mispunch — skip this control entirely - if (rng.bounded(930u) == 0) + if (rng.bounded(static_cast(ss.mispunchRate())) == 0) continue; quickevent::core::CodeDef cd(codes[k].toMap()); double t = cumulative_w / total_weight; @@ -220,8 +217,8 @@ void PunchingTestService::onTimerTick() punches << QVariant(static_cast(punch)); } - // 1/30: extra punch — a wrong control inserted at a chronologically correct position - if (rng.bounded(30u) == 0) { + // Extra punch: a wrong control inserted at a chronologically correct position + if (rng.bounded(static_cast(ss.extraPunchRate())) == 0) { QSet course_codes; for (const auto &v : punches) course_codes.insert(siut::SIPunch(v.toMap()).code()); diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h index 296522cb2..1037f5fdb 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h @@ -11,7 +11,17 @@ class PunchingTestServiceSettings : public ServiceSettings { using Super = ServiceSettings; - QF_VARIANTMAP_FIELD2(int, p, setP, unchInterval, 10) + QF_VARIANTMAP_FIELD2(int, p, setP, unchInterval, 10) // seconds between ticks + + // Per-card imperfections: probability = 1/N + QF_VARIANTMAP_FIELD2(int, u, setU, nknownCardRate, 100) // wrong SI number + QF_VARIANTMAP_FIELD2(int, m, setM, issingStartRate, 80) // no start punch + QF_VARIANTMAP_FIELD2(int, m, setM, issingFinishRate, 250) // no finish punch + QF_VARIANTMAP_FIELD2(int, e, setE, xtraPunchRate, 30) // extra wrong control + QF_VARIANTMAP_FIELD2(int, b, setB, adCheckRate, 1800) // check outside window + + // Per-control imperfection: probability = 1/N + QF_VARIANTMAP_FIELD2(int, m, setM, ispunchRate, 930) // missed control public: PunchingTestServiceSettings(const QVariantMap &o = QVariantMap()) : Super(o) {} diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp index 91ebb4dcd..55550c9b2 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.cpp @@ -21,6 +21,12 @@ PunchingTestServiceWidget::PunchingTestServiceWidget(QWidget *parent) if (svc) { PunchingTestServiceSettings ss = svc->settings(); ui->edPunchInterval->setValue(ss.punchInterval()); + ui->edUnknownCardRate->setValue(ss.unknownCardRate()); + ui->edMissingStartRate->setValue(ss.missingStartRate()); + ui->edMissingFinishRate->setValue(ss.missingFinishRate()); + ui->edExtraPunchRate->setValue(ss.extraPunchRate()); + ui->edBadCheckRate->setValue(ss.badCheckRate()); + ui->edMispunchRate->setValue(ss.mispunchRate()); } } @@ -50,6 +56,12 @@ void PunchingTestServiceWidget::saveSettings() if (svc) { PunchingTestServiceSettings ss = svc->settings(); ss.setPunchInterval(ui->edPunchInterval->value()); + ss.setUnknownCardRate(ui->edUnknownCardRate->value()); + ss.setMissingStartRate(ui->edMissingStartRate->value()); + ss.setMissingFinishRate(ui->edMissingFinishRate->value()); + ss.setExtraPunchRate(ui->edExtraPunchRate->value()); + ss.setBadCheckRate(ui->edBadCheckRate->value()); + ss.setMispunchRate(ui->edMispunchRate->value()); svc->setSettings(ss); } } diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui index 6b288274e..48a3fc1b2 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservicewidget.ui @@ -6,44 +6,243 @@ 0 0 - 300 - 80 + 420 + 380 Punching Test - - - + + + - Punch interval + Generates fake SI card readouts for registered runners at a fixed interval. Each readout simulates realistic imperfections — missing punches, extra controls, unknown card numbers, and bad check times — to stress-test results processing without physical SI cards. + + + true - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - s + + + + QFrame::HLine - - 1 + + QFrame::Sunken - - 3600 + + + + + + + + Punch interval + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + s + + + 1 + + + 3600 + + + 10 + + + + + + + + + Per-card imperfections (1 in N probability) - - 10 + + + + + Unknown card number (1 in) + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + 99999 + + + 100 + + + + + + + Missing start punch (1 in) + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + 99999 + + + 80 + + + + + + + Missing finish punch (1 in) + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + 99999 + + + 250 + + + + + + + Extra wrong control (1 in) + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + 99999 + + + 30 + + + + + + + Bad check time (1 in) + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + 99999 + + + 1800 + + + + + + + + + + Per-control imperfections (1 in N probability) + + + + + Mispunch / missed control (1 in) + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + 99999 + + + 930 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + edPunchInterval + edUnknownCardRate + edMissingStartRate + edMissingFinishRate + edExtraPunchRate + edBadCheckRate + edMispunchRate From 3ffad9b6584479d37d66e39e0f6f8b831e21f1d8 Mon Sep 17 00:00:00 2001 From: Lukas Kettner Date: Fri, 26 Jun 2026 20:17:47 +0200 Subject: [PATCH 3/5] fix: uppercase unsigned literal suffixes and fix static constexpr lambda capture Co-Authored-By: Claude Sonnet 4.6 --- .../src/services/punchingtest/punchingtestservice.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp index 3f5399e6f..60080a3a3 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp @@ -107,17 +107,17 @@ void PunchingTestService::onTimerTick() if (start_time_ms <= 0) { // Runner has no drawn start — place randomly within the past 60 minutes - start_time_ms = static_cast(QRandomGenerator::global()->bounded(60u * 60u * 1000u)); + start_time_ms = static_cast(QRandomGenerator::global()->bounded(60U * 60U * 1000U)); } int abs_start_ms = stage_start_ms + start_time_ms; // Finish 20..30 minutes after start - int running_ms = (20 * 60 + static_cast(QRandomGenerator::global()->bounded(10u * 60u))) * 1000; + int running_ms = (20 * 60 + static_cast(QRandomGenerator::global()->bounded(10U * 60U))) * 1000; int abs_finish_ms = abs_start_ms + running_ms; // SI card times are in seconds within 12-hour AM window (0..43199) static constexpr int SI_HALF_DAY_SEC = 12 * 3600; - auto toSiSec = [SI_HALF_DAY_SEC](int abs_ms) { + auto toSiSec = [](int abs_ms) { // Use positive modulo to handle pre-midnight edge cases return ((abs_ms / 1000) % SI_HALF_DAY_SEC + SI_HALF_DAY_SEC) % SI_HALF_DAY_SEC; }; @@ -129,7 +129,7 @@ void PunchingTestService::onTimerTick() const PunchingTestServiceSettings ss = settings(); if (rng.bounded(static_cast(ss.unknownCardRate())) == 0) - si_id = 1000000 + static_cast(rng.bounded(8000000u)); + si_id = 1000000 + static_cast(rng.bounded(8000000U)); if (rng.bounded(static_cast(ss.missingStartRate())) == 0) si_start_sec = siut::SICard::INVALID_SI_TIME; @@ -150,7 +150,7 @@ void PunchingTestService::onTimerTick() check_offset_sec = 1 + static_cast(rng.bounded(static_cast(max_sec))); } } else { - check_offset_sec = 60 + static_cast(rng.bounded(60u)); // 1–2 minutes + check_offset_sec = 60 + static_cast(rng.bounded(60U)); // 1–2 minutes } int si_check_sec = toSiSec(abs_start_ms - check_offset_sec * 1000); From 2218922eb1d74b554d39419af26c4adf9a06d0db Mon Sep 17 00:00:00 2001 From: Lukas Kettner Date: Wed, 1 Jul 2026 22:00:42 +0200 Subject: [PATCH 4/5] chore: remove unnecessary spaces --- .../punchingtest/punchingtestservice.cpp | 28 +++++++++---------- .../punchingtest/punchingtestservice.h | 14 +++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp index 60080a3a3..98daab672 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp @@ -95,8 +95,8 @@ void PunchingTestService::onTimerTick() int ix = static_cast(QRandomGenerator::global()->bounded(static_cast(candidates.size()))); const auto &cand = candidates[ix]; - int run_id = cand[0].toInt(); - int si_id = cand[1].toInt(); + int run_id = cand[0].toInt(); + int si_id = cand[1].toInt(); int start_time_ms = cand[2].toInt(); // ms relative to stage start auto *runs_plugin = getPlugin(); @@ -110,9 +110,9 @@ void PunchingTestService::onTimerTick() start_time_ms = static_cast(QRandomGenerator::global()->bounded(60U * 60U * 1000U)); } - int abs_start_ms = stage_start_ms + start_time_ms; + int abs_start_ms = stage_start_ms + start_time_ms; // Finish 20..30 minutes after start - int running_ms = (20 * 60 + static_cast(QRandomGenerator::global()->bounded(10U * 60U))) * 1000; + int running_ms = (20 * 60 + static_cast(QRandomGenerator::global()->bounded(10U * 60U))) * 1000; int abs_finish_ms = abs_start_ms + running_ms; // SI card times are in seconds within 12-hour AM window (0..43199) @@ -122,7 +122,7 @@ void PunchingTestService::onTimerTick() return ((abs_ms / 1000) % SI_HALF_DAY_SEC + SI_HALF_DAY_SEC) % SI_HALF_DAY_SEC; }; - int si_start_sec = toSiSec(abs_start_ms); + int si_start_sec = toSiSec(abs_start_ms); int si_finish_sec = toSiSec(abs_finish_ms); auto &rng = *QRandomGenerator::global(); @@ -156,17 +156,17 @@ void PunchingTestService::onTimerTick() // Straight-line distance in metres between two WGS-84 points (Haversine) auto haversine_m = [](double lat1, double lon1, double lat2, double lon2) -> double { - constexpr double R = 6371000.0; + constexpr double R = 6371000.0; constexpr double DEG = M_PI / 180.0; double phi1 = lat1 * DEG, phi2 = lat2 * DEG; double dphi = (lat2 - lat1) * DEG; double dlam = (lon2 - lon1) * DEG; double a = std::sin(dphi / 2) * std::sin(dphi / 2) - + std::cos(phi1) * std::cos(phi2) * std::sin(dlam / 2) * std::sin(dlam / 2); + + std::cos(phi1) * std::cos(phi2) * std::sin(dlam / 2) * std::sin(dlam / 2); return 2.0 * R * std::atan2(std::sqrt(a), std::sqrt(1.0 - a)); }; - quickevent::core::CodeDef start_cd = course.startCode(); + quickevent::core::CodeDef start_cd = course.startCode(); quickevent::core::CodeDef finish_cd = course.finishCode(); int n_controls = codes.size(); @@ -210,8 +210,8 @@ void PunchingTestService::onTimerTick() if (rng.bounded(static_cast(ss.mispunchRate())) == 0) continue; quickevent::core::CodeDef cd(codes[k].toMap()); - double t = cumulative_w / total_weight; - int ctrl_ms = abs_start_ms + static_cast(t * (abs_finish_ms - abs_start_ms)); + double t = cumulative_w / total_weight; + int ctrl_ms = abs_start_ms + static_cast(t * (abs_finish_ms - abs_start_ms)); int ctrl_sec = (ctrl_ms / 1000) % SI_HALF_DAY_SEC; siut::SIPunch punch(cd.code(), ctrl_sec); punches << QVariant(static_cast(punch)); @@ -224,16 +224,16 @@ void PunchingTestService::onTimerTick() course_codes.insert(siut::SIPunch(v.toMap()).code()); int extra_code = 0; const int code_range = quickevent::core::CodeDef::PUNCH_CODE_MAX - - quickevent::core::CodeDef::PUNCH_CODE_MIN + 1; + - quickevent::core::CodeDef::PUNCH_CODE_MIN + 1; for (int attempt = 0; attempt < 20 && extra_code == 0; ++attempt) { int candidate = quickevent::core::CodeDef::PUNCH_CODE_MIN - + static_cast(rng.bounded(static_cast(code_range))); + + static_cast(rng.bounded(static_cast(code_range))); if (!course_codes.contains(candidate)) extra_code = candidate; } if (extra_code > 0) { - double t = rng.generateDouble(); - int extra_ms = abs_start_ms + static_cast(t * (abs_finish_ms - abs_start_ms)); + double t = rng.generateDouble(); + int extra_ms = abs_start_ms + static_cast(t * (abs_finish_ms - abs_start_ms)); int extra_sec = (extra_ms / 1000) % SI_HALF_DAY_SEC; siut::SIPunch extra_punch(extra_code, extra_sec); // Insert at the position that keeps the list in chronological order diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h index 1037f5fdb..b8aff7d4e 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.h @@ -11,17 +11,17 @@ class PunchingTestServiceSettings : public ServiceSettings { using Super = ServiceSettings; - QF_VARIANTMAP_FIELD2(int, p, setP, unchInterval, 10) // seconds between ticks + QF_VARIANTMAP_FIELD2(int, p, setP, unchInterval, 10) // seconds between ticks // Per-card imperfections: probability = 1/N - QF_VARIANTMAP_FIELD2(int, u, setU, nknownCardRate, 100) // wrong SI number - QF_VARIANTMAP_FIELD2(int, m, setM, issingStartRate, 80) // no start punch - QF_VARIANTMAP_FIELD2(int, m, setM, issingFinishRate, 250) // no finish punch - QF_VARIANTMAP_FIELD2(int, e, setE, xtraPunchRate, 30) // extra wrong control - QF_VARIANTMAP_FIELD2(int, b, setB, adCheckRate, 1800) // check outside window + QF_VARIANTMAP_FIELD2(int, u, setU, nknownCardRate, 100) // wrong SI number + QF_VARIANTMAP_FIELD2(int, m, setM, issingStartRate, 80) // no start punch + QF_VARIANTMAP_FIELD2(int, m, setM, issingFinishRate, 250) // no finish punch + QF_VARIANTMAP_FIELD2(int, e, setE, xtraPunchRate, 30) // extra wrong control + QF_VARIANTMAP_FIELD2(int, b, setB, adCheckRate, 1800) // check outside window // Per-control imperfection: probability = 1/N - QF_VARIANTMAP_FIELD2(int, m, setM, ispunchRate, 930) // missed control + QF_VARIANTMAP_FIELD2(int, m, setM, ispunchRate, 930) // missed control public: PunchingTestServiceSettings(const QVariantMap &o = QVariantMap()) : Super(o) {} From 7591dfa04e0e8f0faa2fda7efd8b86bff26a451c Mon Sep 17 00:00:00 2001 From: Lukas Kettner Date: Wed, 1 Jul 2026 22:41:32 +0200 Subject: [PATCH 5/5] chore: reuse latlng_distance instead of new function haversine_m --- .../punchingtest/punchingtestservice.cpp | 18 ++---------------- .../quickevent/plugins/Runs/src/runsplugin.cpp | 4 +--- .../quickevent/plugins/Runs/src/runsplugin.h | 1 + 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp index 98daab672..0f228fec4 100644 --- a/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp +++ b/quickevent/app/quickevent/plugins/Event/src/services/punchingtest/punchingtestservice.cpp @@ -20,8 +20,6 @@ #include #include -#include - using qf::gui::framework::getPlugin; namespace Event::services { @@ -154,18 +152,6 @@ void PunchingTestService::onTimerTick() } int si_check_sec = toSiSec(abs_start_ms - check_offset_sec * 1000); - // Straight-line distance in metres between two WGS-84 points (Haversine) - auto haversine_m = [](double lat1, double lon1, double lat2, double lon2) -> double { - constexpr double R = 6371000.0; - constexpr double DEG = M_PI / 180.0; - double phi1 = lat1 * DEG, phi2 = lat2 * DEG; - double dphi = (lat2 - lat1) * DEG; - double dlam = (lon2 - lon1) * DEG; - double a = std::sin(dphi / 2) * std::sin(dphi / 2) - + std::cos(phi1) * std::cos(phi2) * std::sin(dlam / 2) * std::sin(dlam / 2); - return 2.0 * R * std::atan2(std::sqrt(a), std::sqrt(1.0 - a)); - }; - quickevent::core::CodeDef start_cd = course.startCode(); quickevent::core::CodeDef finish_cd = course.finishCode(); int n_controls = codes.size(); @@ -181,7 +167,7 @@ void PunchingTestService::onTimerTick() quickevent::core::CodeDef cd(codes[k].toMap()); if (has_coords) { double clat = cd.latitude(), clon = cd.longitude(); - leg_dist << haversine_m(prev_lat, prev_lon, clat, clon); + leg_dist << Runs::RunsPlugin::latlng_distance(prev_lat, prev_lon, clat, clon); prev_lat = clat; prev_lon = clon; } else { @@ -189,7 +175,7 @@ void PunchingTestService::onTimerTick() } } if (has_coords) - leg_dist << haversine_m(prev_lat, prev_lon, finish_cd.latitude(), finish_cd.longitude()); + leg_dist << Runs::RunsPlugin::latlng_distance(prev_lat, prev_lon, finish_cd.latitude(), finish_cd.longitude()); else leg_dist << 1.0; diff --git a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp index 64f8270de..b5e3ccbe7 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp +++ b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.cpp @@ -174,8 +174,7 @@ int RunsPlugin::courseForRun(int run_id) } return courseForRun_Classic(run_id); } -namespace { -int latlng_distance(double lat1, double lng1, double lat2, double lng2) +int RunsPlugin::latlng_distance(double lat1, double lng1, double lat2, double lng2) { /// http://www.movable-type.co.uk/scripts/latlong.html if(qFuzzyIsNull(lng2 - lng1) && qFuzzyIsNull(lat2 - lat1)) @@ -194,7 +193,6 @@ int latlng_distance(double lat1, double lng1, double lat2, double lng2) double d = std::sqrt(x*x + y*y) * R; return static_cast(std::ceil(d)); } -} quickevent::core::CourseDef RunsPlugin::courseCodesForRunId(int run_id) { qfLogFuncFrame() << "run id:" << run_id; diff --git a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h index 3df7c594f..4332742a4 100644 --- a/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h +++ b/quickevent/app/quickevent/plugins/Runs/src/runsplugin.h @@ -56,6 +56,7 @@ class RunsPlugin : public qf::gui::framework::Plugin int courseForRelay(int relay_number, int leg); quickevent::core::CourseDef courseCodesForRunId(int run_id); quickevent::core::CourseDef courseForCourseId(int course_id); + static int latlng_distance(double lat1, double lng1, double lat2, double lng2); Q_INVOKABLE int cardForRun(int run_id); qf::core::utils::TreeTable currentStageResultsTable(const QString &class_filter = QString(), int max_competitors_in_class = 0, bool exclude_disq = false);