Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
dc1d4bc
chore(mtcreceiver): bump to 24h-continuity-rc1 (7e9b81d) + clear wrap…
ibiltari Jun 11, 2026
069f951
chore(mtcreceiver): re-pin to 24h-continuity-rc2 (8a30d05)
ibiltari Jun 11, 2026
e121fd9
docs(claude): restructure CLAUDE.md — component detail extracted from…
ibiltari Jul 7, 2026
2454c2f
docs(claude): document gradient-motiond daemon + engine GradientClien…
ibiltari Jul 7, 2026
bc3fb0d
chore(mtcreceiver): re-pin to 59fc76e (mtcframe-conversions-and-fullf…
Jul 29, 2026
1d4b235
docs(planning): add T069 plan for OSC send-failure misclassification
Jul 29, 2026
d9e470d
Revert "chore(mtcreceiver): re-pin to 59fc76e (mtcframe-conversions-a…
Jul 29, 2026
bedcc3f
fix: correct OSC send-failure classification in FadeMotion::evalAndSe…
Jul 29, 2026
ec7e812
fix(log): arm CuemsLogger on the gradient_motion library; route bypasses
ibiltari Jul 29, 2026
adb7ad0
chore(submodules): converge cuemslogger + mtcreceiver on reconciled h…
ibiltari Jul 29, 2026
9f1e0be
build(deb): make packaging build runner-safe; add real BUILD_TESTS op…
Jul 29, 2026
c1ebd03
docs(planning): add T070 plan for layered CI/CD GitHub Actions suite
Jul 29, 2026
2554de5
Merge pull request #7 from stagesoft/fix/library-logging-backend
backenv Jul 30, 2026
ff44b5e
docs: drop stale NNG references; generalize inbound-transport comments
Jul 30, 2026
f92ee09
Merge branch 'chore/bump-mtcreceiver-24h' into fix/osc-send-failure-t…
Jul 30, 2026
0285507
docs: drop stale NNG references; generalize inbound-transport comments
Jul 30, 2026
3f32c9f
Merge branch 'fix/osc-send-failure-threshold-t069' into integration/v…
Jul 30, 2026
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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.1] - 2026-07-29 — Fix OSC send-failure misclassification

See [specs/planning/T069-fix-osc-send-failure-misclassification.md](specs/planning/T069-fix-osc-send-failure-misclassification.md).

### Fixed

* **`FadeMotion::evalAndSend`** (`src/motion/FadeMotion.cpp`): `lo_send()` returns the
number of bytes sent (a positive, non-zero int) on success and `-1` on failure — never
`0`. The failure check used `ret != 0`, which misclassified every successful send as a
failure and killed any fade with `duration_ms` greater than ~25ms once
`kOscFailureThreshold` (5 consecutive ticks) was reached. Changed to `ret < 0`.
* Documented the `OscSendFn` return-value contract on both declarations
(`src/motion/FadeMotion.h`, `src/motion/MotionRegistry.h`) to prevent test mocks (or
future callers) from reintroducing the POSIX-style "0 = success" assumption.
* Added a `WARNING` log on individual OSC send failures in `FadeMotion::evalAndSend`, and
a `DEBUG` log in `MotionRegistry::tick` when a motion recovers from a transient failure
streak — prior visibility was limited to the terminal `MotionError:"osc_send_failed"`.

## [0.3.0] - 2026-05-13 — Phase H: OSC Input Transport

Replaces the NNG bus-client inbound command transport with a localhost UDP OSC listener.
Expand Down
48 changes: 15 additions & 33 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,26 @@
# gradient-motion-engine Development Guidelines
# gradient-motion-engine

Auto-generated from all feature plans. Last updated: 2026-04-23
Part of the **CUEMS** ecosystem — see the [`cuems-RELATIONS`](https://github.com/stagesoft/cuems-RELATIONS) repo for the system index, architecture diagram, and protocol/port map.

## Active Technologies
- C++17 (GCC, `-Wall -O3 -pthread`) + None (C++ standard library only — `<cmath>`, `<vector>`, `<memory>`, `<string>`, `<functional>`) (002-gradient-curves)
- C++17 (GCC, `-Wall -O3 -pthread`) + mtcreceiver v2.0.0 (submodule, pinned at `59fc76e`), (004-adapt-mtc-tick-v2)
- N/A (in-memory adapter; no persistence) (004-adapt-mtc-tick-v2)
- C++17 (GCC, `-Wall -O3 -pthread`) + NNG 1.10.1 (`libnng-dev`, C API — `nng_bus0_open`, (005-nng-bus-client)
- N/A — all state is in-memory. The queue is a fixed-size array; (005-nng-bus-client)
- C++17 (GCC, `-Wall -O3 -pthread`) + liblo (OSC sending), NNG 1.10.1 (already linked), nlohmann-json (already linked), RtMidi via mtcreceiver submodule (already linked) (006-fade-registry-tick-loop)
- N/A — all state in-memory (`std::unordered_map` inside `FadeRegistry`, fixed SPSC queue for status) (006-fade-registry-tick-loop)
## Role

- C++17 (GCC, `-Wall -O3 -pthread`) (001-phase0-scaffold)
Timecode-driven motion and gradient evaluation engine with OSC output — runs as the daemon **`gradient-motiond`** (unit wired by cuems-common). C++17 (GCC, `-Wall -O3 -pthread`). MTC-synced via the `mtcreceiver` submodule; receives commands from the CUEMS engine over localhost UDP OSC and sends OSC out, both via `liblo`. Also uses nlohmann-json.

## Project Structure
**Engine-side client:** `cuems-engine`'s `GradientClient` (`players/GradientClient.py`) is a fire-and-forget UDP OSC client targeting `gradient_osc_port` (7100 in `settings.xml`). Commands: `/gradient/start_fade`, `/gradient/cancel_motion <id>`, `/gradient/cancel_all`. The engine delegates cue fades here (loop-cue fades, ActionCue fades via `ActionHandler`) — the engine's loop only supervises; the fade curve itself is evaluated by this daemon.

```text
src/
tests/
```
## Active technologies (per feature history)

## Commands
- C++17 (GCC, `-Wall -O3 -pthread`), C++ standard library only for the core (`<cmath>`, `<vector>`, `<memory>`, `<string>`, `<functional>`) — `001-phase0-scaffold`, `002-gradient-curves`.
- `mtcreceiver` v2.0.0 (submodule) — `004-adapt-mtc-tick-v2`.
- liblo (OSC), nlohmann-json, RtMidi via mtcreceiver — `006-fade-registry-tick-loop`. All state in-memory (`MotionRegistry` map + fixed SPSC command queue).
- liblo UDP OSC listener (`OscServer`, `127.0.0.1:<gradient_osc_port>`) as the inbound transport — `007-osc-input-transport`. Superseded the NNG bus client of `005-nng-bus-client`; `libnng` is **no longer a build or runtime dependency** (removed in v0.3.0, commit `538d992`), and the outbound NNG status channel is gone — motion status events are logged only.

# Add commands for C++17 (GCC, `-Wall -O3 -pthread`)
## Build & release

## Code Style
Standard C++ submodule build (`git submodule update --init`, cmake/make). Release lineage: `rc_1` carries the fleet-wide MTC >24h work + `24h_extended_support` tag (tip `069f951`, 24h `mtcreceiver` `8a30d05`); `main` is the development line.

C++17 (GCC, `-Wall -O3 -pthread`): Follow standard conventions
For additional per-feature context (project structure, shell commands), read the current plan at [specs/007-osc-input-transport/plan.md](specs/007-osc-input-transport/plan.md). Non-code artifacts follow the same `specs/planning/` convention as cuems-utils.

## Recent Changes
- 006-fade-registry-tick-loop: Added C++17 (GCC, `-Wall -O3 -pthread`) + liblo (OSC sending), NNG 1.10.1 (already linked), nlohmann-json (already linked), RtMidi via mtcreceiver submodule (already linked)
- 005-nng-bus-client: Added C++17 (GCC, `-Wall -O3 -pthread`) + NNG 1.10.1 (`libnng-dev`, C API — `nng_bus0_open`,
- 004-adapt-mtc-tick-v2: Added C++17 (GCC, `-Wall -O3 -pthread`) + mtcreceiver v2.0.0 (submodule, pinned at `59fc76e`),
## Field notes


<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan:
[specs/007-osc-input-transport/plan.md](specs/007-osc-input-transport/plan.md)
<!-- SPECKIT END -->
- Uses the shared `mtcreceiver` submodule — the 2s-resync-skip fix (`aa44894`) and the raw-wire-MTC timebase apply here too; see the mtcreceiver CLAUDE.md.
22 changes: 15 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cmake_minimum_required(VERSION 3.10)
# Allow target_link_libraries() on targets defined in other directories
# (needed to link cuemslogger into mtcreceiver from the root CMakeLists)
cmake_policy(SET CMP0079 NEW)
project(gradient-motion-engine VERSION 0.3.0 LANGUAGES CXX)
project(gradient-motion-engine VERSION 0.3.1 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -39,6 +39,11 @@ add_compile_options(-Wall -O3 -pthread)
# ===========================================================
option(ENABLE_CUEMS_LOGGER "Build with CuemsLogger syslog backend" ON)
option(BUILD_DAEMON "Build the gradient-motiond daemon (requires rtmidi)" ON)
# Default ON so plain `cmake ..` and the CI TEST/COVERAGE stages keep building
# the suite. The packaging BUILD stage (debian/rules) turns it OFF: a .deb only
# ships the daemon, and the latency-budgeted benchmarks are unfit for shared
# runners.
option(BUILD_TESTS "Build the unit/integration test suite" ON)

# ===========================================================
# External dependencies via pkg-config
Expand Down Expand Up @@ -134,8 +139,12 @@ add_subdirectory(src)
# ===========================================================
# Tests
# ===========================================================
enable_testing()
add_subdirectory(tests)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
else()
message(STATUS "BUILD_TESTS=OFF — test suite not configured")
endif()

# ===========================================================
# Version header (generated from project VERSION above)
Expand Down Expand Up @@ -176,10 +185,9 @@ if(BUILD_DAEMON)
target_include_directories(gradient-motiond PRIVATE ${LIBLO_INCLUDE_DIRS})
endif()

if(ENABLE_CUEMS_LOGGER)
target_compile_definitions(gradient-motiond PRIVATE HAVE_CUEMS_LOGGER)
target_link_libraries(gradient-motiond PRIVATE cuemslogger)
endif()
# HAVE_CUEMS_LOGGER + cuemslogger now arrive via gradient_motion's
# PUBLIC usage requirements (src/CMakeLists.txt) — armed on the library
# so all its objects and every consumer share one logging backend.

# Install rules — places the daemon at <prefix>/bin/gradient-motiond.
# debian/rules sets CMAKE_INSTALL_PREFIX=/usr so the .deb ships
Expand Down
2 changes: 1 addition & 1 deletion cuemslogger
17 changes: 8 additions & 9 deletions daemon/comms/OscServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ struct OscServer::Impl {
bool started = false;

static void errorHandler(int num, const char* msg, const char* where) {
std::fprintf(stderr, "ERROR OscServer: liblo error %d — %s (path: %s)\n",
num, msg ? msg : "", where ? where : "");
GME_LOG_ERROR("OscServer: liblo error " + std::to_string(num) + " — "
+ (msg ? msg : "") + " (path: " + (where ? where : "") + ")");
}

// Per-address callback entry point (called on the liblo network thread).
Expand Down Expand Up @@ -84,11 +84,11 @@ struct OscServer::Impl {
break;
}
} catch (const std::exception& e) {
std::fprintf(stderr, "ERROR OscServer: exception in callback for %s: %s\n",
path, e.what());
GME_LOG_ERROR("OscServer: exception in callback for " + std::string(path)
+ ": " + e.what());
} catch (...) {
std::fprintf(stderr, "ERROR OscServer: unknown exception in callback for %s\n",
path);
GME_LOG_ERROR("OscServer: unknown exception in callback for "
+ std::string(path));
}
return 0; // 0 = handled; do not try further methods
}
Expand Down Expand Up @@ -125,8 +125,7 @@ bool OscServer::start() {
impl_->server_thread = lo_server_thread_new(port_str.c_str(), Impl::errorHandler);

if (!impl_->server_thread) {
std::fprintf(stderr, "FATAL OscServer: failed to bind UDP port %s\n",
port_str.c_str());
GME_LOG_CRITICAL("OscServer: failed to bind UDP port " + port_str);
return false;
}

Expand All @@ -143,7 +142,7 @@ bool OscServer::start() {
Impl::onMessage, impl_.get());

if (lo_server_thread_start(impl_->server_thread) != 0) {
std::fprintf(stderr, "FATAL OscServer: lo_server_thread_start failed\n");
GME_LOG_CRITICAL("OscServer: lo_server_thread_start failed");
lo_server_thread_free(impl_->server_thread);
impl_->server_thread = nullptr;
return false;
Expand Down
3 changes: 2 additions & 1 deletion daemon/comms/OscServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ namespace comms {
* wire contract, parses them via `parseFadeOscCommand`, and on `ParseResult::Ok`
* pushes to the `LockFreeQueue<FadeCommand, 64>` supplied at construction.
*
* Namespace: `gme::daemon::comms` (matches NngBusClient convention).
* Namespace: `gme::daemon::comms` — the convention for daemon-specific
* inbound-transport clients.
*
* ## Threading
*
Expand Down
10 changes: 10 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
cuems-gradient-motiond (0.3.1-1) unstable; urgency=medium

* Fix FadeMotion::evalAndSend misclassifying every successful lo_send()
as a failure (ret != 0 instead of ret < 0), which silently killed
every fade with duration_ms > ~25ms via the 5-consecutive-failure
threshold. Add OscSendFn return-value contract docs and warning-level
logging on individual send failures.

-- Adrià Masip <adria@stagelab.coop> Wed, 29 Jul 2026 19:00:53 +0200

cuems-gradient-motiond (0.3.0-1) unstable; urgency=medium

* Phase H — OSC Input Transport (spec 007-osc-input-transport).
Expand Down
35 changes: 35 additions & 0 deletions debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,44 @@ export DEB_BUILD_MAINT_OPTIONS = hardening=+all
%:
dh $@

# --- Submodule guard -------------------------------------------------------
# The daemon links mtcreceiver and cuemslogger, which live in git submodules.
# On a runner that checked out without `submodules: recursive` CMake aborts
# midway with a FATAL_ERROR; fail up front with the actual remedy instead.
override_dh_auto_configure:
@for m in mtcreceiver cuemslogger; do \
test -f "$$m/CMakeLists.txt" || { \
echo "ERROR: submodule '$$m' is missing from the source tree."; \
echo " Run 'git submodule update --init --recursive', or"; \
echo " check out with actions/checkout submodules: recursive."; \
exit 1; }; \
done
dh_auto_configure -- \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DBUILD_DAEMON=ON \
-DBUILD_TESTS=OFF

# --- Packaging BUILD stage: no tests here ---------------------------------
# In the CI/CD pipeline the suite is exercised in dedicated, earlier stages
# (TEST -> COVERAGE) against the same tree; this BUILD stage only has to emit
# a deterministic .deb. Running dh_auto_test here would both duplicate that
# work and fail non-deterministically on shared/virtualized runners (GitHub
# Actions et al.): `test_motion_registry_bench` and `bench_osc_latency` are
# latency-budgeted, the MTC tests want an ALSA sequencer that a container may
# not expose, and `bench_osc_latency` writes its results back into the source
# tree (tests/bench_results/), which dirties a packaging build.
#
# Note this override is what actually skips the run: -DBUILD_TESTS=OFF above
# keeps the suite from being *compiled*, and this keeps it from being *run*,
# so the stage stays correct even if the suite is re-enabled locally.
#
# Escape hatch: `RUN_DEB_TESTS=1 dpkg-buildpackage ...` forces the suite for a
# local package build. An explicit `DEB_BUILD_OPTIONS=nocheck` still wins,
# because dh_auto_test honours it internally.
override_dh_auto_test:
ifeq ($(RUN_DEB_TESTS),1)
dh_auto_test
else
@echo "dh_auto_test: skipped in packaging BUILD stage — tests run in the CI TEST/COVERAGE stages (set RUN_DEB_TESTS=1 to force)"
endif
Loading
Loading