Luckily I did manage to enlist today’s skipper to be my remote eyes and hands, and managed to execute the firmware update nevertheless, including timely dis-and-re-connect of that USB GPS mousy.
you have a vrm url with a gps that has signal / fix for me?
Meanwhile, you’re partially correct. The code is much more beautiful than I thought! The write up so far:
ok you got me, I looked into it, and here is what I found:
- our driver that works with usb connected gps-es does support setting a device instance, and also it works. Ie. restart that driver or reboot the GX and the gps on the dbus will have a different instance:
root@einstein:~# cat /data/log/localsettings/* | tai64nlocal
2025-09-18 18:59:34.539941500 INFO:root:Setting /Settings/Devices/vegps_ttyUSB1/ClassAndVrmInstance changed. Old: gps:0, New: gps:10
2025-09-18 19:00:54.200606500 INFO:root:Setting /Settings/Devices/vegps_ttyUSB1/ClassAndVrmInstance changed. Old: gps:10, New: gps:0
note that its tied to the USB port, so in case USB numbering changes, then this instance will change as well; result is that this is somewhat unreliable unless you add scripting that on boot finds the usb connected gps, checks the device instance, and changes it.
the other driver, that takes gps data from N2K networks, supports the same.
Then, here the proof that the instance indeed changes:
root@einstein:~# dbus -y com.victronenergy.gps.ve_ttyUSB1 / GetValue
value = {'Altitude': [],
'Connected': 1,
'Course': [],
'DeviceInstance': 0,
'FirmwareVersion': [],
'Fix': 0,
'HardwareVersion': [],
'Mgmt/Connection': 'USB',
'Mgmt/ProcessName': 'gps_dbus',
'Mgmt/ProcessVersion': '1.07',
'NrOfSatellites': [],
'Position/Latitude': [],
'Position/Longitude': [],
'ProductId': 41315,
'ProductName': 'NMEA-0183 GPS (USB1)',
'Speed': []}
Next, there is logic in dbus-systemcalc that continously monitors what gps data is on dbus, and then puts the service name as well as speed on the dbus under the /GpsService and /GpsSpeed paths.
I’ve added that to our documentation: dbus · victronenergy/venus Wiki · GitHub. Search for GPS.
The code responsible for gps data transmission to VRM also nicely uses that; so all good there.
But looks like the boat page in gui-v2 doesn’t use that, more on that later.
And meanwhile, a system with working gps would help to sort this quickly.
Yes what do you need? The installation is called ‘De Jonckvrouw’. I enabled remote support just now.
Feel free to snoop around my in my Node-RED flow. I left traces of past attempts to get this sorted.
It is all about the boat page for us,… ![]()
As noted above, local console boat page uses USB GPS, remote console boat page uses virtual GPS.
Happy for you to look at mine as it has both USB and virtual GPS. I am sure you have been on it before.
I bypassed that specific priority issue by removing the gps-dbus.ttyACM0 service altogether and processing the incoming GPS NMEA datastream from serial port to virtual GPS in Node-RED. This works as a charm until the next (remote) reboot or firmware update required manual disconnect and re-insertion of the gps.
[
{
"id": "7e107e80bdf1919f",
"type": "group",
"z": "8fb5afb2b0b585a3",
"name": "Serial Port USB GPS Datastream Processing",
"style": {
"label": true
},
"nodes": [
"serial1",
"buffer_to_string",
"filter_nmea",
"nmea1",
"switch1",
"prep_gga",
"prep_rmc",
"prep_vtg",
"set_motor_power",
"join1",
"smart_filter",
"log_raw",
"log_filtered",
"4c10c0acbf8f0125",
"5353eb79eb43021a",
"9c95944bea642dbc",
"ac9a9a9c185a398f",
"c8313db0a1df40ae"
],
"x": 14,
"y": 1079,
"w": 1612,
"h": 202
},
{
"id": "serial1",
"type": "serial in",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "USB GPS Input",
"serial": "a761d625dd063f3c",
"x": 180,
"y": 1120,
"wires": [
[
"buffer_to_string",
"4c10c0acbf8f0125"
]
]
},
{
"id": "buffer_to_string",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Buffer to String",
"func": "if (Buffer.isBuffer(msg.payload)) {\n msg.payload = msg.payload.toString('utf8');\n}\nreturn msg;",
"outputs": 1,
"x": 180,
"y": 1160,
"wires": [
[
"filter_nmea"
]
]
},
{
"id": "filter_nmea",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Filter Valid NMEA",
"func": "if (typeof msg.payload === 'string' && msg.payload.startsWith('$') && msg.payload.includes('*')) {\n return msg;\n}\nreturn null;",
"outputs": 1,
"x": 190,
"y": 1200,
"wires": [
[
"nmea1"
]
]
},
{
"id": "nmea1",
"type": "nmea",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Parse NMEA",
"x": 390,
"y": 1160,
"wires": [
[
"switch1"
]
]
},
{
"id": "switch1",
"type": "switch",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Route by Sentence",
"property": "payload.sentence",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "GGA",
"vt": "str"
},
{
"t": "eq",
"v": "RMC",
"vt": "str"
},
{
"t": "eq",
"v": "VTG",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 570,
"y": 1160,
"wires": [
[
"prep_gga"
],
[
"prep_rmc"
],
[
"prep_vtg"
]
]
},
{
"id": "prep_gga",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Prep GGA",
"func": "msg.topic = \"GGA\";\nmsg.payload = {\n lat: msg.payload.lat,\n lon: msg.payload.lon,\n alt: msg.payload.alt,\n numSat: msg.payload.numSat,\n horDilution: msg.payload.horDilution,\n fixType: msg.payload.fixType\n};\nreturn msg;",
"outputs": 1,
"x": 770,
"y": 1120,
"wires": [
[
"join1"
]
]
},
{
"id": "prep_rmc",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Prep RMC",
"func": "msg.topic = \"RMC\";\nmsg.payload = {\n lat: msg.payload.lat,\n lon: msg.payload.lon,\n speedKnots: msg.payload.speedKnots,\n trackTrue: msg.payload.trackTrue,\n status: msg.payload.status\n};\nreturn msg;",
"outputs": 1,
"x": 770,
"y": 1160,
"wires": [
[
"join1"
]
]
},
{
"id": "prep_vtg",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Prep VTG",
"func": "msg.topic = \"VTG\";\nmsg.payload = {\n speedKnots: msg.payload.speedKnots,\n trackTrue: msg.payload.trackTrue\n};\nreturn msg;",
"outputs": 1,
"x": 760,
"y": 1200,
"wires": [
[
"join1"
]
]
},
{
"id": "set_motor_power",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Set Motor Power",
"func": "msg.topic = \"power\";\nmsg.payload = { motor_power: (typeof msg.payload === 'number' ? msg.payload / 1000 : 0) };\nreturn msg;",
"outputs": 1,
"x": 790,
"y": 1240,
"wires": [
[
"join1"
]
]
},
{
"id": "join1",
"type": "join",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Combine GGA, RMC, VTG & Power",
"mode": "custom",
"build": "object",
"property": "payload",
"propertyType": "msg",
"key": "topic",
"joiner": "\\n",
"joinerType": "str",
"useparts": true,
"accumulate": false,
"timeout": "1",
"count": "3",
"reduceRight": false,
"reduceExp": "",
"reduceInit": "",
"reduceInitType": "",
"reduceFixup": "",
"x": 1070,
"y": 1200,
"wires": [
[
"log_raw",
"smart_filter"
]
]
},
{
"id": "smart_filter",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "Smart Filter",
"func": "var combined = msg.payload;\nvar gga = combined.GGA || {};\nvar rmc = combined.RMC || {};\nvar vtg = combined.VTG || {};\nvar power = combined.power || {};\n\nvar lat = gga.lat !== undefined && !isNaN(gga.lat) ? gga.lat : (rmc.lat !== undefined && !isNaN(rmc.lat) ? rmc.lat : null);\nvar lon = gga.lon !== undefined && !isNaN(gga.lon) ? gga.lon : (rmc.lon !== undefined && !isNaN(rmc.lon) ? rmc.lon : null);\nvar alt = gga.alt !== undefined && !isNaN(gga.alt) ? gga.alt : null;\nvar speedKnots = rmc.speedKnots !== undefined && !isNaN(rmc.speedKnots) ? rmc.speedKnots : (vtg.speedKnots !== undefined && !isNaN(vtg.speedKnots) ? vtg.speedKnots : null);\nvar trackTrue = rmc.trackTrue !== undefined ? rmc.trackTrue : (vtg.trackTrue !== undefined ? vtg.trackTrue : null);\n\nvar speed = speedKnots !== null ? speedKnots * 0.514444 : null;\n\nvar prev = context.get('prev') || {speed: 0, rawSpeed: 0, trackTrue: null, time: Date.now(), lat: null, lon: null, alt: 0, lowSpeedCount: 0};\nvar now = Date.now();\nvar dt = (now - prev.time) / 1000;\nif (dt <= 0) dt = 1;\n\nvar reliable = true;\nvar rawSpeed = speed !== null ? speed : prev.rawSpeed;\n\nif (rmc.status !== \"valid\" || gga.fixType !== \"fix\" || (gga.numSat !== undefined && gga.numSat < 6) || (gga.horDilution !== undefined && gga.horDilution > 5)) {\n reliable = false;\n}\n/*\nif (alt !== null && prev.alt !== null && Math.abs(alt - prev.alt) > 10) {\n reliable = false;\n}\n*/\nif (!reliable) {\n msg.payload.filtered = {\n lat: prev.lat,\n lon: prev.lon,\n alt: prev.alt,\n speed: prev.speed,\n// rawSpeed: prev.rawSpeed,\n rawSpeed: rawSpeed, // always use actual rawSpeed\n trackTrue: prev.trackTrue\n };\n context.set('prev', { ...prev, time: now });\n return msg;\n}\n\nvar disableSpeedFilter = false; // Set to true to disable filtered speed processing\n\nvar filteredTrack = trackTrue !== null ? trackTrue : prev.trackTrue;\n\n//var rawSpeed = speed !== null ? speed : prev.rawSpeed;\nvar filteredSpeed = speed !== null ? speed : prev.speed;\nif (filteredSpeed !== null && !disableSpeedFilter) {\n var motor_power = power.motor_power || 0; // kW from input\n motor_power = Math.min(15, Math.max(0, motor_power));\n var efficiency = 0.7;\n var v_calc = Math.abs(speed || prev.speed) || 0.1; // Min 0.1 m/s for div\n var raw_delta = (speed || 0) - prev.speed;\n var thrust_sign = Math.sign(raw_delta || prev.speed || 0);\n if (thrust_sign === 0 && motor_power > 0) thrust_sign = 1; // Assume forward if power >0 and no delta\n var f_thrust = (motor_power * 1000 * efficiency / v_calc) * thrust_sign;\n var c_drag = 500; // kg/m, tuned for ~10kW at 2 m/s\n var f_drag = c_drag * Math.pow(prev.speed, 2) * Math.sign(prev.speed || 0);\n var net_f = f_thrust - f_drag;\n var a_max = net_f / 20000;\n var a_wind_max = 0.1; // Max unpowered accel (wind/drift)\n if (motor_power < 0.1) {\n a_max = thrust_sign * Math.min(Math.abs(a_max), a_wind_max);\n }\n var lowThreshold = 0.14; // ~0.5 km/h\n if (Math.abs(filteredSpeed) < lowThreshold && motor_power < 0.1) {\n prev.lowSpeedCount = (prev.lowSpeedCount || 0) + 1;\n if (prev.lowSpeedCount >= 4) filteredSpeed = 0;\n } else {\n prev.lowSpeedCount = 0;\n }\n var delta = filteredSpeed - prev.speed;\n var a = delta / dt;\n var a_limit = thrust_sign > 0 ? Math.max(0, a_max) : Math.min(0, a_max);\n if ((thrust_sign > 0 && a > a_limit) || (thrust_sign < 0 && a < a_limit) || (thrust_sign === 0 && Math.abs(a) > Math.abs(a_limit))) {\n filteredSpeed = prev.speed + a_limit * dt;\n }\n}\n\nvar alpha_alt = 0.1; // Default low-pass alpha\nif (gga.numSat >= 8 && gga.horDilution <= 2) alpha_alt = 0.3; // Trust more if good fix\n\nvar filteredAlt = alt !== null ? (alpha_alt * alt + (1 - alpha_alt) * (prev.alt || 0)) : prev.alt;\n\nvar filteredLat = lat !== null ? lat : prev.lat;\nvar filteredLon = lon !== null ? lon : prev.lon;\n\ncontext.set('prev', {\n lat: filteredLat,\n lon: filteredLon,\n alt: filteredAlt,\n speed: filteredSpeed,\n rawSpeed: rawSpeed,\n trackTrue: filteredTrack,\n time: now,\n lowSpeedCount: prev.lowSpeedCount || 0\n});\n\nmsg.payload.filtered = {\n lat: filteredLat,\n lon: filteredLon,\n alt: filteredAlt,\n speed: filteredSpeed,\n rawSpeed: rawSpeed,\n trackTrue: filteredTrack\n};\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1330,
"y": 1200,
"wires": [
[
"log_filtered",
"9c95944bea642dbc"
]
]
},
{
"id": "log_raw",
"type": "file",
"z": "8fb5afb2b0b585a3",
"d": true,
"g": "7e107e80bdf1919f",
"name": "Log Raw Data",
"filename": "/tmp/gps_raw.log",
"filenameType": "str",
"appendNewline": true,
"createDir": true,
"overwriteFile": "false",
"encoding": "none",
"x": 1080,
"y": 1240,
"wires": [
[]
]
},
{
"id": "log_filtered",
"type": "file",
"z": "8fb5afb2b0b585a3",
"d": true,
"g": "7e107e80bdf1919f",
"name": "Log Filtered Data",
"filename": "/tmp/gps_filtered.log",
"appendNewline": true,
"createDir": true,
"overwriteFile": "false",
"encoding": "none",
"x": 1310,
"y": 1240,
"wires": [
[]
]
},
{
"id": "4c10c0acbf8f0125",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "count",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "counter",
"x": 370,
"y": 1120,
"wires": []
},
{
"id": "5353eb79eb43021a",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "count",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "counter",
"x": 1530,
"y": 1140,
"wires": []
},
{
"id": "9c95944bea642dbc",
"type": "delay",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "",
"pauseType": "rate",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "2",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": true,
"allowrate": false,
"outputs": 1,
"x": 1330,
"y": 1140,
"wires": [
[
"5353eb79eb43021a",
"c8313db0a1df40ae"
]
]
},
{
"id": "ac9a9a9c185a398f",
"type": "link in",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "link in 2",
"links": [
"c2791d7e184f6d30"
],
"x": 55,
"y": 1240,
"wires": [
[
"set_motor_power"
]
]
},
{
"id": "c8313db0a1df40ae",
"type": "link out",
"z": "8fb5afb2b0b585a3",
"g": "7e107e80bdf1919f",
"name": "link out 4",
"mode": "link",
"links": [
"f9c2ae40a4f63614"
],
"x": 1435,
"y": 1140,
"wires": []
},
{
"id": "a761d625dd063f3c",
"type": "serial-port",
"name": "",
"serialport": "/dev/ttyACM0",
"serialbaud": "38400",
"databits": 8,
"parity": "none",
"stopbits": 1,
"waitfor": "",
"dtr": "none",
"rts": "none",
"cts": "none",
"dsr": "none",
"newline": "\\n",
"bin": "false",
"out": "char",
"addchar": "",
"responsetimeout": 10000
}
]
[
{
"id": "c8ca4cebfb312063",
"type": "group",
"z": "8fb5afb2b0b585a3",
"name": "Virtual GPS Post Processing",
"style": {
"label": true
},
"nodes": [
"0e4c55a2ef816e7f",
"9017a71c25381726",
"dcbe87ace046bc4a",
"f3a5a1bcaadddf63",
"59254e55dec7e417",
"9c8de14ba9853d2f",
"dda7d82a4cd516a2",
"85b2d278f2ac7b36",
"b176fc34c550e68c",
"cb198d16bdb66233",
"c3f982038f1b601b",
"ab51a83af4b6fca6",
"42cfd865f7ce3a96",
"8edb25545f83abc9",
"endnode",
"debug_lat",
"debug_lon",
"debug_alt",
"debug_course",
"debug_raw_speed",
"669ac9ee62d2aeb7",
"af954c709ef66b30",
"49cba18c0309f65a",
"570bf77837834db2",
"d2b8b7846bbbc9d7",
"827f4e9424440dbd",
"5633af06f00fd628",
"e676f275cf2d8661",
"def9501945b94117",
"5bd9c20d35fc9db8",
"cd8568a16eac0d3e",
"f9c2ae40a4f63614",
"935f242472c0522e"
],
"x": 14,
"y": 639,
"w": 1972,
"h": 422
},
{
"id": "0e4c55a2ef816e7f",
"type": "victron-virtual",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "",
"device": "gps",
"default_values": false,
"battery_capacity": 25,
"include_battery_temperature": false,
"generator_type": "ac",
"generator_nrofphases": 1,
"include_engine_hours": false,
"include_starter_voltage": false,
"include_history_energy": false,
"grid_nrofphases": 1,
"include_motor_temp": false,
"include_controller_temp": false,
"include_coolant_temp": false,
"include_motor_rpm": true,
"include_motor_direction": true,
"position": 0,
"pvinverter_nrofphases": 1,
"switch_count": 1,
"switch_1_type": 1,
"switch_2_type": 1,
"switch_3_type": 1,
"switch_4_type": 1,
"fluid_type": 0,
"include_tank_battery": false,
"include_tank_temperature": false,
"tank_battery_voltage": 3.3,
"tank_capacity": 0.2,
"temperature_type": 2,
"include_humidity": false,
"include_pressure": false,
"include_temp_battery": false,
"temp_battery_voltage": 3.3,
"x": 690,
"y": 680,
"wires": []
},
{
"id": "9017a71c25381726",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/Speed",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/Speed",
"name": "/Speed",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1290,
"y": 900,
"wires": []
},
{
"id": "dcbe87ace046bc4a",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/Altitude",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/Altitude",
"name": "/Altitude",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1280,
"y": 840,
"wires": []
},
{
"id": "f3a5a1bcaadddf63",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/Position/Latitude",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/Position/Latitude",
"name": "/Position/Latitude",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1260,
"y": 720,
"wires": []
},
{
"id": "59254e55dec7e417",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/Position/Longitude",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/Position/Longitude",
"name": "/Position/Longitude",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1250,
"y": 780,
"wires": []
},
{
"id": "9c8de14ba9853d2f",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "km/h",
"func": "msg.payload = 3.6 * msg.payload\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1750,
"y": 900,
"wires": [
[
"dda7d82a4cd516a2"
]
]
},
{
"id": "dda7d82a4cd516a2",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "km/h flt",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 1880,
"y": 900,
"wires": []
},
{
"id": "85b2d278f2ac7b36",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "km/h",
"func": "msg.payload = 3.6 * msg.payload\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1750,
"y": 1020,
"wires": [
[
"b176fc34c550e68c"
]
]
},
{
"id": "b176fc34c550e68c",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "km/h raw",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 1880,
"y": 1020,
"wires": []
},
{
"id": "cb198d16bdb66233",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/Fix",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/Fix",
"name": "/Fix",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 380,
"y": 720,
"wires": []
},
{
"id": "c3f982038f1b601b",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/NrOfSatellites",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/NrOfSatellites",
"name": "/NrOfSatellites",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 420,
"y": 780,
"wires": []
},
{
"id": "ab51a83af4b6fca6",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/Course",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/Course",
"name": "/Course",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1290,
"y": 960,
"wires": []
},
{
"id": "42cfd865f7ce3a96",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "10",
"topic": "",
"payload": "1",
"payloadType": "num",
"x": 170,
"y": 720,
"wires": [
[
"cb198d16bdb66233"
]
]
},
{
"id": "8edb25545f83abc9",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "10",
"topic": "",
"payload": "15",
"payloadType": "num",
"x": 170,
"y": 780,
"wires": [
[
"c3f982038f1b601b"
]
]
},
{
"id": "endnode",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Extract & Output Topics",
"func": "var filtered = msg.payload.filtered || {};\n\nvar latMsg = filtered.lat !== null ? { topic: \"latitude\", payload: filtered.lat } : null;\nvar lonMsg = filtered.lon !== null ? { topic: \"longitude\", payload: filtered.lon } : null;\nvar altMsg = filtered.alt !== null ? { topic: \"altitude\", payload: filtered.alt } : null;\nvar speedMsg = filtered.speed !== null ? { topic: \"speed\", payload: filtered.speed } : null;\nvar courseMsg = filtered.trackTrue !== null ? { topic: \"course\", payload: filtered.trackTrue } : null;\nvar rawSpeedMsg = filtered.rawSpeed !== null ? { topic: \"raw_speed\", payload: filtered.rawSpeed } : null;\n\nreturn [latMsg, lonMsg, altMsg, speedMsg, courseMsg, rawSpeedMsg];",
"outputs": 6,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 210,
"y": 940,
"wires": [
[
"af954c709ef66b30"
],
[
"49cba18c0309f65a"
],
[
"570bf77837834db2"
],
[
"5633af06f00fd628"
],
[
"d2b8b7846bbbc9d7"
],
[
"935f242472c0522e"
]
]
},
{
"id": "debug_lat",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Latitude Output",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 1560,
"y": 720,
"wires": []
},
{
"id": "debug_lon",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Longitude Output",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 1570,
"y": 780,
"wires": []
},
{
"id": "debug_alt",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Altitude Output",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 1560,
"y": 840,
"wires": []
},
{
"id": "debug_course",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Course Output",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 1560,
"y": 960,
"wires": []
},
{
"id": "debug_raw_speed",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Raw Speed Output",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 1570,
"y": 1020,
"wires": []
},
{
"id": "669ac9ee62d2aeb7",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Filtered Speed Output",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 1580,
"y": 900,
"wires": []
},
{
"id": "af954c709ef66b30",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "mux",
"func": "return msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 990,
"y": 720,
"wires": [
[
"f3a5a1bcaadddf63",
"debug_lat"
]
]
},
{
"id": "49cba18c0309f65a",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "mux",
"func": "return msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 990,
"y": 780,
"wires": [
[
"59254e55dec7e417",
"debug_lon"
]
]
},
{
"id": "570bf77837834db2",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "mux",
"func": "return msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 990,
"y": 840,
"wires": [
[
"dcbe87ace046bc4a",
"debug_alt"
]
]
},
{
"id": "d2b8b7846bbbc9d7",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "mux",
"func": "return msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 990,
"y": 960,
"wires": [
[
"ab51a83af4b6fca6",
"debug_course"
]
]
},
{
"id": "827f4e9424440dbd",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "mux",
"func": "return msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 990,
"y": 900,
"wires": [
[
"9c8de14ba9853d2f",
"9017a71c25381726",
"669ac9ee62d2aeb7"
]
]
},
{
"id": "5633af06f00fd628",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "Speed Limiter",
"func": "if (msg.payload <= 0.13889)\n{\n msg.payload = 0\n} else if (msg.payload >= 2.77777)\n{\n msg.payload = 2.77777\n}\nmsg.topic = \"speed\"\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 500,
"y": 940,
"wires": [
[
"e676f275cf2d8661",
"cd8568a16eac0d3e"
]
]
},
{
"id": "e676f275cf2d8661",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "moving average",
"func": "// This code runs for each incoming message (msg) in the Function node.\n// It computes a weighted moving average over up to 5 recent data points and outputs the smoothed value.\n\n// Hardcoded weights for the moving average. These apply to the most recent data point first (index 0) to the oldest (index 4).\n// Weights are between 0.0 and 1.0. Set to 0.0 to ignore older points, effectively shortening the buffer.\nconst weights = [1.0, 0.8, 0.25, 0.125, 0.06125]; // Default weights as specified.\n\n// Single flow context key for the buffer (unique per node using node.id).\nconst bufferKey = `averagingBuffer_${node.id}`;\n\n// Load the buffer from flow context (array of recent data points); initialize if not present.\nlet buffer = flow.get(bufferKey) || [];\n\n// Check if the payload is a valid number.\nif (msg.payload !== null && !isNaN(msg.payload)) {\n // Add the new data point to the buffer.\n buffer.push(parseFloat(msg.payload)); // Convert to number for safety.\n \n // Limit buffer size to the length of weights (e.g., 5).\n if (buffer.length > weights.length) {\n buffer.shift(); // Remove the oldest point.\n }\n \n // Calculate the weighted average (smoothed value).\n let weightedSum = 0;\n let totalWeight = 0;\n \n // Loop through buffer from most recent to oldest.\n for (let i = 0; i < buffer.length; i++) {\n const dataIndex = buffer.length - 1 - i; // Most recent at end.\n const weight = weights[i] || 0; // Default to 0 if no weight.\n weightedSum += buffer[dataIndex] * weight;\n totalWeight += weight;\n }\n \n // Compute average; fallback to latest value if no weights.\n const smoothedValue = totalWeight > 0 ? weightedSum / totalWeight : buffer[buffer.length - 1];\n \n // Output the smoothed value.\n msg.payload = smoothedValue;\n \n // Save the updated buffer back to flow context.\n flow.set(bufferKey, buffer);\n \n return msg; // Send the message.\n} else {\n // Invalid payload: Log a warning and skip.\n node.warn(\"Invalid payload received: \" + JSON.stringify(msg.payload) + \". Skipping output.\");\n return null;\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 700,
"y": 900,
"wires": [
[
"def9501945b94117"
]
]
},
{
"id": "def9501945b94117",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "upsample",
"func": "// This code runs for each incoming message (msg) in the Function node.\n// It performs time-based linear interpolation to upsample the input frequency (e.g., from 1 Hz smoothed values) to a higher output frequency (hardcoded to 5 Hz).\n// The node outputs interpolated values at 5 Hz via a timer, providing smooth transitions between input points.\n// Inputs are used to update the target value for interpolation; outputs are generated independently by the timer.\n// All persistent state is stored in a single flow context object under one key.\n// Deduplication is applied to outputs: Only send if the absolute difference from the last sent value exceeds a small epsilon, to avoid spamming duplicates (e.g., constant zeros or any constant).\n// On the first valid input, immediately output the value to ensure it's always sent, even if zero.\n\n// Hardcoded output frequency: 5 Hz (outputs every 200 ms).\nconst outputFrequency = 5;\nconst outputIntervalMs = 1000 / outputFrequency;\n\n// Duration for the smooth transition (interpolation) between old and new input values, in milliseconds.\nconst transitionDurationMs = 1000; // 1 second matches typical 1 Hz input rate.\n\n// Epsilon for deduplication: Only send if absolute difference from last sent exceeds this (handles constants like zero).\nconst epsilon = 1e-6; // Small threshold for change detection.\n\n// Single flow context key for the entire state object (unique per node using node.id).\nconst stateKey = `upsamplingState_${node.id}`;\n\n// Load the state object from flow context; initialize if not present.\nlet state = flow.get(stateKey) || {\n previousValue: 0, // Starting value for the current transition\n targetValue: 0, // Target value (latest input) for the transition\n startTime: undefined, // Timestamp when the last transition started\n closeHandlerSet: false, // Flag to track if the close handler is set\n timerStarted: false, // Flag to track if the timer is running\n lastSentValue: undefined // Last output value sent (for deduplication)\n};\n\n// Define the timer callback outside any blocks: Computes and sends the current interpolated value, with deduplication.\nfunction sendInterpolatedOutput() {\n // Load the latest state each tick (single key read).\n const currentState = flow.get(stateKey);\n if (!currentState || currentState.startTime === undefined) return; // Skip if not initialized.\n \n const now = Date.now();\n let fraction = (now - currentState.startTime) / transitionDurationMs;\n if (fraction > 1) fraction = 1;\n \n const interpolated = currentState.previousValue + fraction * (currentState.targetValue - currentState.previousValue);\n \n // Deduplication: Send only if absolute change exceeds epsilon.\n let shouldSend = true;\n if (currentState.lastSentValue !== undefined) {\n const absDiff = Math.abs(interpolated - currentState.lastSentValue);\n shouldSend = absDiff > epsilon;\n }\n \n if (shouldSend) {\n node.send({ payload: interpolated });\n currentState.lastSentValue = interpolated;\n flow.set(stateKey, currentState); // Update state with new lastSentValue (single key write).\n }\n}\n\n// Check if the payload is a valid number (expected to be a smoothed value from the averaging node).\nif (msg.payload !== null && !isNaN(msg.payload)) {\n const inputValue = parseFloat(msg.payload);\n \n // Setup the close handler for cleanup if not already set.\n if (!state.closeHandlerSet) {\n node.on('close', function() {\n // Clear the timer if running (stored in node context to avoid serialization issues).\n const timerId = context.get('timerId');\n if (timerId) {\n clearInterval(timerId);\n context.set('timerId', undefined);\n }\n // Delete the single state object from flow context to avoid clutter.\n flow.set(stateKey, undefined);\n });\n state.closeHandlerSet = true;\n }\n \n // Update interpolation state.\n const currentTime = Date.now();\n let currentInterpolatedValue;\n let isFirstInput = false;\n \n if (state.startTime === undefined) {\n // First valid input: Initialize state at the input value.\n currentInterpolatedValue = inputValue;\n state.previousValue = inputValue;\n state.targetValue = inputValue;\n state.startTime = currentTime;\n isFirstInput = true;\n } else {\n // Calculate the current position in the ongoing transition.\n let progressFraction = (currentTime - state.startTime) / transitionDurationMs;\n if (progressFraction > 1) progressFraction = 1; // Clamp if transition is complete.\n currentInterpolatedValue = state.previousValue + progressFraction * (state.targetValue - state.previousValue);\n }\n \n // Start a new transition to the latest input value.\n state.previousValue = currentInterpolatedValue;\n state.targetValue = inputValue;\n state.startTime = currentTime;\n \n // Save the updated state object back to flow context (single key).\n flow.set(stateKey, state);\n \n // Start the timer if not already running.\n if (!state.timerStarted) {\n // Start the interval and store the timer ID in node context (memory-only, avoids flow context serialization issues).\n const timerId = setInterval(sendInterpolatedOutput, outputIntervalMs);\n context.set('timerId', timerId);\n state.timerStarted = true;\n \n // Save the state again after starting the timer (flag update).\n flow.set(stateKey, state);\n }\n \n // On first input, immediately output the value to ensure it's always sent (even if zero), and set lastSentValue.\n if (isFirstInput) {\n node.send({ payload: inputValue });\n state.lastSentValue = inputValue;\n flow.set(stateKey, state); // Update lastSentValue.\n }\n \n // No immediate output from input (except for first); the timer handles ongoing outputs.\n return null;\n} else {\n // Invalid payload: Log a warning and skip.\n node.warn(\"Invalid payload received: \" + JSON.stringify(msg.payload) + \". Skipping update.\");\n return null;\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 860,
"y": 900,
"wires": [
[
"827f4e9424440dbd"
]
]
},
{
"id": "5bd9c20d35fc9db8",
"type": "link out",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "link out 1",
"mode": "link",
"links": [
"91dd0bfa72fb50f3"
],
"x": 1435,
"y": 1020,
"wires": []
},
{
"id": "cd8568a16eac0d3e",
"type": "link out",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "link out 3",
"mode": "link",
"links": [
"e0dcdd1cc28eb3b2"
],
"x": 1435,
"y": 940,
"wires": []
},
{
"id": "f9c2ae40a4f63614",
"type": "link in",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "link in 4",
"links": [
"c8313db0a1df40ae"
],
"x": 55,
"y": 940,
"wires": [
[
"endnode"
]
]
},
{
"id": "935f242472c0522e",
"type": "function",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "mux",
"func": "return msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 990,
"y": 1020,
"wires": [
[
"debug_raw_speed",
"85b2d278f2ac7b36",
"5bd9c20d35fc9db8"
]
]
}
]
Here the ‘work in progress’ using a bigSSH node to enable remote root SSH access to execute commands concerning the start/stop/removal of dbus devices and virtual device instance numbering
[
{
"id": "54f673c06c921b89",
"type": "group",
"z": "8fb5afb2b0b585a3",
"name": "RUN rm /service/gps-dbus.ttyACM0 right after USB GPS insertion",
"style": {
"label": true
},
"nodes": [
"4c95bf80781e4cf4",
"0c9824c09e7d8f59",
"cf26e2b0087e3e47",
"8db72d1179334947",
"7a85f5b4e7e10b53",
"f038bb1dd9773b1d",
"0358decb9f02395a",
"828779d7561997e0",
"51d8cc85b1e0e325",
"2726f45ea2a6f4e1",
"e6b9de6236fb3701",
"4d1391984c0beeb5",
"04a5b2169989180f",
"1cd3a108ba0c713c",
"75762b78945df63d",
"1d093e2ee7c489f6",
"ea0dea956f8170b5",
"6fa4bd3d3fe8fab9",
"0c69d74ca811bc64",
"73a3431641cc0294",
"7b12a9391aeca341",
"ba2942bd86dc1d2a",
"bfca80e9b9d3eae5",
"7cc52a19fc3485c6",
"5bbb63cd75cd4cd2",
"fed4a9af822dc427",
"a4448f05c8c0ecbe",
"24c2e1377d5e19c4"
],
"x": 14,
"y": 1491.5,
"w": 932,
"h": 889.5
},
{
"id": "4c95bf80781e4cf4",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "debug 2",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 840,
"y": 1600,
"wires": []
},
{
"id": "0c9824c09e7d8f59",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "debug 3",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 840,
"y": 1660,
"wires": []
},
{
"id": "cf26e2b0087e3e47",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "svstat /service/gps-dbus.ttyACM0",
"commandLine": "svstat /service/gps-dbus.ttyACM0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 360,
"y": 1600,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "8db72d1179334947",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 1600,
"wires": [
[
"cf26e2b0087e3e47"
]
]
},
{
"id": "7a85f5b4e7e10b53",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "svc -d /service/gps-dbus.ttyACM0",
"commandLine": "svc -d /service/gps-dbus.ttyACM0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 360,
"y": 1680,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "f038bb1dd9773b1d",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 1680,
"wires": [
[
"7a85f5b4e7e10b53"
]
]
},
{
"id": "0358decb9f02395a",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "svc -u /service/gps-dbus.ttyACM0",
"commandLine": "svc -u /service/gps-dbus.ttyACM0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 360,
"y": 1760,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "828779d7561997e0",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 1760,
"wires": [
[
"0358decb9f02395a"
]
]
},
{
"id": "51d8cc85b1e0e325",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "rm /service/gps-dbus.ttyACM0",
"commandLine": "rm /service/gps-dbus.ttyACM0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 350,
"y": 1840,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "2726f45ea2a6f4e1",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"d": true,
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": "0.1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 1820,
"wires": [
[
"51d8cc85b1e0e325"
]
]
},
{
"id": "e6b9de6236fb3701",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "ln -s /opt/victronenergy/service-templates/gps-dbus /service/gps-dbus.ttyACM0",
"commandLine": "ln -s /opt/victronenergy/service-templates/gps-dbus /service/gps-dbus.ttyACM0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 500,
"y": 2000,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "4d1391984c0beeb5",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 2000,
"wires": [
[
"e6b9de6236fb3701"
]
]
},
{
"id": "04a5b2169989180f",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "dbus -y | grep gps",
"commandLine": "dbus -y | grep gps",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 310,
"y": 2080,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "1cd3a108ba0c713c",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 2080,
"wires": [
[
"04a5b2169989180f"
]
]
},
{
"id": "75762b78945df63d",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "dbus -y com.victronenergy.gps.ve_ttyACM0 /DeviceInstance SetValue 200",
"commandLine": "dbus -y com.victronenergy.gps.ve_ttyACM0 /DeviceInstance SetValue 200",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 480,
"y": 2160,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "1d093e2ee7c489f6",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 2140,
"wires": [
[
"75762b78945df63d"
]
]
},
{
"id": "ea0dea956f8170b5",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "dbus -y com.victronenergy.gps.ve_ttyACM0 /DeviceInstance GetValue",
"commandLine": "dbus -y com.victronenergy.gps.ve_ttyACM0 /DeviceInstance GetValue",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 470,
"y": 2240,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "6fa4bd3d3fe8fab9",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 2240,
"wires": [
[
"ea0dea956f8170b5"
]
]
},
{
"id": "0c69d74ca811bc64",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "10",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 2180,
"wires": [
[
"75762b78945df63d"
]
]
},
{
"id": "73a3431641cc0294",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "dbus -y com.victronenergy.gps.virtual_0e4c55a2ef816e7f /DeviceInstance SetValue 0",
"commandLine": "dbus -y com.victronenergy.gps.virtual_0e4c55a2ef816e7f /DeviceInstance SetValue 0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": true,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 520,
"y": 2320,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "7b12a9391aeca341",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 2300,
"wires": [
[
"73a3431641cc0294"
]
]
},
{
"id": "ba2942bd86dc1d2a",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "10",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 2340,
"wires": [
[
"73a3431641cc0294"
]
]
},
{
"id": "bfca80e9b9d3eae5",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "svstat /service/gps-dbus.ttyUSB0",
"commandLine": "svstat /service/gps-dbus.ttyUSB0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 360,
"y": 1540,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "7cc52a19fc3485c6",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 1540,
"wires": [
[
"bfca80e9b9d3eae5"
]
]
},
{
"id": "5bbb63cd75cd4cd2",
"type": "bigssh",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "rm /service/gps-dbus.ttyUSB0",
"commandLine": "rm /service/gps-dbus.ttyUSB0",
"commandArgs": "",
"minError": 1,
"minWarning": 1,
"noStdin": false,
"format": "",
"payloadIsArg": false,
"myssh": "53238a8e0e43bae0",
"x": 350,
"y": 1920,
"wires": [
[
"24c2e1377d5e19c4"
],
[
"4c95bf80781e4cf4"
],
[
"0c9824c09e7d8f59"
]
]
},
{
"id": "fed4a9af822dc427",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "15",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 120,
"y": 1920,
"wires": [
[
"5bbb63cd75cd4cd2"
]
]
},
{
"id": "a4448f05c8c0ecbe",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 1860,
"wires": [
[
"51d8cc85b1e0e325"
]
]
},
{
"id": "24c2e1377d5e19c4",
"type": "debug",
"z": "8fb5afb2b0b585a3",
"g": "54f673c06c921b89",
"name": "debug 1",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 840,
"y": 1540,
"wires": []
},
{
"id": "53238a8e0e43bae0",
"type": "SSH_Credentials",
"host": "127.0.0.1",
"port": "22",
"userlabel": "root@127.0.0.1"
}
]
I have been down that route and indeed managed to set custom device instances. But that didn’t result in a switchover of the systemwide priority GPS datastream as far as I can recall..
thanks both. I logged into pwfarnell his system and got what I wanted, a brief check on how the data that I was looking for looks:
‘GpsService’: ‘com.victronenergy.gps.ve_ttyUSB0’,
‘GpsSpeed’: 0.0,
By the way, @pwfarnell , below is a copy of our virtual GPS. Note how the fix path is empty. For this system wide selection mechanism to work, you need to set Fix to numeric 1.
Leaving it set to INVALID, which is what that empty array, [] is, doesn’t work.
root@einstein:~# dbus -y com.victronenergy.gps.virtual_33b273864e49d3a7 / GetValue
value = {'Altitude': [],
'Connected': 1,
'Course': [],
'CustomName': 'Dummy gps',
'DeviceInstance': 100,
'Fix': [],
'Mgmt/Connection': 'Virtual',
'Mgmt/ProcessName': 'dbus-victron-virtual',
'Mgmt/ProcessVersion': '0.1.17',
'NrOfSatellites': [],
'Position/Latitude': 52.22222
'Position/Longitude': -2.11111,
'ProductId': 49256,
'ProductName': 'Virtual gps',
'Serial': '33b273864e49d3a7',
'Speed': 0.0}
root@einstein:~#
And, make sure to set the device instance of that GPS lower than the USB-connected one. To change the USB connected one, use the Settings → VRM → VRM device instances menu.
Then everything on your side is done correctly.
But after that I expect all of it to still not work correctly on the boat page, as that is is most likely broken and we’ll be looking into that.
It’s a bit a shame the flow renderer doesn’t render victron nodes
[
{
"id": "0e4c55a2ef816e7f",
"type": "victron-virtual",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "",
"device": "gps",
"default_values": false,
"battery_capacity": 25,
"include_battery_temperature": false,
"generator_type": "ac",
"generator_nrofphases": 1,
"include_engine_hours": false,
"include_starter_voltage": false,
"include_history_energy": false,
"grid_nrofphases": 1,
"include_motor_temp": false,
"include_controller_temp": false,
"include_coolant_temp": false,
"include_motor_rpm": true,
"include_motor_direction": true,
"position": 0,
"pvinverter_nrofphases": 1,
"switch_count": 1,
"switch_1_type": 1,
"switch_2_type": 1,
"switch_3_type": 1,
"switch_4_type": 1,
"fluid_type": 0,
"include_tank_battery": false,
"include_tank_temperature": false,
"tank_battery_voltage": 3.3,
"tank_capacity": 0.2,
"temperature_type": 2,
"include_humidity": false,
"include_pressure": false,
"include_temp_battery": false,
"temp_battery_voltage": 3.3,
"x": 690,
"y": 680,
"wires": []
},
{
"id": "cb198d16bdb66233",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/Fix",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/Fix",
"name": "/Fix",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 380,
"y": 720,
"wires": []
},
{
"id": "c3f982038f1b601b",
"type": "victron-output-custom",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"service": "com.victronenergy.gps/100",
"path": "/NrOfSatellites",
"serviceObj": {
"service": "com.victronenergy.gps/100",
"name": "Virtual gps (100)"
},
"pathObj": {
"path": "/NrOfSatellites",
"name": "/NrOfSatellites",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 420,
"y": 780,
"wires": []
},
{
"id": "42cfd865f7ce3a96",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "10",
"topic": "",
"payload": "1",
"payloadType": "num",
"x": 170,
"y": 720,
"wires": [
[
"cb198d16bdb66233"
]
]
},
{
"id": "8edb25545f83abc9",
"type": "inject",
"z": "8fb5afb2b0b585a3",
"g": "c8ca4cebfb312063",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "10",
"topic": "",
"payload": "15",
"payloadType": "num",
"x": 170,
"y": 780,
"wires": [
[
"c3f982038f1b601b"
]
]
}
]
maybe, maybe not: login to ssh and check the result to be sure.
Thank you Matthijs. Set Fix to 1, set USB GPS to a higher instance, I will report how boat page works tomorrow.
Jan, I like how you have done your filtering, I do not think it will work for me, the the variability in the latitude and longitude is larger than the distance travelled in one second so I need a lot of averaging.
For that I first need to figure out how to implement the Tailscale tunneling setup to SSH in remotely over multiple mobile provider NAT layers. We’ll get there eventually, until then the bigSSH node @ root will have to do.
And this is just the beginning, as I hinted on before, I’m working on an integrated sensor fusing solution based on cheap-as-you-can-get commercial-of-the-shelf tech from the open source friendly drone enthusiast community.
Previously unimaginable levels of realtime gps precision solutions are becoming readily available at the lower end of a triple digit cost base. Think centimeters at over 5Hz.
I have a small (6meter) dual engine boat project waiting for me to start experimenting with electronic anchoring, autopilot and follow me (swimming) functionality based on those drone flight controller developments. If I ever can find the time that is.
I’m quite sure that you can’t set a device instance that way.
For tailscale, see: GitHub - kwindrem/TailscaleGX: tailscale for Victron Energy GX devices
Thanks, I’ll revisit the ‘device instance’ driving ‘global priority’ route with the next f/w update. For Tailscale we are waiting for our Starlink router to arrive.
Virtual GPS instance 100, USB GPS instance 101, both local and remote consoles now use the USB GPS. Consistent but not what was wanted.

