Pilgrim.EasyWorship
0.2.0
dotnet add package Pilgrim.EasyWorship --version 0.2.0
NuGet\Install-Package Pilgrim.EasyWorship -Version 0.2.0
<PackageReference Include="Pilgrim.EasyWorship" Version="0.2.0" />
<PackageVersion Include="Pilgrim.EasyWorship" Version="0.2.0" />
<PackageReference Include="Pilgrim.EasyWorship" />
paket add Pilgrim.EasyWorship --version 0.2.0
#r "nuget: Pilgrim.EasyWorship, 0.2.0"
#:package Pilgrim.EasyWorship@0.2.0
#addin nuget:?package=Pilgrim.EasyWorship&version=0.2.0
#tool nuget:?package=Pilgrim.EasyWorship&version=0.2.0
ew2mqtt
A cross-platform .NET 10 service that bridges EasyWorship's
undocumented ezwremote TCP API to MQTT. State changes (slide number, presentation
number, overlays) are published as retained MQTT topics, and a parallel command topic
tree accepts incoming next/prev/goto/play/overlay commands and forwards them to
EasyWorship.
Designed for Home Assistant, Node-RED, OBS automations, lighting consoles, and anything else that speaks MQTT.
Status: alpha. Built from publicly reverse-engineered protocol details (see Credits) and tested end-to-end against a fake server. Behaviour against a real EasyWorship instance may surface protocol corner cases that need configuration tweaks β see Risks & defaults.
Features
- Auto-discovery of EasyWorship via mDNS (
_ezwremote._tcp.local.) with manualHost:Portfallback. - Bidirectional: publishes
status/paired/heartbeatevents to MQTT and subscribes to a command topic fornextSlide,prevSlide,gotoSlide,play/pause/toggle, schedule navigation, and overlay control. - Stable pairing: the GUID used for pairing is generated and persisted on first run, so reconnects don't require operator approval again.
- Robust reconnection: two-phase exponential backoff that never gives up (1sβ5s for three minutes, then steady 30s).
- MQTT LWT (
availabilitytopic) for clean dashboards. - Cross-platform: Linux, macOS, Windows. systemd, launchd, and Windows Service
integration ship in
packaging/. - NuGet-publishable libraries so you can embed the EasyWorship client in your own .NET app without taking the whole bridge.
Install
Docker
docker run --rm \
--network host \
-e Ew2Mqtt__Mqtt__Server=mqtt://broker.lan:1883 \
-e Ew2Mqtt__EasyWorship__DiscoveryMode=AutoThenManual \
ghcr.io/pilgrimsoftworks/ew2mqtt:latest
mDNS discovery requires --network host because multicast cannot cross the Docker
bridge. If you can't use host networking, configure a static
Ew2Mqtt__EasyWorship__Host=10.0.0.5 and skip discovery.
Self-contained binary
Download a tarball from the Releases page (Linux/macOS/Windows, x64/arm64), extract, and run:
./ew2mqtt --Ew2Mqtt:Mqtt:Server=mqtt://localhost:1883
systemd
sudo install -d /etc/ew2mqtt
sudo install -m644 packaging/systemd/ew2mqtt.service /etc/systemd/system/
sudo install -m755 ew2mqtt /usr/local/bin/
sudo systemctl enable --now ew2mqtt
Windows Service
sc.exe create ew2mqtt binPath= "C:\Program Files\ew2mqtt\ew2mqtt.exe" start= auto
sc.exe failure ew2mqtt reset= 86400 actions= restart/5000/restart/5000/restart/30000
See packaging/windows/INSTALL.md for details.
macOS (launchd)
sudo cp packaging/launchd/no.pilgrim.ew2mqtt.plist /Library/LaunchDaemons/
sudo launchctl load /Library/LaunchDaemons/no.pilgrim.ew2mqtt.plist
Pairing & permissions
When you approve the pairing in EasyWorship's Remote panel, the device starts in view-only mode (lock icon π next to the device name). The bridge can receive state but all outgoing commands β slide navigation, overlay toggles, play/pause β will be silently dropped by EasyWorship.
Click the lock icon next to the ew2mqtt device in EasyWorship's Remote panel
to grant control. The icon flips to a remote-control symbol and commands start
working immediately. The next status frame will show permissions non-zero.
If overlay or navigation commands appear to do nothing despite the bridge
logging EW out: {...} and EasyWorship echoing back a status with unchanged
state, this is the cause 99 % of the time.
Configuration
appsettings.json (or env vars with Ew2Mqtt__... / EW2MQTT_...):
{
"Ew2Mqtt": {
"InstanceId": "default",
"EasyWorship": {
"DiscoveryMode": "AutoThenManual",
"Host": null,
"Port": null,
"Uid": null,
"DeviceName": "ew2mqtt",
"DeviceType": 8,
"HeartbeatIntervalSeconds": 3
},
"Mqtt": {
"Server": "mqtt://localhost:1883",
"Username": null,
"Password": null,
"Tls": false,
"TopicBase": "ew2mqtt",
"RetainState": true
}
}
}
EasyWorship's ezwremote listener binds a dynamic port β there is no fixed
default. Leave Host/Port null and let auto-discovery (mDNS, or the
same-machine process probe) learn the real address. Only set Port explicitly
for Manual/AutoThenManual fallback if you genuinely know the port the
running EasyWorship is on; a missing port in those modes is logged as a
configuration error rather than defaulted.
Set Mqtt.Tls to true (or use an mqtts:// server URL) for TLS. The broker
certificate is validated against the system trust store with the default
settings β there is no option for self-signed certificates or a custom CA, so
the broker must present a publicly/enterprise-trusted certificate. Username/
Password are sent as MQTT credentials and should only be used over TLS.
Uid is auto-generated and persisted on first run to:
- Linux:
~/.local/state/ew2mqtt/uid - macOS:
~/Library/Application Support/ew2mqtt/uid - Windows:
%LOCALAPPDATA%\ew2mqtt\uid
MQTT topics
All topics are rooted at <TopicBase>/<InstanceId>, e.g. ew2mqtt/default.
Published (state, retained)
| Topic | Payload |
|---|---|
availability |
online / offline (LWT) |
connection |
{"state":"Connected","paired":true,...} |
state/logo |
ON / OFF |
state/black |
ON / OFF |
state/clear |
ON / OFF |
state/presentation/number |
<int> |
state/presentation/rowid |
<int> |
state/slide/number |
<int> |
state/slide/rowid |
<int> |
state/schedule/rev |
<int> |
state/live/rev |
<int> |
state/imagehash |
<hex> |
state/requestrev |
<int> |
state/slide/title |
song slide title (e.g. Verse 1) |
state/slide/content |
slide lyrics / text |
state/slide/index |
1-based position in current presentation |
state/slide/total |
total slides in current presentation |
state/presentation/title |
song / presentation title |
state/presentation/loaded |
true once every slide's slideInfo has arrived; false while still loading |
state/presentation/slides |
full song as JSON: {pres_rowid, liverev, slides:[{index,slide_rowid,title,content}, β¦]} β published when the presentation finishes loading |
state/schedule |
current EW schedule as JSON: {count, items:[{index,pres_rowid,revision,title}, β¦]} β fetched after pair and updated as song titles arrive |
state |
aggregate JSON |
Published (events, non-retained)
| Topic | Payload |
|---|---|
events/pairing |
{"paired":true} |
events/heartbeat |
{"requestrev":42,"ts":"β¦"} |
events/unknown |
{"action":"β¦","raw":"β¦"} |
events/slide-changed |
{"slide_rowid":β¦,"index":β¦,"total":β¦,"title":"β¦","content":"β¦"} (fires on every slide change) |
events/presentation-loaded |
{"pres_rowid":β¦,"liverev":β¦,"slide_count":N,"ts":"β¦"} (fires once per presentation after the final slideInfo lands) |
Subscribed (commands)
| Topic | Payload | Effect |
|---|---|---|
cmd/nextSlide / prevSlide |
empty | navigate |
cmd/gotoStartSlide / gotoStartPresentation |
empty | jump |
cmd/gotoSlide |
<int> or {"slide":N} |
jump |
cmd/gotoSchedule |
<int> or {"schedule":N} |
jump |
cmd/nextSchedule / prevSchedule |
empty | navigate |
cmd/nextBuild / prevBuild |
empty | step animations |
cmd/play / pause / toggle |
empty | media |
cmd/overlay/{logo,black,clear} |
ON/OFF/TOGGLE |
per-bit |
cmd/overlay |
{"logo":..,"black":..,"clear":..} |
atomic |
cmd/activate |
{"pres_rowid":N,"slide_rowid":M} or {"schedule_index":N,"slide_index":M} |
go live with a specific schedule item's slide |
cmd |
{"action":"...",...} |
envelope form |
Library usage
The protocol client is published as Pilgrim.EasyWorship on NuGet. mDNS discovery
is split into Pilgrim.EasyWorship.Discovery so consumers who only want manual
host:port can skip the Zeroconf dependency.
using Pilgrim.EasyWorship;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddLogging();
services.AddEasyWorshipClient(o =>
{
o.Host = "10.0.0.5";
o.Port = 50123; // EasyWorship's ezwremote port is dynamic β use the actual port
o.Uid = "your-stable-guid";
});
await using var sp = services.BuildServiceProvider();
var client = sp.GetRequiredService<IEasyWorshipClient>();
client.StatusReceived += (_, ev) => Console.WriteLine($"slide {ev.SlideNo}");
await client.StartAsync();
Building from source
Requires the .NET 10 SDK.
dotnet build
Run the tests (the suites use Microsoft.Testing.Platform;
on the .NET 10 SDK run them directly rather than via dotnet test):
dotnet run --project tests/Pilgrim.EasyWorship.Tests
dotnet run --project tests/Pilgrim.EasyWorship.Discovery.Tests
dotnet run --project tests/Pilgrim.EasyWorship.Mqtt.Tests
Run the service locally:
dotnet run --project src/Pilgrim.EasyWorship.Mqtt \
--Ew2Mqtt:Mqtt:Server=mqtt://localhost:1883 \
--Ew2Mqtt:EasyWorship:Host=10.0.0.5
Publish a self-contained binary:
dotnet publish src/Pilgrim.EasyWorship.Mqtt -c Release -r linux-x64 \
-p:PublishSingleFile=true -p:SelfContained=true -p:PublishTrimmed=true
Slide content & navigation
The bridge automatically fetches the live presentation list and per-slide content after pairing:
- After
paired, sendsGetLiveDatato learn the ordered slide list (slide_rowidrevisionfor each slide).
- For every slide it learns about, sends
getSlideInfoto fetch the slide title and lyric content; results are cached for the lifetime of the process. - On every
statusframe (i.e. slide change), republishesstate/slide/{title,content,index,total}and the song title understate/presentation/title. Also fires a single non-retainedevents/slide-changedevent with the same data merged into one JSON payload β the easiest single trigger for automations.
Notes:
- In live-area "double-click from library" flow, EW assigns synthetic negative
slide_rowidvalues (e.g.-100165, -100164β¦). Title/content lookup still works β EW responds togetSlideInfofor these IDs too. pres_noandslide_noon the raw status frame are always 0 in current EW 7 builds; they only populate when a Schedule is being run as a presentation proper. Usestate/slide/index(computed from LiveData) instead.- Lyrics are sent as-is to MQTT. If your broker is shared and you need to keep lyric content private, run a private broker or namespace the topic.
Risks & defaults
The two reverse-engineered references for the protocol disagree on a few details that may change between EasyWorship versions:
| Field | ew2vm | companion | ew2mqtt default |
|---|---|---|---|
device_type |
0 | 8 | 8 (configurable) |
| Heartbeat | 3 s | 30 s | 3 s (configurable) |
requestrev |
int | string | string in/out, parser accepts both |
EW 7 closes the TCP socket within milliseconds of pairing if heartbeats don't
arrive quickly enough β 30 s (Companion's default) is too slow. We default to
3 s (matching ew2vm). If you see clean pairings followed by immediate read loop ended (server likely closed connection) lines, drop the interval further
or check that heartbeats are reaching EW at all.
If your EasyWorship version pairs but never sends status updates, try
DeviceType=0. Reports welcome.
Credits
This project would not exist without the protocol reverse-engineering done by:
- mikenor/ew2vm (Python, EW β vMix)
- bitfocus/companion-module-softouch-easyworship (Bitfocus Companion module)
Legal & disclaimer
ew2mqtt is an independent, unofficial project. It is not affiliated with, authorized, sponsored, or endorsed by Softouch Development, Inc. or any EasyWorship entity. "EasyWorship" and any related marks are the property of their respective owners and are used here only nominatively, to describe what this software interoperates with.
The ezwremote protocol is undocumented; ew2mqtt's implementation was derived
solely to achieve interoperability between an independently created program
and EasyWorship β the kind of interoperability use protected under the EEA
Software Directive (2009/24/EC, as implemented in Norwegian Γ₯ndsverkloven
Β§ 41). This repository contains no EasyWorship source code, binaries, or
assets; the protocol notes and tests describe functional wire-format facts
only. ew2mqtt is provided "as is", without warranty, as set out in the
license.
License
MIT β Copyright (c) 2026 Pilgrim Softworks AS
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- System.IO.Pipelines (>= 10.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Pilgrim.EasyWorship:
| Package | Downloads |
|---|---|
|
Pilgrim.EasyWorship.Discovery
mDNS-based discovery for EasyWorship "ezwremote" services. Companion package to Pilgrim.EasyWorship. Not affiliated with or endorsed by Softouch Development, Inc.; "EasyWorship" is their trademark, used here only to describe interoperability. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.0 | 123 | 5/17/2026 |