@marineyachtradar/signalk-playback-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MarineYachtRadar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # MaYaRa Radar Playback Plugin for SignalK
2
+
3
+ A SignalK plugin for playing back `.mrr` radar recordings through the SignalK Radar API.
4
+
5
+ ## What This Plugin Does
6
+
7
+ This plugin allows you to play pre-recorded radar data (`.mrr` files) through SignalK's Radar API. During playback, the recording appears as a virtual radar that any SignalK radar consumer can connect to and display.
8
+
9
+ **Use cases:**
10
+ - Test and develop SignalK radar display applications without live radar hardware
11
+ - Demo radar functionality at exhibitions or presentations
12
+ - Debug radar rendering code with consistent, repeatable data
13
+ - Share interesting radar captures with other developers
14
+
15
+ ## Installation
16
+
17
+ Install from the **SignalK App Store**:
18
+
19
+ 1. Open your SignalK server web interface
20
+ 2. Go to **Appstore** > **Available**
21
+ 3. Search for "MaYaRa Radar Playback"
22
+ 4. Click **Install**
23
+ 5. Restart SignalK when prompted
24
+
25
+ ## Getting Started
26
+
27
+ ### 1. Access the Playback Interface
28
+
29
+ After installation, navigate to:
30
+ ```
31
+ http://your-signalk-server:3000/plugins/@marineyachtradar/signalk-playback-plugin/playback.html
32
+ ```
33
+
34
+ Or find it in SignalK's **Webapps** menu.
35
+
36
+ ### 2. Upload a Recording
37
+
38
+ You can upload `.mrr` or `.mrr.gz` files:
39
+ - **Drag and drop** a file onto the upload zone
40
+ - Or **click** the upload zone to browse for a file
41
+
42
+ Recordings are stored on the SignalK server and persist across restarts.
43
+
44
+ ### 3. Load and Play
45
+
46
+ 1. Select a recording from the list
47
+ 2. Click **Load** to prepare it for playback
48
+ 3. Click **Play** to start playback
49
+ 4. Use **Pause** and **Stop** as needed
50
+ 5. Enable **Loop** to repeat the recording continuously
51
+
52
+ ### 4. View the Radar
53
+
54
+ Click **View Radar** to open the radar display. This shows the playback radar using the built-in viewer.
55
+
56
+ ## Viewing with Other Clients
57
+
58
+ During playback, the recording registers as a virtual radar in SignalK. The radar ID follows the pattern `playback-{filename}`.
59
+
60
+ ### Using mayara-server-signalk-plugin
61
+
62
+ If you have the **mayara-server-signalk-plugin** installed (the main MaYaRa radar plugin), you can also view playback recordings through its interface:
63
+
64
+ 1. Start playback in this plugin
65
+ 2. Open the mayara-server-signalk-plugin's radar viewer
66
+ 3. The playback radar will appear in the radar list
67
+ 4. Select it to view the recording with full MaYaRa GUI features
68
+
69
+ ### Using Other SignalK Radar Consumers
70
+
71
+ Any application that implements the SignalK Radar API can display the playback:
72
+ - The radar appears at `/signalk/v2/api/vessels/self/radars/playback-{filename}`
73
+ - Spoke data streams via SignalK's binary WebSocket
74
+
75
+ ## Obtaining Recording Files
76
+
77
+ Recording files (`.mrr`) are created by **mayara-server** when connected to a live radar:
78
+
79
+ 1. Run mayara-server with a radar connected
80
+ 2. Open the recordings page at `http://localhost:6502/recordings.html`
81
+ 3. Select a radar and click **Start Recording**
82
+ 4. Click **Stop Recording** when done
83
+ 5. Download the recording as `.mrr.gz` (compressed)
84
+ 6. Upload to this SignalK plugin
85
+
86
+ ## File Format
87
+
88
+ - `.mrr` - MaYaRa Radar Recording (uncompressed)
89
+ - `.mrr.gz` - Gzip-compressed recording (~95% smaller for transfer)
90
+
91
+ Both formats are supported for upload. Files are stored uncompressed on the server for fast playback.
92
+
93
+ ## Troubleshooting
94
+
95
+ **Recording won't load:**
96
+ - Check the SignalK server logs for errors
97
+ - Ensure the file is a valid `.mrr` or `.mrr.gz` file
98
+ - Verify the file wasn't corrupted during transfer
99
+
100
+ **No radar appears in SignalK:**
101
+ - Make sure playback is started (not just loaded)
102
+ - Refresh the radar consumer application
103
+ - Check that SignalK's Radar API is enabled
104
+
105
+ **Playback stutters:**
106
+ - This can happen on slower systems with large recordings
107
+ - Try using recordings with fewer spokes per revolution
108
+
109
+ ## Technical Details
110
+
111
+ This plugin:
112
+ - Reads `.mrr` files directly (no mayara-server required)
113
+ - Registers as a RadarProvider via SignalK's Radar API
114
+ - Emits spoke frames through SignalK's `binaryStreamManager`
115
+ - Plays back frames at their original recorded timing
116
+ - Sets power status to "transmit" so GUI shows radar as active (not STANDBY)
117
+ - Pre-loads all frames for accurate timing
118
+ - Auto-stops current playback when loading a different file
119
+
120
+ ## Development
121
+
122
+ Build options:
123
+
124
+ ```bash
125
+ # Build with GUI from npm (default)
126
+ node build.js
127
+
128
+ # Build with local mayara-gui (for development)
129
+ node build.js --local-gui
130
+
131
+ # Create tarball for manual install (includes public/)
132
+ node build.js --local-gui --pack
133
+ ```
134
+
135
+ The `--local-gui` option copies GUI files from the sibling `../mayara-gui` directory instead of from npm.
136
+
137
+ The `--pack` option creates a `.tgz` tarball with `public/` included (normally excluded by `.npmignore`). Install with:
138
+
139
+ ```bash
140
+ npm install /path/to/marineyachtradar-signalk-playback-plugin-0.1.0.tgz
141
+ ```
142
+
143
+ ## Related Projects
144
+
145
+ - **[mayara-server](https://github.com/MaYaRa-Marine/mayara-server)** - Standalone radar server (creates recordings)
146
+ - **[mayara-server-signalk-plugin](https://github.com/MaYaRa-Marine/mayara-server-signalk-plugin)** - SignalK plugin for live radar (connects to mayara-server)
147
+
148
+ ## License
149
+
150
+ MIT License - See [LICENSE](LICENSE) for details.
package/build.js ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script for mayara-server-signalk-playbackrecordings-plugin
4
+ *
5
+ * Copies @marineyachtradar/mayara-gui from npm to public/
6
+ * The plugin adds its own playback.html for file upload/control.
7
+ *
8
+ * Usage: node build.js [options]
9
+ * --local-gui Use local mayara-gui instead of npm (for development)
10
+ * --pack Create a .tgz tarball with public/ included (for manual install)
11
+ */
12
+
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+ const { execSync } = require('child_process')
16
+
17
+ const args = process.argv.slice(2)
18
+ const useLocalGui = args.includes('--local-gui')
19
+ const createPack = args.includes('--pack')
20
+
21
+ const scriptDir = __dirname
22
+ const publicDest = path.join(scriptDir, 'public')
23
+
24
+ /**
25
+ * Recursively copy directory contents
26
+ */
27
+ function copyDir(src, dest) {
28
+ if (!fs.existsSync(src)) {
29
+ console.error(`Source directory not found: ${src}`)
30
+ process.exit(1)
31
+ }
32
+
33
+ if (fs.existsSync(dest)) {
34
+ fs.rmSync(dest, { recursive: true })
35
+ }
36
+
37
+ fs.mkdirSync(dest, { recursive: true })
38
+
39
+ const entries = fs.readdirSync(src, { withFileTypes: true })
40
+ for (const entry of entries) {
41
+ const srcPath = path.join(src, entry.name)
42
+ const destPath = path.join(dest, entry.name)
43
+ if (entry.isDirectory()) {
44
+ copyDir(srcPath, destPath)
45
+ } else {
46
+ fs.copyFileSync(srcPath, destPath)
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Copy GUI from npm package
53
+ */
54
+ function setupGuiFromNpm() {
55
+ console.log('Copying GUI from node_modules...\n')
56
+
57
+ const guiSource = path.join(scriptDir, 'node_modules', '@marineyachtradar', 'mayara-gui')
58
+
59
+ if (!fs.existsSync(guiSource)) {
60
+ console.error('Error: @marineyachtradar/mayara-gui not found in node_modules')
61
+ console.error('Make sure it is listed in package.json dependencies')
62
+ process.exit(1)
63
+ }
64
+
65
+ if (fs.existsSync(publicDest)) {
66
+ fs.rmSync(publicDest, { recursive: true })
67
+ }
68
+ fs.mkdirSync(publicDest, { recursive: true })
69
+
70
+ // Copy GUI files (exclude package.json, node_modules, etc.)
71
+ const guiPatterns = [
72
+ { ext: '.html' },
73
+ { ext: '.js' },
74
+ { ext: '.css' },
75
+ { ext: '.ico' },
76
+ { ext: '.svg' },
77
+ { dir: 'assets' },
78
+ { dir: 'proto' },
79
+ { dir: 'protobuf' }
80
+ ]
81
+
82
+ // Exclude recordings.html since we have our own playback UI
83
+ const excludeFiles = ['recordings.html', 'recordings.js', 'recordings.css']
84
+
85
+ const entries = fs.readdirSync(guiSource, { withFileTypes: true })
86
+ for (const entry of entries) {
87
+ if (excludeFiles.includes(entry.name)) continue
88
+
89
+ const srcPath = path.join(guiSource, entry.name)
90
+ const destPath = path.join(publicDest, entry.name)
91
+
92
+ if (entry.isDirectory()) {
93
+ if (guiPatterns.some(p => p.dir === entry.name)) {
94
+ copyDir(srcPath, destPath)
95
+ }
96
+ } else {
97
+ if (guiPatterns.some(p => p.ext && entry.name.endsWith(p.ext))) {
98
+ fs.copyFileSync(srcPath, destPath)
99
+ }
100
+ }
101
+ }
102
+
103
+ // Copy our custom files from plugin/public/ (overrides GUI files)
104
+ const pluginPublic = path.join(scriptDir, 'plugin', 'public')
105
+ if (fs.existsSync(pluginPublic)) {
106
+ const customEntries = fs.readdirSync(pluginPublic, { withFileTypes: true })
107
+ let customCount = 0
108
+ for (const entry of customEntries) {
109
+ const srcPath = path.join(pluginPublic, entry.name)
110
+ const destPath = path.join(publicDest, entry.name)
111
+ if (entry.isDirectory()) {
112
+ copyDir(srcPath, destPath)
113
+ customCount++
114
+ } else {
115
+ fs.copyFileSync(srcPath, destPath)
116
+ customCount++
117
+ }
118
+ }
119
+ console.log(`Added ${customCount} custom files from plugin/public/`)
120
+ }
121
+
122
+ const fileCount = fs.readdirSync(publicDest, { recursive: true }).length
123
+ console.log(`Copied ${fileCount} GUI files to public/\n`)
124
+ }
125
+
126
+ /**
127
+ * Copy GUI from local sibling directory (for development)
128
+ */
129
+ function setupGuiFromLocal() {
130
+ const localGuiPath = path.join(scriptDir, '..', 'mayara-gui')
131
+ console.log(`Copying GUI from local ${localGuiPath}...\n`)
132
+
133
+ if (fs.existsSync(publicDest)) {
134
+ fs.rmSync(publicDest, { recursive: true })
135
+ }
136
+ fs.mkdirSync(publicDest, { recursive: true })
137
+
138
+ const guiPatterns = [
139
+ { ext: '.html' },
140
+ { ext: '.js' },
141
+ { ext: '.css' },
142
+ { ext: '.ico' },
143
+ { ext: '.svg' },
144
+ { dir: 'assets' },
145
+ { dir: 'proto' },
146
+ { dir: 'protobuf' }
147
+ ]
148
+
149
+ // Exclude recordings files since we have our own playback UI
150
+ const excludeFiles = ['recordings.html', 'recordings.js', 'recordings.css']
151
+
152
+ const entries = fs.readdirSync(localGuiPath, { withFileTypes: true })
153
+ for (const entry of entries) {
154
+ if (excludeFiles.includes(entry.name)) continue
155
+
156
+ const srcPath = path.join(localGuiPath, entry.name)
157
+ const destPath = path.join(publicDest, entry.name)
158
+
159
+ if (entry.isDirectory()) {
160
+ if (guiPatterns.some(p => p.dir === entry.name)) {
161
+ copyDir(srcPath, destPath)
162
+ }
163
+ } else {
164
+ if (guiPatterns.some(p => p.ext && entry.name.endsWith(p.ext))) {
165
+ fs.copyFileSync(srcPath, destPath)
166
+ }
167
+ }
168
+ }
169
+
170
+ // Copy our custom files from plugin/public/ (overrides GUI files)
171
+ const pluginPublic = path.join(scriptDir, 'plugin', 'public')
172
+ if (fs.existsSync(pluginPublic)) {
173
+ const customEntries = fs.readdirSync(pluginPublic, { withFileTypes: true })
174
+ let customCount = 0
175
+ for (const entry of customEntries) {
176
+ const srcPath = path.join(pluginPublic, entry.name)
177
+ const destPath = path.join(publicDest, entry.name)
178
+ if (entry.isDirectory()) {
179
+ copyDir(srcPath, destPath)
180
+ customCount++
181
+ } else {
182
+ fs.copyFileSync(srcPath, destPath)
183
+ customCount++
184
+ }
185
+ }
186
+ console.log(`Added ${customCount} custom files from plugin/public/`)
187
+ }
188
+
189
+ const fileCount = fs.readdirSync(publicDest, { recursive: true }).length
190
+ console.log(`Copied ${fileCount} files from local mayara-gui/ to public/\n`)
191
+ }
192
+
193
+ function main() {
194
+ console.log('=== MaYaRa SignalK Playback Plugin Build ===\n')
195
+
196
+ // Skip build if public/ already exists with content (installed from --pack tarball)
197
+ // This prevents postinstall from failing when mayara-gui isn't in node_modules
198
+ if (fs.existsSync(publicDest) && !useLocalGui && !createPack) {
199
+ const files = fs.readdirSync(publicDest)
200
+ if (files.length > 0) {
201
+ console.log(`public/ already exists with ${files.length} files (installed from tarball)`)
202
+ console.log('Skipping build.\n')
203
+ console.log('=== Build complete ===')
204
+ return
205
+ }
206
+ }
207
+
208
+ if (useLocalGui) {
209
+ setupGuiFromLocal()
210
+ } else {
211
+ setupGuiFromNpm()
212
+ }
213
+
214
+ // Create tarball if --pack flag is set
215
+ if (createPack) {
216
+ console.log('Creating tarball with public/ included...\n')
217
+
218
+ // Temporarily remove public/ from .npmignore
219
+ const npmignorePath = path.join(scriptDir, '.npmignore')
220
+ const npmignoreContent = fs.readFileSync(npmignorePath, 'utf8')
221
+ const npmignoreWithoutPublic = npmignoreContent.replace(/^public\/\n?/m, '')
222
+ fs.writeFileSync(npmignorePath, npmignoreWithoutPublic)
223
+
224
+ // Also temporarily add public/ to files in package.json
225
+ const pkgPath = path.join(scriptDir, 'package.json')
226
+ const pkgContent = fs.readFileSync(pkgPath, 'utf8')
227
+ const pkg = JSON.parse(pkgContent)
228
+ const originalFiles = [...pkg.files]
229
+ pkg.files.push('public/**/*')
230
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
231
+
232
+ try {
233
+ // Run npm pack
234
+ execSync('npm pack', { stdio: 'inherit', cwd: scriptDir })
235
+ console.log('\nTarball created successfully!')
236
+ } finally {
237
+ // Restore .npmignore
238
+ fs.writeFileSync(npmignorePath, npmignoreContent)
239
+ // Restore package.json
240
+ pkg.files = originalFiles
241
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
242
+ }
243
+ }
244
+
245
+ console.log('\n=== Build complete ===')
246
+ }
247
+
248
+ main()
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@marineyachtradar/signalk-playback-plugin",
3
+ "version": "0.1.0",
4
+ "description": "MaYaRa Radar Playback - Play .mrr radar recordings through SignalK Radar API (Developer Tool)",
5
+ "main": "plugin/index.js",
6
+ "scripts": {
7
+ "build": "node build.js",
8
+ "postinstall": "node build.js",
9
+ "test": "echo \"No tests yet\" && exit 0"
10
+ },
11
+ "keywords": [
12
+ "signalk-node-server-plugin",
13
+ "signalk-webapp",
14
+ "signalk-category-instruments",
15
+ "radar",
16
+ "marine",
17
+ "mayara",
18
+ "playback",
19
+ "recording"
20
+ ],
21
+ "signalk": {
22
+ "displayName": "MaYaRa Radar Playback",
23
+ "appIcon": "assets/MaYaRa_RED.png",
24
+ "webapp": {
25
+ "name": "MaYaRa Radar Playback",
26
+ "description": "Play .mrr radar recordings for testing SignalK Radar API consumers",
27
+ "location": "/plugins/@marineyachtradar/signalk-playback-plugin/"
28
+ }
29
+ },
30
+ "files": [
31
+ "plugin/**/*",
32
+ "build.js"
33
+ ],
34
+ "engines": {
35
+ "signalk": ">=2.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@marineyachtradar/mayara-gui": "^0.6.0"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/MarineYachtRadar/mayara-server-signalk-playbackrecordings-plugin"
43
+ },
44
+ "author": "MarineYachtRadar Contributors",
45
+ "license": "Apache-2.0"
46
+ }