Pilgrim.EasyWorship.Discovery 0.2.0

dotnet add package Pilgrim.EasyWorship.Discovery --version 0.2.0
                    
NuGet\Install-Package Pilgrim.EasyWorship.Discovery -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pilgrim.EasyWorship.Discovery" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pilgrim.EasyWorship.Discovery" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Pilgrim.EasyWorship.Discovery" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pilgrim.EasyWorship.Discovery --version 0.2.0
                    
#r "nuget: Pilgrim.EasyWorship.Discovery, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pilgrim.EasyWorship.Discovery@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pilgrim.EasyWorship.Discovery&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Pilgrim.EasyWorship.Discovery&version=0.2.0
                    
Install as a Cake Tool

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 manual Host:Port fallback.
  • Bidirectional: publishes status/paired/heartbeat events to MQTT and subscribes to a command topic for nextSlide, 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 (availability topic) 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, sends GetLiveData to learn the ordered slide list (slide_rowid
    • revision for each slide).
  • For every slide it learns about, sends getSlideInfo to fetch the slide title and lyric content; results are cached for the lifetime of the process.
  • On every status frame (i.e. slide change), republishes state/slide/{title,content,index,total} and the song title under state/presentation/title. Also fires a single non-retained events/slide-changed event 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_rowid values (e.g. -100165, -100164…). Title/content lookup still works β€” EW responds to getSlideInfo for these IDs too.
  • pres_no and slide_no on 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. Use state/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:

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 106 5/17/2026