Skip to content

Commit cb51a27

Browse files
committed
Documentation improvements
1 parent ce9e8ae commit cb51a27

4 files changed

Lines changed: 181 additions & 50 deletions

File tree

README.md

Lines changed: 167 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
# HapticWebPlugin
21

3-
A Logi Actions SDK plugin that exposes MX Master 4 haptic feedback via a local HTTP API, enabling external programs to trigger tactile waveforms.
2+
<h1 align="center">
3+
<br>
4+
<a href="https://haptics.jmw.nz">
5+
<img src="web-demo/static/og-image.png" alt="HapticWebPlugin" width="600" />
6+
</a>
7+
</h1>
8+
9+
10+
11+
A Logi Actions SDK plugin that exposes MX Master 4 haptic feedback via a local HTTPS server and WebSocket API.
412

513
## Requirements
614

@@ -10,64 +18,187 @@ A Logi Actions SDK plugin that exposes MX Master 4 haptic feedback via a local H
1018

1119
## API Server
1220

13-
The plugin runs an HTTP server on `http://127.0.0.1:8765/` when loaded.
21+
The plugin runs an HTTPS server on `https://local.jmw.nz:41443/` when loaded.
22+
23+
### About local.jmw.nz
24+
25+
`local.jmw.nz` resolves to `127.0.0.1`. The plugin uses this domain to serve HTTPS with a valid SSL certificate instead of self-signed certs that browsers reject. The certificate is automatically downloaded from GitHub, cached locally, and refreshed every 24 hours.
1426

15-
### Endpoints
27+
### REST API Endpoints
1628

1729
| Endpoint | Method | Description |
1830
|----------|--------|-------------|
1931
| `/` | GET | Health check with service info and available endpoints |
20-
| `/waveforms` | GET | List all 16 available haptic **waveforms** with descriptions |
32+
| `/waveforms` | GET | List all 15 available haptic waveforms |
2133
| `/haptic/{waveform}` | POST | Trigger a specific haptic waveform |
2234

35+
### WebSocket API
36+
37+
**Endpoint:** `wss://local.jmw.nz:41443/ws`
38+
39+
Keeps a warm connection for theoritically lower latency.
40+
41+
**Protocol:**
42+
- Send a single byte containing the waveform index (0-14)
43+
- No response is sent back
44+
- Connection stays open for repeated triggers
45+
2346
### Available Waveforms
2447

25-
| Waveform | Category |
26-
|----------|----------|
27-
| `sharp_collision` | Precision enhancers |
28-
| `sharp_state_change` | Progress indicators |
29-
| `knock` | Incoming events |
30-
| `damp_collision` | Precision enhancers |
31-
| `mad` | Progress indicators |
32-
| `ringing` | Incoming events |
33-
| `subtle_collision` | Precision enhancers |
34-
| `completed` | Progress indicators |
35-
| `jingle` | Incoming events |
36-
| `damp_state_change` | Precision enhancers |
37-
| `firework` | Progress indicators |
38-
| `happy_alert` | Progress indicators |
39-
| `wave` | Progress indicators |
40-
| `angry_alert` | Progress indicators |
41-
| `square` | Progress indicators |
48+
The plugin supports 15 haptic waveforms across different categories:
49+
50+
| Waveform | Index | Category |
51+
|----------|-------|----------|
52+
| `sharp_collision` | 0 | Precision enhancers |
53+
| `sharp_state_change` | 1 | Progress indicators |
54+
| `knock` | 2 | Incoming events |
55+
| `damp_collision` | 3 | Precision enhancers |
56+
| `mad` | 4 | Progress indicators |
57+
| `ringing` | 5 | Incoming events |
58+
| `subtle_collision` | 6 | Precision enhancers |
59+
| `completed` | 7 | Progress indicators |
60+
| `jingle` | 8 | Incoming events |
61+
| `damp_state_change` | 9 | Precision enhancers |
62+
| `firework` | 10 | Progress indicators |
63+
| `happy_alert` | 11 | Progress indicators |
64+
| `wave` | 12 | Progress indicators |
65+
| `angry_alert` | 13 | Progress indicators |
66+
| `square` | 14 | Progress indicators |
4267

4368
## Example Usage
4469

70+
### HTTP REST API
71+
4572
```bash
4673
# Health check
47-
curl http://127.0.0.1:8765/
74+
curl https://local.jmw.nz:41443/
4875

4976
# List all available waveforms
50-
curl http://127.0.0.1:8765/waveforms
77+
curl https://local.jmw.nz:41443/waveforms
78+
79+
# Trigger haptic feedback (note: -d '' is required to send Content-Length header)
80+
curl -X POST -d '' https://local.jmw.nz:41443/haptic/sharp_collision
81+
curl -X POST -d '' https://local.jmw.nz:41443/haptic/happy_alert
82+
curl -X POST -d '' https://local.jmw.nz:41443/haptic/completed
83+
# {
84+
# "success": true,
85+
# "waveform": "completed"
86+
# }
87+
88+
```
5189

52-
# Trigger haptic feedback
53-
curl -X POST http://127.0.0.1:8765/haptic/sharp_collision
54-
curl -X POST http://127.0.0.1:8765/haptic/happy_alert
55-
curl -X POST http://127.0.0.1:8765/haptic/completed
90+
> **Note:** POST requests require a `Content-Length` header. When using curl, include `-d ''` to send an empty body with the proper header otherwise the request will hang. I have some *thoughts* about this design choice by the otherwise great NetCoreServer library.
91+
92+
### WebSocket API (JavaScript)
93+
94+
```javascript
95+
// Connect to WebSocket
96+
const ws = new WebSocket('wss://local.jmw.nz:41443/ws');
97+
98+
ws.onopen = () => {
99+
console.log('Connected to HapticWeb');
100+
101+
// Trigger "sharp_collision" (index 0)
102+
ws.send(new Uint8Array([0]));
103+
104+
// Trigger "completed" (index 7)
105+
ws.send(new Uint8Array([7]));
106+
107+
// Trigger "happy_alert" (index 11)
108+
ws.send(new Uint8Array([11]));
109+
};
110+
111+
ws.onclose = () => console.log('Disconnected');
112+
ws.onerror = (err) => console.error('WebSocket error:', err);
56113
```
57114

58-
### Example Response
59115

60-
```json
61-
{
62-
"success": true,
63-
"waveform": "sharp_collision"
64-
}
116+
## Technical Details
117+
118+
### Architecture
119+
- **Server:** NetCoreServer-based HTTPS/WSS
120+
- **Port:** 41443 (HTTPS only)
121+
- **Certificate:** Auto-managed from GitHub
122+
- **CORS:** Enabled, any origin
123+
- **Binding:** `127.0.0.1` only
124+
125+
### Certificate Management
126+
- Downloads from the GitHub `certs` branch on first run
127+
- Cached locally in plugin data directory
128+
- Refreshes every 24 hours
129+
- Warns 14 days before expiry
130+
- Falls back to cache if GitHub is unreachable
131+
132+
## Manual Installation
133+
134+
1. Open Logitech Options+ and click on your MX Master 4.
135+
2. In the left sidebar, open the `HAPTIC FEEDBACK` tab and click on the haptic feedback settings popover.
136+
3. In the new right sidebar, click the `INSTALL AND UNINSTALL PLUGINS` button. Now you're in the plugin management window.
137+
4. Download the `HapticWeb.lplug4` release asset from [GitHub Releases](https://github.com/fallstop/HapticWebPlugin/releases) and double-click it.
138+
5. This will trigger an installation dialog in Logitech Options+. Press continue.
139+
6. The server starts automatically at `https://local.jmw.nz:41443/`
140+
7. Test at [https://haptics.jmw.nz](https://haptics.jmw.nz)
141+
142+
### Uninstalling
143+
A manually installed plugin doesn't show in the plugin list, so to uninstall, simply delete the plugin folder:
144+
145+
```bash
146+
# Windows
147+
C:\Users\USERNAME\AppData\Local\Logi\LogiPluginService\Plugins\HapticWebPlugin\
148+
149+
# macOS
150+
~/Library/Application Support/Logi/LogiPluginService/Plugins/HapticWebPlugin/
65151
```
66152

67-
## Building
153+
## Development
68154

155+
### Building
69156
```bash
157+
# Debug build (auto-reloads in Logi Plugin Service)
70158
dotnet build -c Debug
159+
160+
# Release build
161+
dotnet build -c Release
162+
```
163+
164+
### Packaging & Distribution
165+
```bash
166+
# Create plugin package
167+
logiplugintool pack ./bin/Release ./HapticWeb.lplug4
168+
169+
# Install locally
170+
logiplugintool install ./HapticWeb.lplug4
171+
172+
# Uninstall
173+
logiplugintool uninstall HapticWeb
71174
```
72175

73-
The plugin auto-reloads after building if the Logi Plugin Service is running.
176+
### Testing
177+
- **Manual testing:** Use the demo site at [https://haptics.jmw.nz](https://haptics.jmw.nz)
178+
- **CLI testing:** Use curl commands shown in [Example Usage](#example-usage)
179+
- **Live reload:** Debug builds auto-reload when the plugin is rebuilt
180+
181+
## Troubleshooting
182+
183+
### Certificate Issues
184+
If you see certificate errors:
185+
1. Check plugin status in Logi Options+ (should show green "Normal")
186+
2. Verify internet connection (initial certificate download requires network)
187+
3. Restart Logi Plugin Service
188+
4. Report an issue with the plugin logs if the problem persists
189+
190+
### Port Already in Use
191+
If port 41443 is in use:
192+
1. Stop other services using the port: `lsof -ti:41443 | xargs kill -9` (macOS/Linux)
193+
2. Restart Logi Plugin Service
194+
195+
### WebSocket Connection Failed
196+
If WebSocket connections fail:
197+
1. Verify the server is running: `curl https://local.jmw.nz:41443/`
198+
2. Check browser console for errors
199+
3. Ensure you're using `wss://` (not `ws://`)
200+
4. Verify `local.jmw.nz` resolves to `127.0.0.1`: `ping local.jmw.nz`
201+
202+
## MIT License
203+
204+
See [LICENSE.md](LICENSE.md) for details.

src/HapticWebPlugin.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private async Task InitializeServerAsync()
8989
catch (Exception ex)
9090
{
9191
PluginLog.Error(ex, "Failed to initialize server");
92-
this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, $"Server initialization failed: {ex.Message}");
92+
this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, "Server failed to start. Try restarting the plugin.");
9393
}
9494
}
9595

@@ -105,16 +105,16 @@ private void UpdatePluginStatus()
105105
this.OnPluginStatusChanged(
106106
Loupedeck.PluginStatus.Warning,
107107
this._certificateManager.StatusMessage,
108-
"https://github.com/fallstop/HapticWebPlugin/actions",
109-
"Check GitHub Actions");
108+
"https://github.com/fallstop/HapticWebPlugin/issues",
109+
"Report Issue");
110110
break;
111111

112112
case CertificateStatus.ExpiringSoon:
113113
this.OnPluginStatusChanged(
114114
Loupedeck.PluginStatus.Warning,
115115
this._certificateManager.StatusMessage,
116-
"https://github.com/fallstop/HapticWebPlugin/actions",
117-
"Check GitHub Actions");
116+
null,
117+
null);
118118
break;
119119

120120
case CertificateStatus.Valid:

src/Helpers/CertificateManager.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public async Task<Boolean> InitializeAsync()
5454
if (!downloaded && !this.HasCachedCertificate())
5555
{
5656
this.Status = CertificateStatus.Error;
57-
this.StatusMessage = "No cached certificate and unable to download from GitHub. Check internet connection.";
57+
this.StatusMessage = "Can't download SSL certificate. Check your internet connection and try restarting the plugin.";
5858
PluginLog.Error(this.StatusMessage);
5959
return false;
6060
}
@@ -66,7 +66,7 @@ public async Task<Boolean> InitializeAsync()
6666
{
6767
PluginLog.Error(ex, "Failed to initialize certificate manager");
6868
this.Status = CertificateStatus.Error;
69-
this.StatusMessage = $"Certificate initialization failed: {ex.Message}";
69+
this.StatusMessage = $"SSL certificate setup failed. Try restarting the plugin.";
7070
return false;
7171
}
7272
}
@@ -233,7 +233,7 @@ private Boolean LoadCachedCertificate()
233233
if (!File.Exists(certPath))
234234
{
235235
this.Status = CertificateStatus.Error;
236-
this.StatusMessage = "Certificate file not found in cache";
236+
this.StatusMessage = "SSL certificate not found. Check your internet connection and restart the plugin.";
237237
return false;
238238
}
239239

@@ -245,7 +245,7 @@ private Boolean LoadCachedCertificate()
245245
if (DateTime.UtcNow > this.CertificateExpiry)
246246
{
247247
this.Status = CertificateStatus.Expired;
248-
this.StatusMessage = $"SSL certificate expired on {this.CertificateExpiry:yyyy-MM-dd}. HTTPS may not work correctly.";
248+
this.StatusMessage = $"SSL certificate expired. Restart the plugin to download a new one.";
249249
PluginLog.Warning(this.StatusMessage);
250250
return true;
251251
}
@@ -254,14 +254,14 @@ private Boolean LoadCachedCertificate()
254254
if (daysUntilExpiry <= CertExpiryWarningDays)
255255
{
256256
this.Status = CertificateStatus.ExpiringSoon;
257-
this.StatusMessage = $"SSL certificate expires in {(Int32)daysUntilExpiry} days ({this.CertificateExpiry:yyyy-MM-dd})";
257+
this.StatusMessage = $"SSL certificate expires in {(Int32)daysUntilExpiry} days. It should have auto-renwed by now.";
258258
PluginLog.Warning(this.StatusMessage);
259259
return true;
260260
}
261261

262262
this.Status = CertificateStatus.Valid;
263-
this.StatusMessage = $"SSL certificate valid until {this.CertificateExpiry:yyyy-MM-dd}";
264-
PluginLog.Info(this.StatusMessage);
263+
this.StatusMessage = null;
264+
PluginLog.Info($"SSL certificate valid until {this.CertificateExpiry:yyyy-MM-dd}");
265265
return true;
266266
}
267267
catch (Exception ex)

src/Helpers/HttpsServer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void Start()
4949

5050
if (this._certificate == null)
5151
{
52-
this._bindError = "No SSL certificate available";
52+
this._bindError = "Server can't start without SSL certificate";
5353
PluginLog.Error(this._bindError);
5454
return;
5555
}
@@ -80,7 +80,7 @@ public void Start()
8080
ex.SocketErrorCode == SocketError.AddressAlreadyInUse ||
8181
ex.SocketErrorCode == SocketError.AccessDenied)
8282
{
83-
this._bindError = $"Port {this._httpsPort} is already in use or access denied";
83+
this._bindError = $"Port {this._httpsPort} is already in use. Close any other apps using this port.";
8484
PluginLog.Error(ex, this._bindError);
8585
}
8686
catch (Exception ex)

0 commit comments

Comments
 (0)