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.
- CMake 4.0+
- Ninja
- GCC / Clang with C++23 support (or MSVC on Windows)
- Git
- Windows only: Npcap in WinPcap-compatible mode — required at runtime for raw EtherCAT packet capture (install with "Install Npcap in WinPcap API-compatible Mode" checked)
git clone <repo-url>
cd motion-master
git submodule update --init --recursive
./tools/configure.sh
./tools/build.shThe motion-master binary lands in build/x64-linux-debug/apps/motion_master/.
motion-master [OPTIONS]
-h, --help Print this help message and exit
--version Display program version and exit
-c, --config TEXT:FILE Path to JSONC config file (JSON with // and /* */ comments)
-p, --port UINT [8443] HTTP/WebSocket port
--cert TEXT:FILE TLS certificate file
--key TEXT:FILE TLS private key file
--cors-origin TEXT [https://motion-master.synapticon.com]
Value sent in Access-Control-Allow-Origin (use '*' to allow any)
-d, --driver TEXT Fieldbus driver: soem (omit to defer initialisation to the HTTP API)
-l, --log-level TEXT [info] Log level: trace, debug, info, warn, error
-a, --adapter TEXT Network adapter for EtherCAT: interface name or MAC address
--list-adapters Print available network adapters and exit
--open Open https://motion-master.synapticon.com/app/ in the default browser after startup
On Linux, motion-master requires raw socket and RT scheduling capabilities. ./tools/build.sh runs sudo setcap cap_sys_nice,cap_net_admin,cap_net_raw=eip on the binary after linking — you will be prompted for your password. Without these capabilities the binary still runs but EtherCAT initialisation will fail.
Release packages are available on the Releases page. Every release ships three artefacts:
| 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 |
All packages install to /opt/motion-master/ with a /usr/local/bin/motion-master symlink.
sudo apt install ./motion-master-<version>-amd64.deb # install or upgrade
sudo apt remove motion-master # remove (leaves cert.pem / key.pem)
sudo apt purge motion-master # full removal including certsThe 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.
sudo dnf install ./motion-master-<version>-x86_64.rpm # Fedora / RHEL (install or upgrade)
sudo zypper install ./motion-master-<version>-x86_64.rpm # openSUSE (install or upgrade)
sudo dnf remove motion-master # full removalOn 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.
tar -xzf motion-master-<version>-linux-x64.tar.gz
cd motion-master-<version>-linux-x64
sudo ./setup.sh # sets capabilities once; re-run after any OS update that resets them
./motion-master --help# Build
git submodule update --init --recursive
docker build -t motion-master .--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.
TLS certificates
Release images have cert.pem/key.pem baked in (CI places them at the repo root before docker build). Developer images built from source fall back to the acme.sh cert or a self-signed cert. The discovery order is the same as tools/run.sh:
# Release image — bundled cert used automatically
docker run --rm --network host motion-master
# Developer image — mount acme.sh cert from host (no browser warning)
docker run --rm --network host \
-v "$HOME/.acme.sh/local.motion-master.synapticon.com_ecc:/root/.acme.sh/local.motion-master.synapticon.com_ecc:ro" \
motion-master
# Developer image — self-signed fallback (browser security exception required)
docker run --rm --network host motion-masterUpdating an expired cert on an older image
The bundled cert is renewed monthly, but an older image keeps its original cert. Override it at runtime — the volume mount shadows the baked-in file:
docker run --rm --network host \
-v /path/to/cert.pem:/opt/motion-master/cert.pem:ro \
-v /path/to/key.pem:/opt/motion-master/key.pem:ro \
motion-masterOr point to an arbitrary path with env vars:
docker run --rm --network host \
-e CERT=/certs/cert.pem -e KEY=/certs/key.pem \
-v /path/to/cert.pem:/certs/cert.pem:ro \
-v /path/to/key.pem:/certs/key.pem:ro \
motion-masterCapabilities
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.
# EtherCAT only (no RT requirement on the host kernel)
docker run --rm --network host \
--cap-add NET_ADMIN --cap-add NET_RAW \
motion-master --driver soem --adapter eth0
# RT scheduling only (PREEMPT_RT host kernel required)
docker run --rm --network host \
--cap-add SYS_NICE --cap-add IPC_LOCK --ulimit memlock=-1 \
motion-master
# Full EtherCAT + RT
docker run --rm --network host \
--cap-add NET_ADMIN --cap-add NET_RAW \
--cap-add SYS_NICE --cap-add IPC_LOCK --ulimit memlock=-1 \
motion-master --driver soem --adapter eth0Production 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:
./tools/run.shIt looks for a certificate in this order:
cert.pem/key.pemnext to the binary (present in release builds)~/.acme.sh/local.motion-master.synapticon.com_ecc/— if you haveacme.shinstalled locally with the Let's Encrypt cert (no browser warning)- Self-signed fallback — generated on the fly; requires accepting a browser security exception once per server restart
Test the API (add -k only when using the self-signed fallback):
curl -k https://localhost:8443/api/version
curl -k https://localhost:8443/api/swagger.ymlThe server sends Access-Control-Allow-Origin: https://motion-master.synapticon.com by default so the production PWA can reach a locally running backend. When developing the UI against a different origin (e.g. Vite dev server on http://localhost:5173), override it via the CORS_ORIGIN env var picked up by tools/run.sh, or by passing --cors-origin to the binary directly:
# Vite dev server
CORS_ORIGIN=http://localhost:5173 ./tools/run.sh
# Allow any origin (development only — do not use in production)
CORS_ORIGIN='*' ./tools/run.sh
# Equivalent, calling the binary directly
./build/x64-linux-debug/apps/motion_master/motion-master --cors-origin http://localhost:5173--driver and --adapter are optional at startup. When omitted, the fieldbus is uninitialised and GET /api/devices returns an empty array. Use the lifecycle endpoints to initialise at runtime:
# 1. Discover available adapters
curl -k https://localhost:8443/api/adapters
# 2. Initialise the fieldbus driver
curl -k -X POST https://localhost:8443/api/init \
-H 'Content-Type: application/json' \
-d '{"driver":"soem","adapter":"eth0"}'
# 3. Scan for slaves and populate the device list
curl -k -X POST https://localhost:8443/api/scan
# 4. List discovered devices
curl -k https://localhost:8443/api/devices
# 5. Transition all devices to Op state (state values: 1=Init, 2=PreOp, 3=Boot, 4=SafeOp, 8=Op)
curl -k -X POST https://localhost:8443/api/state \
-H 'Content-Type: application/json' \
-d '{"state":8}'
# 6. Transition specific devices, with a custom timeout
curl -k -X POST https://localhost:8443/api/state \
-H 'Content-Type: application/json' \
-d '{"state":2,"positions":[1,2],"timeout":3000}'
# 7. Tear down (stops driver, clears device list; init + scan can be called again)
curl -k -X POST https://localhost:8443/api/resetConnect a WebSocket client to wss://localhost:8443/ws. The server sends two message types:
{"type": "monitoring", "topic": "pdos", "data": [1234567890, 39, 0, 12345]}
{"type": "notification", "data": {"event": "slaves_changed"}}Fetch the monitoring schema to interpret the data array:
curl -k https://localhost:8443/api/monitoring/pdosAll 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/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) |
format— runsclang-formatover all.cc/.hsources 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— runscpplintto check for include order, deprecated constructs, and header guards. Configured viaCPPLINT.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 withwarning,style,performance,portabilitychecks at--std=c++23and exits non-zero on any finding.
All components — C++ backend, React UI, OpenAPI spec, and npm packages — share a single semver. VERSION (repo root) is the canonical source; CMake reads it automatically to populate libs/core/version.h. Use the bump script to update every location in one shot:
./tools/bump-version.sh 6.1.0
./tools/bump-version.sh 6.1.0-alpha.0After bumping, commit the changed files, then push a v<version> tag to trigger the release workflow:
git add -A
git commit -m "chore: bump version to 6.1.0"
git tag v6.1.0
git push && git push --tags| Workflow | Trigger | Purpose |
|---|---|---|
build.yml |
push / PR to main |
Build, test; vcpkg packages cached in ~/.cache/vcpkg/archives |
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; update TLS_CERT / TLS_KEY secrets |
release.yml |
v* tag push |
Build release binary, bundle cert + key from secrets, publish GitHub Release with .tar.gz, .deb, and .rpm packages |
The vcpkg cache key is OS + vcpkg.json hash. The first run after a dependency change rebuilds from source; subsequent runs restore from cache.
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 |
| Platform | Status |
|---|---|
| Linux x86-64 | Primary target |
| Linux ARM | Planned |
| Windows x64 | Planned |
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.
# Build
./tools/build.sh
# Run 30 s at 1 ms period, write jitter.csv
sudo ./build/x64-linux-debug/hil/jitter_bench/jitter_bench
# Simulate 300 µs of task load per cycle
sudo ./build/x64-linux-debug/hil/jitter_bench/jitter_bench --workload 300
# Plot (requires matplotlib)
python3 hil/jitter_bench/plot_jitter.py jitter.csv
python3 hil/jitter_bench/plot_jitter.py jitter.csv -o report.png
# Full option list
./build/x64-linux-debug/hil/jitter_bench/jitter_bench --helpThe 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.