|
Motion Master 6.0.0-alpha.50
Next-generation motion control software
|
Next-generation motion control software for SOMANET servo drives. Browser-based control interface, real-time process data exchange, and a secure HTTP API and WebSocket interface — control from any language, any tool, any AI agent.
See FEATURES.md for a full catalog of capabilities.
Design documents with Mermaid diagrams (rendered natively on GitHub):
Release packages are available on the Releases page. Every release ships binaries for Linux, Windows, and macOS:
| Artefact | Format | Install on |
|---|---|---|
motion-master-<version>-linux-x64.tar.gz | Tarball | Any Linux x86-64 |
motion-master-<version>-amd64.deb | Debian package | Ubuntu / Debian |
motion-master-<version>-x86_64.rpm | RPM package | Fedora / RHEL / openSUSE |
motion-master-<version>-windows-x64.zip | Zip archive | Windows x64 |
motion-master-<version>-macos-arm64.tar.gz | Tarball | macOS (Apple Silicon) |
The Linux .deb/.rpm packages install to /opt/motion-master/ with a /usr/local/bin/motion-master symlink.
The postinst script automatically sets the required capabilities (cap_sys_nice, cap_net_admin, cap_net_raw) on the binary. On upgrade the capabilities are re-applied to the new binary automatically.
Note:
apt removeleavescert.pemandkey.pembehind as conffiles. Useapt purgefor a complete uninstall.
On uninstall, unmodified cert.pem and key.pem are removed automatically. If you replaced them with your own, they are saved as cert.pem.rpmsave / key.pem.rpmsave.
Unzip motion-master-<version>-windows-x64.zip — it contains motion-master.exe, the bundled cert.pem/key.pem, an auto-loaded motion-master.jsonc (preset to a 4 ms real-time cycle, robust on stock Windows timers), the annotated motion-master.example.jsonc, and the required vcpkg runtime DLLs. Install the two runtime dependencies listed under Usage → Prerequisites (Visual C++ Redistributable and Npcap), then run motion-master.exe from the extracted directory — it picks up the neighbouring motion-master.jsonc automatically (edit it to change the cycle period or any other setting).
Motion Master also runs as a container image. Building the image and publishing it to a registry is covered under Development → Docker image; this section covers running an image you built locally or pulled from a registry.
--network host is required on all docker run commands — the server binds to 127.0.0.1 and Docker's port forwarding never reaches the loopback interface.
Every image bakes cert.pem/key.pem in at build time by fetching them from the rolling tls-cert release (see Development → Docker image); an image built offline ships with empty placeholders and self-heals on start instead. The runtime discovery order is the same as tools/run.sh:
The bundled cert is renewed monthly, but an older image keeps its original cert. By default the container self-heals: if the baked-in cert is missing, expired, or expiring soon at startup it fetches a fresh one from the rolling release before serving (set "tls": { "autoUpdate": false } in a mounted config to disable, e.g. for air-gapped hosts). To pin your own cert instead, override it at runtime — the volume mount shadows the baked-in file:
Or point to an arbitrary path with env vars:
Docker drops most Linux capabilities by default. On a bare-metal install postinst/setup.sh stamps the binary with setcap so any user can run it and it receives the required capabilities automatically. Inside a container, file capabilities are ignored — you grant the equivalent capabilities to the container process with --cap-add at docker run time instead.
| Capability | What it unlocks | Required for |
|---|---|---|
CAP_NET_RAW | Open raw/packet sockets | SOEM sending/receiving raw EtherCAT frames |
CAP_NET_ADMIN | Configure network interfaces | SOEM putting the NIC into promiscuous mode |
CAP_SYS_NICE | Set SCHED_FIFO scheduling policy and RT priority | Real-time game loop |
CAP_IPC_LOCK | Call mlockall() to pin process memory | Preventing page faults during RT cycles |
--ulimit memlock=-1 is also required alongside CAP_IPC_LOCK — without it the kernel rejects mlockall() even when the capability is present.
The binary degrades gracefully: missing RT caps produce a warning and the loop runs without RT guarantees; missing EtherCAT caps cause POST /api/init to fail when a SOEM driver is requested.
To auto-initialise the fieldbus in a container, mount a JSONC config with a fieldbus block ({ "driver": "soem", "adapter": "eth0" }) and pass --config — there are no --driver/--adapter flags. Omit it to defer initialisation to POST /api/init at runtime.
Pull and run a published image, mounting cert.pem/key.pem from the current directory. Source paths must be absolute — a relative -v cert.pem:... is interpreted as a named volume, not your local file, and the container silently falls back to a self-signed cert. Use $(pwd)/ to force an absolute path:
docker run pulls the image automatically if it isn't present locally. Drop the two -v flags to let the container self-heal its cert instead (fetches a fresh Let's Encrypt cert at startup; see Updating an expired cert above).
Requirements for running a release binary (building from source has its own — see Development):
| Platform | Status |
|---|---|
| Linux x86-64 | Primary target — .tar.gz, .deb, .rpm |
| Windows x64 | Supported — .zip |
| macOS (Apple Silicon) | Supported — .tar.gz |
| Linux ARM64 | Built & tested in CI; no release package yet |
The command line carries only actions and the cert-fetch source URLs. Every tunable setting — ports, fieldbus driver/adapter, log level, CORS origin, TLS cert/key paths, and cert auto-update — lives in a JSONC config file (see motion-master.example.jsonc). Motion Master loads that file from one of two places: the path you pass with -c/--config, or, absent that, a motion-master.jsonc sitting next to the executable (auto-discovered — --config wins over it). With neither, all built-in defaults apply. The Windows release ships such a motion-master.jsonc preconfigured to a 4 ms real-time cycle (robust on stock Windows timers); Linux and macOS keep the 1 ms default and ship only the annotated example.
On Linux, motion-master requires raw socket and RT scheduling capabilities. Release packages set these automatically (deb/rpm postinst, tarball setup.sh). Without these capabilities the binary still runs but EtherCAT initialisation will fail.
Build toolchain:
The motion-master binary lands in build/x64-linux-debug/apps/motion_master/.
On Linux, ./tools/build.sh --setcap runs sudo setcap cap_sys_nice,cap_net_admin,cap_net_raw,cap_ipc_lock=eip on the binary after linking — you will be prompted for your password. Without cap_net_raw/cap_net_admin EtherCAT initialisation fails; without cap_sys_nice/cap_ipc_lock the binary still runs but the RT game loop cannot raise its priority or pin its memory (visible as mlockall: no on the Game Loop page).
Production releases bundle a real Let's Encrypt TLS certificate for local.motion-master.synapticon.com, so the PWA at https://motion-master.synapticon.com connects without any browser warning.
For development, the run script picks up a cert automatically:
It looks for a certificate in this order:
cert.pem / key.pem next to the binary (present in release builds)~/.acme.sh/local.motion-master.synapticon.com_ecc/ — if you have acme.sh installed locally with the Let's Encrypt cert (no browser warning)If the cert is missing, expired, or expiring soon at startup, the binary fetches a fresh one from the rolling release and installs it before serving (disable by setting tls.autoUpdate to false in the config). You can also refresh on demand — motion-master --update-cert (terminal) or the Refresh certificate button on the PWA's Connection page (POST /api/cert/refresh). The cert is rotated monthly and published at a stable URL, decoupled from app releases:
Test the API (add -k only when using the self-signed fallback):
The server sends Access-Control-Allow-Origin: https://motion-master.synapticon.com by default so the production PWA can reach a locally running backend. The origin is the server.corsOrigin config setting (there is no CLI flag). When developing the UI against a different origin (e.g. Vite dev server on http://localhost:5173), the easiest path is the CORS_ORIGIN env var, which tools/run.sh bakes into the config it generates:
Calling the binary directly, set it in your config file instead:
Build the runtime image from a source checkout:
The build fetches the current cert.pem/key.pem from the rolling tls-cert release and bakes them in — the same single source the running binary's self-heal uses, so no secrets are involved. An offline build bakes empty placeholders instead; the container then self-signs and the startup self-heal fetches a real cert on first run. See Installation → Docker for running the image, the cert discovery order, and the --cap-add flags.
tools/docker-build.sh builds the image from the current VERSION and tags it with the bare version (Docker convention — no v prefix) plus a rolling tag: latest for a stable release, or next for a prerelease (a - in the version, e.g. 6.0.0-alpha.34). It defaults to the markosankovic/motion-master repository; override with an argument or the IMAGE env var.
Prereleases move next, not latest, so a plain docker pull markosankovic/motion-master (which resolves latest) always lands on the newest stable image.
You can add your own HTTP endpoints in C++ without touching the server core. libs/example is a copy-me starter (the server-side counterpart of the web/apps/example PWA) that registers GET /api/example/devices:
To add your own:
libs/example, then rename the directory, the mm::example namespace, and the /api/example/... route prefix.*_logic.{h,cc} — plain, HTTP-agnostic functions that take a DeviceManager& (testable with no server). Format responses in *_routes.cc using the mm::api::sendJson / sendError / sendStatus helpers so the content type and CORS headers match the built-in routes.CMakeLists.txt and link your lib into apps/motion_master.Wire it in main.cc before the server starts:
Your registerRoutes(uWS::SSLApp&, const mm::api::RouteContext&) runs once on the HTTP event-loop thread, after the built-in routes and before the catch-all 404. Register only your own paths (e.g. /api/yourapp/...) — never the /api/* or /* wildcards. The transport glue lives in libs/api (mm::api); the domain layer (mm::node) stays free of any HTTP/uWebSockets dependency. These routes are intentionally not part of swagger.yml — that spec documents the stable built-in API only.
All scripts default to the x64-linux-debug preset. Pass a preset name as the first argument to override (e.g. ./tools/build.sh x64-linux-release).
| Script | Description |
|---|---|
./tools/configure.sh | Run CMake configure |
./tools/build.sh | Build all targets |
./tools/run.sh | Run the binary with the best available TLS cert (real cert if acme.sh is set up, self-signed otherwise) |
./tools/test.sh | Run tests |
./tools/format.sh | Auto-format all sources with clang-format |
./tools/lint.sh | Run cpplint (pip install cpplint if missing) |
./tools/cppcheck.sh | Run cppcheck static analysis |
./tools/check.sh | Run format, cppcheck, and lint in sequence |
./tools/clean.sh | Remove the build directory |
./tools/bump-version.sh <version> | Bump the project semver everywhere (see Versioning) |
./tools/package.sh [preset] | Build .deb and .rpm packages (requires cert.pem/key.pem in the build dir) |
./tools/docker-build.sh [image] | Build + tag the Docker image from the current VERSION (version tag + latest/next) |
format** — runs clang-format over all .cc/.h sources and rewrites them in-place. Enforces Google style with a 100-column limit as defined in .clang-format. CI fails if any file is not already formatted.lint** — runs cpplint to check for include order, deprecated constructs, and header guards. Configured via CPPLINT.cfg. Naming conventions are enforced in code review, not by this tool.cppcheck** — static analysis that catches bugs the compiler doesn't warn about: null pointer dereferences, out-of-bounds access, uninitialized variables, resource leaks, etc. Runs with warning,style,performance,portability checks at --std=c++23 and exits non-zero on any finding.Managed via vcpkg. No manual installation needed — vcpkg downloads and builds everything on first configure.
| Package | Purpose |
|---|---|
| CLI11 | Command-line argument parsing |
| spdlog | Structured logging |
| nlohmann-json | JSONC config file parsing (comments enabled) and HTTP response serialization |
| neargye-semver | Semantic versioning |
| uwebsockets | HTTP and WebSocket server (TLS via OpenSSL) |
| GTest | Unit testing |
Motion Master targets CONFIG_PREEMPT_RT kernels for hard real-time operation. The GameLoop sets SCHED_FIFO priority 80 and calls mlockall before entering the cycle loop. The cycle timer uses clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME) so scheduling jitter in one cycle never accumulates into drift.
The hil/ directory contains standalone binaries for validating RT behaviour on a pre-configured Linux machine. They are built automatically with the rest of the project but require root (or CAP_SYS_NICE + CAP_IPC_LOCK) to produce valid results.
Measures the cycle-to-cycle scheduling jitter of the GameLoop timer loop — how much each actual cycle interval deviates from the target period.
The plot shows a time-series with P99/P99.9 reference lines and a jitter histogram. The terminal output prints min/max/mean/stddev/P50/P95/P99/P99.9 and an overrun count. Compare a standard kernel against PREEMPT_RT by running with --workload 300 (a realistic 1 ms cycle budget) on each.
All components — C++ backend, React UI, OpenAPI spec, and npm packages — share a single semver. VERSION (repo root) is the canonical source: a one-line plain-text file (e.g. 6.0.0-alpha.31). There is no auto-increment — a human picks the next version. Everything else is either derived from VERSION at build time or kept in sync with it by the bump script.
Never edit VERSION by hand. Run the bump script with the new version:
It writes VERSION, then propagates the value to every location that isn't auto-derived: vcpkg.json, the package.json manifests (root workspace + motion-master, motion-master-client, hil/api), swagger.yml (info.version), the version_test.cc assertion, and the UI sidebar badge in RootLayout.tsx.
CMake does the propagation into native code at configure time — version.h is generated, never edited by hand:
CMakeLists.txt reads the file into a variable: file(STRINGS "${CMAKE_SOURCE_DIR}/VERSION" MM_VERSION).libs/core/CMakeLists.txt runs configure_file(version.h.in …), substituting @MM_VERSION@ in the template to produce the build-dir version.h:
The static_assert is a compile-time guard: a malformed version in VERSION breaks the build rather than shipping a bad string. The Doxyfile version is propagated the same way.
After bumping, commit the changed files, then push a v<version> tag to trigger the release workflow:
The v* tag builds the platform binaries and publishes @synapticon/motion-master-client@<version> to npm (prereleases under the next dist-tag). Two drift nets back the sync: version_test.cc fails if its hard-coded string falls out of step, and the api-client-drift CI job fails if the committed API client is stale against swagger.yml.
| Workflow | Trigger | Purpose |
|---|---|---|
build-linux-x64.yml | push / PR to main | Build & test (Linux x64); vcpkg packages cached |
build-linux-arm64.yml | push / PR to main | Build & test (Linux ARM64) |
build-macos-arm64.yml | push / PR to main | Build & test (macOS Apple Silicon) |
build-windows-x64.yml | push / PR to main | Build & test (Windows x64) |
lint.yml | push / PR to main | clang-format + cpplint checks |
cert-renewal.yml | 1st of every month | Renew Let's Encrypt cert via acme-dns; publish it to the rolling tls-cert release |
release.yml | v* tag push | Build all platforms, bundle cert + key from the rolling tls-cert release, publish GitHub Release with .tar.gz, .deb, .rpm (Linux), .zip (Windows), and .tar.gz (macOS arm64) |
The vcpkg cache key is OS + vcpkg.json hash. The first run after a dependency change rebuilds from source; subsequent runs restore from cache.