@rhizomatics/signalk-einklabel-plugin 0.6.2 → 0.6.4
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/CHANGELOG.md +10 -0
- package/README.md +23 -2
- package/dist/cli/index.js +5 -4
- package/dist/cli/liveContext.js +11 -2
- package/dist/devices/zhsunyco/index.js +15 -4
- package/dist/plugin.js +2 -1
- package/dist/pluginVersion.d.ts +6 -0
- package/dist/pluginVersion.js +11 -0
- package/dist/render/svgRenderer.js +3 -0
- package/dist/repaintScheduler.js +11 -3
- package/package.json +1 -1
- package/templates/tide.svg +27 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# 0.6.4
|
|
2
|
+
|
|
3
|
+
- Log improvements for device paint requests
|
|
4
|
+
- CLI scan now shows `hwid`, and adds reason for failing to retrieve `battery` level
|
|
5
|
+
|
|
6
|
+
# 0.6.3
|
|
7
|
+
|
|
8
|
+
- Improve logging if an image cannot be found in an `assets` directory to match a SignalK path value
|
|
9
|
+
- Expose `plugin_version` as a path for the `einklabel` source
|
|
10
|
+
|
|
1
11
|
# 0.6.2
|
|
2
12
|
|
|
3
13
|
- Auto default SignalK URL if `-u` or `-e` not passed as CLI arguments
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/prettier/prettier)
|
|
8
8
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
** Fully working but limited vendor/product support **
|
|
10
|
+
** Fully working but limited vendor/product support and requires Linux for device access **
|
|
11
11
|
|
|
12
12
|
A SignalK plugin to display data from SignalK paths, APIs and plugins on Electronic Shelf Labels (ESL) over a Bluetooth Low Energy (BLE) connection using simple SVG templates.
|
|
13
13
|
|
|
@@ -25,7 +25,9 @@ Unlike some eInk projects, this plugin doesn't require any physical modification
|
|
|
25
25
|
|
|
26
26
|
Most of these requirements are about making SignalK work with Bluetooth Low Energy, which is good thing to have anyway, since vendors like Victron, Switchbot, Ruuvi and others have BLE enabled hardware that's useful to have on a boat. [Direct BLE support](https://github.com/SignalK/signalk-server/issues/2411) in SignalK is being planned in 2026.
|
|
27
27
|
|
|
28
|
-
1. A SignalK server,
|
|
28
|
+
1. A SignalK server, **running Linux**
|
|
29
|
+
- MacOS and Windows aren't supported by the [BLE interface layer](https://www.npmjs.com/package/@naugehyde/node-ble), however can be used for template development and
|
|
30
|
+
debugging (everything except `scan` and `paint`)
|
|
29
31
|
2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy), which is Bluetooth v4.0 or higher
|
|
30
32
|
|
|
31
33
|
- Bluetooth adapters for Linux can be tricky, TP-Link UB400 and Asus USB-BT500 are two well-known and available ones
|
|
@@ -73,6 +75,24 @@ The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk
|
|
|
73
75
|
To show the lunar phase, the `environment.moon.phaseName` path is required, which can
|
|
74
76
|
be easily achieved by installing and configuring the `derived-data` plugin.
|
|
75
77
|
|
|
78
|
+
## Scanning for Devices
|
|
79
|
+
|
|
80
|
+
Since these are ultra-low power devices, they don't respond instantly to either identify themselves or accept a new image. By default, both scanning and painting have time-outs to wait for a response, which can be altered in the plugin configuration or CLI argument.
|
|
81
|
+
|
|
82
|
+
One other quirk is that some devices respond with a different name at different times, for example the genric `WOESL` sometimes and model specific `WL17500C74` other times. However, the MAC address, e.g. `66:66:17:50:0D:2B` is constant, and this is what's tracked by the plugin.
|
|
83
|
+
|
|
84
|
+
The plugin will optionally re-scan whenever it starts up, although this isn't essential once a label has been configured.
|
|
85
|
+
|
|
86
|
+
### Scans from CLI
|
|
87
|
+
|
|
88
|
+
The command line tools, run from inside the `.signalk` directory, can be used to help troubleshoot
|
|
89
|
+
|
|
90
|
+
- Scan for longer, in this example 90 seconds
|
|
91
|
+
- ```npx esl-cli scan -d 90```
|
|
92
|
+
- Scan for all BLE devices, whatever they are
|
|
93
|
+
- ```npx esl-cli scan -a```
|
|
94
|
+
|
|
95
|
+
|
|
76
96
|
## Templating
|
|
77
97
|
|
|
78
98
|
Templates are simply SVG files, to which expressions can be added to use SignalK data, with options to make it easier to read, like rounding or simplifying dates and times. The template can have sample data in the placeholder, so is easy to layout and visualize.
|
|
@@ -95,6 +115,7 @@ The source can be overridden to use the SignalK server's Resources API instead.
|
|
|
95
115
|
|
|
96
116
|
- `path=repainted` - the timestamp of the current repaint - for example `source=einklabel,path=repainted,format=local_datetime_short` to show when the label was last updated.
|
|
97
117
|
- `path=local_zone` - a short zone name (e.g. `BST`) for the same timezone used for `local_time`/`day_mon`/`local_datetime_short` (see above) - a fallback for `environment.time.timezoneRegion,format=utc_offset` on installs that never publish that path, since it needs no SignalK metadata of its own. Falls back to a plain UTC offset like `GMT+1` where the host's locale has no real abbreviation for the zone.
|
|
118
|
+
- `path=plugin_version` - expose the version of the eInk Label plugin itself.
|
|
98
119
|
|
|
99
120
|
#### Customizing Output
|
|
100
121
|
|
package/dist/cli/index.js
CHANGED
|
@@ -114,7 +114,7 @@ program
|
|
|
114
114
|
.option('-a, --all-devices', "list every nearby BLE device, not just ones a registered driver recognised - unmatched devices show address/name/mfr/rssi only, since there's no driver to do a vendor-specific read like battery")
|
|
115
115
|
.action(async (opts) => {
|
|
116
116
|
const durationMs = Number(opts.duration) * 1000;
|
|
117
|
-
const header = ['vendor', 'address', 'name', 'pid', 'label', 'mfr', 'battery', 'rssi'];
|
|
117
|
+
const header = ['vendor', 'address', 'name', 'pid', 'hwid', 'label', 'mfr', 'battery', 'rssi'];
|
|
118
118
|
const rows = [];
|
|
119
119
|
let matchedCount = 0;
|
|
120
120
|
const drivers = (0, registry_1.allDrivers)();
|
|
@@ -129,7 +129,7 @@ program
|
|
|
129
129
|
.getRSSI()
|
|
130
130
|
.then((value) => (value === undefined ? undefined : Number(value)))
|
|
131
131
|
.catch(() => undefined);
|
|
132
|
-
rows.push(['(unmatched)', address, name ?? '', '', '', mfr, '', String(rssi ?? '')]);
|
|
132
|
+
rows.push(['(unmatched)', address, name ?? '', '', '', '', mfr, '', String(rssi ?? '')]);
|
|
133
133
|
}
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
@@ -137,9 +137,10 @@ program
|
|
|
137
137
|
const found = await driver.identifyDevice(device, address, name, manufacturerId, manufacturerData);
|
|
138
138
|
(0, log_1.logDebug)(`${driver.vendor}: identified ${found.name ?? found.address}`);
|
|
139
139
|
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, '0')}` : '';
|
|
140
|
+
const hwid = found.hwVersion ? `0x${found.hwVersion}` : '';
|
|
140
141
|
const label = found.metadata?.label ?? '';
|
|
141
142
|
const battery = found.batteryMv !== undefined ? `${found.batteryMv}mV` : '';
|
|
142
|
-
rows.push([driver.vendor, found.address, found.name ?? '', pid, label, mfr, battery, String(found.rssi ?? '')]);
|
|
143
|
+
rows.push([driver.vendor, found.address, found.name ?? '', pid, hwid, label, mfr, battery, String(found.rssi ?? '')]);
|
|
143
144
|
});
|
|
144
145
|
});
|
|
145
146
|
if (matchedCount === 0) {
|
|
@@ -192,7 +193,7 @@ program
|
|
|
192
193
|
const connectTimeoutMs = Number(opts.connectTimeout) * 1000;
|
|
193
194
|
await (0, bleDiscovery_1.withRetries)(Number(opts.retries), async (attempt) => {
|
|
194
195
|
if (attempt > 1) {
|
|
195
|
-
(0, log_1.logDebug)(`paint
|
|
196
|
+
(0, log_1.logDebug)(`Attempting paint ${attempt}/${opts.retries}`);
|
|
196
197
|
}
|
|
197
198
|
await driver.paint(bitmap, { address: opts.address, aesKey: opts.aesKey, modelOverride, connectTimeoutMs });
|
|
198
199
|
});
|
package/dist/cli/liveContext.js
CHANGED
|
@@ -11,6 +11,7 @@ const formatters_1 = require("../render/formatters");
|
|
|
11
11
|
const httpJson_1 = require("../httpJson");
|
|
12
12
|
const unitCategories_1 = require("../unitCategories");
|
|
13
13
|
const pathMeta_1 = require("../pathMeta");
|
|
14
|
+
const pluginVersion_1 = require("../pluginVersion");
|
|
14
15
|
const log_1 = require("./log");
|
|
15
16
|
const RESOURCES_API_PATH = '/signalk/v2/api/resources';
|
|
16
17
|
/** `context=self` -> `vessels/self`, `context=vessels.urn:mrn:imo:mmsi:1` -> `vessels/urn:mrn:imo:mmsi:1` - matches the REST path for that context's full-data-model subtree. */
|
|
@@ -78,7 +79,11 @@ async function assembleLiveContext(signalkUrl, bindings) {
|
|
|
78
79
|
const categories = await (0, unitCategories_1.fetchCategoryDisplayUnits)(signalkUrl, categoryNames);
|
|
79
80
|
// Matches `considerRepaint` in repaintScheduler.ts - the CLI has no real device repaint to time, so
|
|
80
81
|
// a `source=einklabel,path=repainted` binding just resolves to "now", same as a live render would.
|
|
81
|
-
const meta = {
|
|
82
|
+
const meta = {
|
|
83
|
+
repainted: new Date().toISOString(),
|
|
84
|
+
local_zone: (0, formatters_1.resolveLocalZoneAbbreviation)({ signalk }),
|
|
85
|
+
plugin_version: pluginVersion_1.PLUGIN_VERSION,
|
|
86
|
+
};
|
|
82
87
|
return { signalk, resources, pathMeta, categories, meta };
|
|
83
88
|
}
|
|
84
89
|
async function readJsonFile(filePath) {
|
|
@@ -147,6 +152,10 @@ async function assembleExampleContext(examplesDir, bindings) {
|
|
|
147
152
|
}
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
|
-
const meta = {
|
|
155
|
+
const meta = {
|
|
156
|
+
repainted: new Date().toISOString(),
|
|
157
|
+
local_zone: (0, formatters_1.resolveLocalZoneAbbreviation)({ signalk }),
|
|
158
|
+
plugin_version: pluginVersion_1.PLUGIN_VERSION,
|
|
159
|
+
};
|
|
151
160
|
return { signalk, resources, pathMeta: {}, categories, meta };
|
|
152
161
|
}
|
|
@@ -31,7 +31,7 @@ class ZhsunycoDriver {
|
|
|
31
31
|
}
|
|
32
32
|
async identifyDevice(device, address, name, manufacturerId, manufacturerData) {
|
|
33
33
|
const advertisedInfo = manufacturerData ? (0, protocol_1.decodeAdvertisedInfo)(manufacturerData) : undefined;
|
|
34
|
-
const { info, batteryMv } = await readDeviceDetails(device, advertisedInfo);
|
|
34
|
+
const { info, batteryMv } = await readDeviceDetails(device, address, advertisedInfo);
|
|
35
35
|
return {
|
|
36
36
|
address,
|
|
37
37
|
name,
|
|
@@ -113,7 +113,7 @@ exports.ZhsunycoDriver = ZhsunycoDriver;
|
|
|
113
113
|
* config characteristic `paint()` reads, just to identify the device rather than to size a
|
|
114
114
|
* render.
|
|
115
115
|
*/
|
|
116
|
-
async function readDeviceDetails(device, advertisedInfo) {
|
|
116
|
+
async function readDeviceDetails(device, address, advertisedInfo) {
|
|
117
117
|
const fallback = { info: advertisedInfo, batteryMv: undefined };
|
|
118
118
|
const read = async () => {
|
|
119
119
|
try {
|
|
@@ -134,7 +134,12 @@ async function readDeviceDetails(device, advertisedInfo) {
|
|
|
134
134
|
await device.disconnect();
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
-
catch {
|
|
137
|
+
catch (err) {
|
|
138
|
+
// Swallowed rather than thrown - a device that refuses this connect (e.g. busy elsewhere,
|
|
139
|
+
// out of range) should still show up in the scan with whatever the advertisement itself
|
|
140
|
+
// carried, just without battery/PID-by-read. Logged so a blank battery column has a reason
|
|
141
|
+
// instead of looking like the read was simply never attempted.
|
|
142
|
+
console.error(`zhsunyco [${address}]: battery/config read failed: ${err.message}`);
|
|
138
143
|
return fallback;
|
|
139
144
|
}
|
|
140
145
|
};
|
|
@@ -142,5 +147,11 @@ async function readDeviceDetails(device, advertisedInfo) {
|
|
|
142
147
|
// `getPrimaryService`/`readValue`) has no timeout of its own either - race the whole read so
|
|
143
148
|
// one unresponsive device can't stall the rest of the scan (see `plugin.ts`'s `scanInProgress`,
|
|
144
149
|
// which otherwise stays set forever and silently skips every later scan).
|
|
145
|
-
return Promise.race([
|
|
150
|
+
return Promise.race([
|
|
151
|
+
read(),
|
|
152
|
+
(0, bleDiscovery_1.sleep)(SCAN_CONNECT_TIMEOUT_MS * 2).then(() => {
|
|
153
|
+
console.error(`zhsunyco [${address}]: battery/config read timed out after ${SCAN_CONNECT_TIMEOUT_MS * 2}ms`);
|
|
154
|
+
return fallback;
|
|
155
|
+
}),
|
|
156
|
+
]);
|
|
146
157
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -29,7 +29,8 @@ async function runStartupScan(app, discovered, durationSeconds) {
|
|
|
29
29
|
}
|
|
30
30
|
discovered.push(found);
|
|
31
31
|
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, '0')}` : 'unknown';
|
|
32
|
-
|
|
32
|
+
const hwid = found.hwVersion ?? 'unknown';
|
|
33
|
+
app.debug(`discovered ${driver.vendor} device "${found.name ?? ''}" [${found.address}] pid=${pid} hwid=${hwid}`);
|
|
33
34
|
});
|
|
34
35
|
});
|
|
35
36
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The installed plugin's own version, read from `package.json` once at module load - exposed to
|
|
3
|
+
* templates as `source=einklabel,path=plugin_version` (see `meta` in repaintScheduler.ts and
|
|
4
|
+
* `cli/liveContext.ts`), e.g. to show a build/version marker on a label for support purposes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PLUGIN_VERSION: string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLUGIN_VERSION = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
/**
|
|
7
|
+
* The installed plugin's own version, read from `package.json` once at module load - exposed to
|
|
8
|
+
* templates as `source=einklabel,path=plugin_version` (see `meta` in repaintScheduler.ts and
|
|
9
|
+
* `cli/liveContext.ts`), e.g. to show a build/version marker on a label for support purposes.
|
|
10
|
+
*/
|
|
11
|
+
exports.PLUGIN_VERSION = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf-8')).version;
|
|
@@ -128,6 +128,9 @@ class SvgRenderer {
|
|
|
128
128
|
const key = (0, assets_1.normalizeAssetKey)((0, binding_1.resolveBinding)(binding, context));
|
|
129
129
|
const assetPath = key && (0, assets_1.resolveAssetPath)(svgTemplatePath, binding.assets, key);
|
|
130
130
|
if (!assetPath) {
|
|
131
|
+
console.error(key
|
|
132
|
+
? `image "${descElement.textContent}" has no asset file for value "${key}" in "${binding.assets}"`
|
|
133
|
+
: `image "${descElement.textContent}" resolved to no usable value to pick an asset file with`);
|
|
131
134
|
element.parentNode?.removeChild(element);
|
|
132
135
|
continue;
|
|
133
136
|
}
|
package/dist/repaintScheduler.js
CHANGED
|
@@ -13,6 +13,7 @@ const formatters_1 = require("./render/formatters");
|
|
|
13
13
|
const unitCategories_1 = require("./unitCategories");
|
|
14
14
|
const pathMeta_1 = require("./pathMeta");
|
|
15
15
|
const resolveApiUrl_1 = require("./resolveApiUrl");
|
|
16
|
+
const pluginVersion_1 = require("./pluginVersion");
|
|
16
17
|
const INTERVAL_POLL_MS = 60000;
|
|
17
18
|
const SUBSCRIPTION_DEBOUNCE_MS = 2000;
|
|
18
19
|
function statePath(app) {
|
|
@@ -139,23 +140,30 @@ async function considerRepaint(app, config, device, state, getApiUrl) {
|
|
|
139
140
|
}
|
|
140
141
|
const renderContext = {
|
|
141
142
|
...rawContext,
|
|
142
|
-
meta: {
|
|
143
|
+
meta: {
|
|
144
|
+
repainted: new Date().toISOString(),
|
|
145
|
+
local_zone: (0, formatters_1.resolveLocalZoneAbbreviation)(rawContext),
|
|
146
|
+
plugin_version: pluginVersion_1.PLUGIN_VERSION,
|
|
147
|
+
},
|
|
143
148
|
};
|
|
144
149
|
const renderer = new svgRenderer_1.SvgRenderer();
|
|
145
150
|
const bitmap = await renderer.render(templatePath, renderContext, metadata.width, metadata.height - metadata.voffset);
|
|
146
151
|
const connectTimeoutMs = config.paintConnectTimeoutSeconds * 1000;
|
|
152
|
+
let paintDurationMs = 0;
|
|
147
153
|
await (0, bleDiscovery_1.withRetries)(config.paintRetries, async (attempt) => {
|
|
148
154
|
if (attempt > 1) {
|
|
149
|
-
app.debug(`"${device.friendlyName}": paint
|
|
155
|
+
app.debug(`"${device.friendlyName}": attempting paint ${attempt}/${config.paintRetries}`);
|
|
150
156
|
}
|
|
157
|
+
const startedAt = Date.now();
|
|
151
158
|
await driver.paint(bitmap, { address: model.address, aesKey: device.aesKey, connectTimeoutMs });
|
|
159
|
+
paintDurationMs = Date.now() - startedAt;
|
|
152
160
|
});
|
|
153
161
|
state[device.friendlyName] = { hash };
|
|
154
162
|
saveState(app, state);
|
|
155
163
|
if (device.forceRepaint) {
|
|
156
164
|
clearForceRepaint(app, device.friendlyName);
|
|
157
165
|
}
|
|
158
|
-
app.debug(`"${device.friendlyName}": repainted`);
|
|
166
|
+
app.debug(`"${device.friendlyName}": repainted (paint took ${paintDurationMs}ms)`);
|
|
159
167
|
}
|
|
160
168
|
function startRepaintScheduler(app, config) {
|
|
161
169
|
const state = loadState(app);
|
package/package.json
CHANGED
package/templates/tide.svg
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
inkscape:pagecheckerboard="0"
|
|
24
24
|
inkscape:deskcolor="#d1d1d1"
|
|
25
25
|
inkscape:zoom="1.3906433"
|
|
26
|
-
inkscape:cx="
|
|
27
|
-
inkscape:cy="
|
|
26
|
+
inkscape:cx="144.89697"
|
|
27
|
+
inkscape:cy="197.39066"
|
|
28
28
|
inkscape:window-width="1488"
|
|
29
29
|
inkscape:window-height="674"
|
|
30
30
|
inkscape:window-x="24"
|
|
@@ -71,19 +71,19 @@
|
|
|
71
71
|
transform="scale(0.89274878,1.1201359)">Tidal Station Name<desc
|
|
72
72
|
id="desc1">source=resources,resource=tides,path=station.name</desc></text><text
|
|
73
73
|
id="source"
|
|
74
|
-
x="
|
|
74
|
+
x="367.44577"
|
|
75
75
|
y="226.1548"
|
|
76
76
|
font-size="18"
|
|
77
77
|
font-family="serif"
|
|
78
78
|
fill="black"
|
|
79
|
-
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-inkscape-font-specification:'Roboto',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000;stroke-width:3.80045"
|
|
80
|
-
transform="scale(0.
|
|
79
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-inkscape-font-specification:'\'Roboto\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000;stroke-width:3.80045"
|
|
80
|
+
transform="scale(0.95907862,1.0426674)"><desc
|
|
81
81
|
id="desc21">source=resources,resource=tides,path=station.source.name</desc><tspan
|
|
82
82
|
sodipodi:role="line"
|
|
83
83
|
id="tspan20"
|
|
84
|
-
x="
|
|
84
|
+
x="367.44577"
|
|
85
85
|
y="226.1548"
|
|
86
|
-
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.
|
|
86
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-inkscape-font-specification:'\'Roboto\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:3.80045">SOURCE</tspan></text><text
|
|
87
87
|
id="last_repaint"
|
|
88
88
|
x="4.3477793"
|
|
89
89
|
y="201.9895"
|
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
fill="red"
|
|
120
120
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
|
|
121
121
|
id="desc15">source=resources,resource=tides,path=extremes[2].label</desc>High or Low</text><text
|
|
122
|
-
id="
|
|
123
|
-
x="
|
|
122
|
+
id="timezone"
|
|
123
|
+
x="330.60489"
|
|
124
124
|
y="204.67754"
|
|
125
125
|
font-size="10.373px"
|
|
126
126
|
font-family="sans-serif"
|
|
@@ -128,7 +128,20 @@
|
|
|
128
128
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;direction:rtl;fill:#000000;stroke-width:0.864419"
|
|
129
129
|
transform="scale(0.86441854,1.156847)">UTC+01:00<desc
|
|
130
130
|
id="desc11">source=einklabel,path=local_zone</desc></text><text
|
|
131
|
-
id="
|
|
131
|
+
id="plugin_version"
|
|
132
|
+
x="194.07487"
|
|
133
|
+
y="204.51942"
|
|
134
|
+
font-size="10.373px"
|
|
135
|
+
font-family="sans-serif"
|
|
136
|
+
fill="#ffff00"
|
|
137
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;direction:rtl;fill:#000000;stroke-width:0.864419"
|
|
138
|
+
transform="scale(0.86441854,1.156847)"><desc
|
|
139
|
+
id="desc16">source=einklabel,path=plugin_version</desc><tspan
|
|
140
|
+
sodipodi:role="line"
|
|
141
|
+
id="tspan1"
|
|
142
|
+
x="194.07487"
|
|
143
|
+
y="204.51942">v0.0.1</tspan></text><text
|
|
144
|
+
id="min_tide"
|
|
132
145
|
x="282.65424"
|
|
133
146
|
y="24.967674"
|
|
134
147
|
font-size="12px"
|
|
@@ -140,7 +153,7 @@
|
|
|
140
153
|
id="tspan17"
|
|
141
154
|
x="282.65424"
|
|
142
155
|
y="24.967674">0.0m</tspan></text><text
|
|
143
|
-
id="
|
|
156
|
+
id="max_tide"
|
|
144
157
|
x="360.77148"
|
|
145
158
|
y="24.967674"
|
|
146
159
|
font-size="12px"
|
|
@@ -211,7 +224,7 @@
|
|
|
211
224
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.55397"
|
|
212
225
|
transform="scale(0.74654856,1.3394976)">03:27<desc
|
|
213
226
|
id="desc3">source=resources,resource=tides,path=extremes[0].time,format=local_time</desc></text><text
|
|
214
|
-
id="extremes.0.
|
|
227
|
+
id="extremes.0.date"
|
|
215
228
|
x="10.326324"
|
|
216
229
|
y="74.630478"
|
|
217
230
|
font-size="14"
|
|
@@ -220,7 +233,7 @@
|
|
|
220
233
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
|
|
221
234
|
transform="scale(0.82428216,1.2131768)"><desc
|
|
222
235
|
id="desc2">source=resources,resource=tides,path=extremes[0].time,format=day_mon</desc>27 Jun</text><text
|
|
223
|
-
id="extremes.
|
|
236
|
+
id="extremes.1.date"
|
|
224
237
|
x="9.9649849"
|
|
225
238
|
y="123.34451"
|
|
226
239
|
font-size="14"
|
|
@@ -229,7 +242,7 @@
|
|
|
229
242
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
|
|
230
243
|
transform="scale(0.82428216,1.2131768)"><desc
|
|
231
244
|
id="desc7">source=resources,resource=tides,path=extremes[1].time,format=day_mon</desc>27 Jun</text><text
|
|
232
|
-
id="extremes.
|
|
245
|
+
id="extremes.2.date"
|
|
233
246
|
x="12.934491"
|
|
234
247
|
y="156.77794"
|
|
235
248
|
font-size="14"
|