Now that RV-C is working properly, it would be great if all of the RV-C devices could be discovered and written to in Node Red. It would be way cool to use the Cerbo as a gateway and build some panels to interact with 3rd party devices.
For anyone that runs across this, it’s essentially already implemented and works well. You just need to install the socketcan palette, which will be set to communicate on can0. Here is an example flow that will switch an output on a Firefly G12 and read the status back. Just be careful to filter your DGN’s so you aren’t creating a giant cpu load. If you do overload the cpu, node-red will stop responding. You can put node red into safe mode to resolve it. You will need the flowfuse dashboard for the button group.
[
{
"id": "924a78e23a6b67c1",
"type": "tab",
"label": "Flow 2",
"disabled": false,
"info": "",
"env": []
},
{
"id": "05c780adab08450a",
"type": "socketcan-in",
"z": "924a78e23a6b67c1",
"name": "socketcan-in",
"config": "6ecb6c96f6214bc4",
"x": 870,
"y": 60,
"wires": []
},
{
"id": "03a88c8b3a09d176",
"type": "socketcan-out",
"z": "924a78e23a6b67c1",
"name": "socketcan-out",
"config": "6ecb6c96f6214bc4",
"x": 130,
"y": 60,
"wires": [
[
"0240f6de4bc36578"
]
]
},
{
"id": "5b6617a157262e0f",
"type": "function",
"z": "924a78e23a6b67c1",
"name": "Encode to G12",
"func": "// Generic function to send on/off to G12\n// Send DIMMER_COMMAND_2 (0x1FEDB)\n// Use true/false boolean input\n\nconst DGN = 0x1FEDB; // DGN for G12\nconst INSTANCE = 15; // set to G12 Output\nconst DEST_ADDR = 0x01; // adjust if you need a specific target SA\n\nfunction buildFrame(dgn, da, data) {\n return {\n canid: (dgn << 8) | (da & 0xFF),\n ext: true,\n rtr: false,\n dlc: data.length,\n data\n };\n}\n\n// Normalize incoming payload to a 0..251 \"desired level\"\nfunction toLevel(input) {\n let v = input;\n\n if (v && typeof v === \"object\") {\n if (\"desiredLevel\" in v) v = v.desiredLevel;\n else if (\"level\" in v) v = v.level;\n else if (\"on\" in v) return v.on ? 251 : 0;\n }\n\n if (typeof v === \"string\") {\n const s = v.trim().toLowerCase();\n if (s === \"on\" || s === \"true\") return 251;\n if (s === \"off\"|| s === \"false\") return 0;\n const n = Number(s);\n if (!Number.isNaN(n)) v = n;\n }\n\n if (typeof v === \"boolean\") return v ? 251 : 0;\n if (typeof v !== \"number\" || Number.isNaN(v)) return 0;\n\n // Treat 0..1 as fraction, 0..100 as percent, otherwise raw 0..251\n if (v > 0 && v <= 1) return Math.max(0, Math.min(251, Math.round(v * 251)));\n if (v >= 0 && v <= 100) return Math.max(0, Math.min(251, Math.round(v * 2.51)));\n return Math.max(0, Math.min(251, Math.round(v)));\n}\n\nlet group = 0;\nif (msg.payload && typeof msg.payload === \"object\" && \"group\" in msg.payload) {\n const g = Number(msg.payload.group);\n if (!Number.isNaN(g)) group = g & 0xFF;\n}\n\nconst level = toLevel(msg.payload);\n\n// Build and send frame: [instance, group, level]\nconst frame = buildFrame(DGN, DEST_ADDR, [INSTANCE, group, level]);\n\nreturn { payload: frame };\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 680,
"y": 60,
"wires": [
[
"05c780adab08450a"
]
]
},
{
"id": "9e3502d99bf9b11d",
"type": "ui-button-group",
"z": "924a78e23a6b67c1",
"name": "",
"group": "3df0d3490710d5a7",
"order": 4,
"width": "0",
"height": "0",
"label": "G12 Switch",
"className": "",
"rounded": true,
"useThemeColors": false,
"passthru": false,
"options": [
{
"label": "Off",
"icon": "",
"value": "false",
"valueType": "bool",
"color": "#d4d4d4"
},
{
"label": "On",
"icon": "",
"value": "true",
"valueType": "bool",
"color": "#f4bc25"
}
],
"topic": "topic",
"topicType": "msg",
"x": 490,
"y": 60,
"wires": [
[
"5b6617a157262e0f"
]
]
},
{
"id": "0240f6de4bc36578",
"type": "function",
"z": "924a78e23a6b67c1",
"name": "Decode G12",
"func": "// Generic DIMMER status → ui_switch (boolean)\n// Accepts PGNs: 0x1FEDB (command) and 0x1FEDA (status)\n// Set the instance to watch here:\nconst INSTANCE = 15; // ← change thisto the G12 output. (set to -1 to disable instance filtering)\n\nlet frame = msg.payload;\nif (!frame || typeof frame.canid !== \"number\" || !Array.isArray(frame.data)) return null;\n\nconst dgn = (frame.canid >>> 8) & 0x3FFFF;\nif (dgn !== 0x1FEDB && dgn !== 0x1FEDA) return null;\n\nconst instance = Number(frame.data[0] ?? -1);\nif (INSTANCE >= 0 && instance !== INSTANCE) return null;\n\n// Level at byte 2 (0..251). Any nonzero = ON\nconst level = Number(frame.data[2] ?? 0);\nconst on = level > 0;\n\n// Output to drive a ui_switch\nmsg.payload = on; // true/false\nmsg.topic = `device/${instance}/status`;\nmsg.meta = {\n dgn_hex: \"0x\" + dgn.toString(16).toUpperCase(),\n instance,\n level,\n sa: frame.canid & 0xFF\n};\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 310,
"y": 60,
"wires": [
[
"9e3502d99bf9b11d"
]
]
},
{
"id": "6ecb6c96f6214bc4",
"type": "socketcan-config",
"interface": "can0"
},
{
"id": "3df0d3490710d5a7",
"type": "ui-group",
"name": "Switches",
"page": "30186ff8bd45c991",
"width": "2",
"height": "1",
"order": 2,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "30186ff8bd45c991",
"type": "ui-page",
"name": "Page 1",
"ui": "d3ede36e308c0fa4",
"path": "/page1",
"icon": "home",
"layout": "grid",
"theme": "cc0aada03716a7fe",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 1,
"className": "",
"visible": "true",
"disabled": "false"
},
{
"id": "d3ede36e308c0fa4",
"type": "ui-base",
"name": "My Dashboard",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "none",
"titleBarStyle": "hidden",
"showReconnectNotification": true,
"notificationDisplayTime": 1,
"showDisconnectNotification": true,
"allowInstall": false
},
{
"id": "cc0aada03716a7fe",
"type": "ui-theme",
"name": "Default Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0094ce",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "comfortable",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
}
]
hi just a question about your setup, I can’t seem to get it to work I get error while creating channel. I think it could be port naming Mine are VE.Can set to RV-c?
Thanks
I believe mine is also VE.Can set to RV-C. I’ll take a look and confirm that next time I’m in front of it. Did you install the socketcan pallet? Did you try any other ports?
Yes socketcan is installed.
The RV-c works as I can see the devices in the networks status.
Thanks
Hi Erik, off topic, but can you elaborate more on the code behind “Decode G12” and “Encode G12” function node? I am trying to inject a RV-C command mimicking G12 to turn on AC, but since G12 send a off command every second, AC refuse to turn on. Thanks in advance.
Which portion is not working? What’s the specific error and where is it coming from?
Yes, it took me a while to sort out the AC. On my Firefly G12, sending a switch command to turn on the output for the AC, I experienced the same thing, it would turn off again. The solution is to not send a command to turn on the individual outputs that go to the AC unit. Use the RV-C spec commands for operating AC and a thermostat instead. It works just like it should and will stay in-sync with the Firefly. Take a look at my github project and download the node red flow for a functioning example. Everything is working except for the fan speed control, which I just need to get around to. GitHub - Erik98146/LaunchControl: A small and simple AMOLED control panel for Jayco Terrain and Entegra Launch adventure vans with Lithionics/Victron components. · GitHub
I did get it working it was just a typo on my end on the port name.
My question now is do you have any more info on your Decode G12 function, I will be doing decoding on a Tiffin Spyder and just wanted to understand your function as a starting point. Thanks in advance
The main things are the DGN and the Instance. For switching Firefly G12 outputs, for example, they are treated as dimmers and respond to the dimmer DGN from the RV-C spec. The instance number is the actual output of the G12 that you want to switch. The firefly will also respond to other commands, like the thermostat DGN. Unfortunately I’m away from my system for a bit, so I can’t give you better info. If you look over the official RV-C spec, it should start to make sense. I should be in front of my system again in a week or so of you still have questions, I’ll update my github with additional info on that aspect in the next week or two. You can see some additional examples if you look at the flow from my github: GitHub - Erik98146/LaunchControl: A small and simple AMOLED control panel for Jayco Terrain and Entegra Launch adventure vans with Lithionics/Victron components. · GitHub
Wow Erik, thanks for sharing, and what amazing project you got there! I am also planning on creating an E-ink display down the road for battery status, but I might change my mind now that I see your AMOLED display. But back to the question on AC control, so what I tried so far was recorded the RV-C THERMOSTAT_COMMAND_1 (1FEF9h) messages send by G12 to AC when I activated and deactivated it via the panel, identify those particular messages, and inject it via socketcan.in. I also tried the direct AIR_CONDITIONER_COMMAND (1FFE0h) and both attempts resulting in AC rapidly turning on and off, as G12 keeps sending its own 1FEF9h messages. My next attempt would be to physically isolate the RV-C wires going to AC, but sounds like you have figured it out how to tame the G12.
But what you said just gave me a new idea…may be I have been going about it wrong. Instead of mimicking G12 (for me that’s SA: 9C) and fight over the thermostat_command_1, how about mimicking the touch panel instead (SA: 9F) with the thermostat_command_1…
I wish I had my van at the moment, but it’s at the shop. Hopefully I’ll have it back by the weekend. Try loading the flow from the github link I posted, that will show the DGN’s I used for G12 AC that worked well. I’m pretty sure I used the thermostat commands, with some modifications. I decoded the DGN first, and recorded what was sent from the firefly touchscreen for AC control. Like I said, don’t try to switch the G12 outputs to the AC directly, it will create the exact problem you described.
BTW- I started the project with an e-ink display (adafruit magtag). Since I plan to keep my display and controls right next to where I sleep, I didn’t want an annoying LCD display, which is why I went the e-ink route. I wrote the code (with the help of AI) in python. It worked ok, but was a pain because the MQTT connections wasn’t all that reliable, and dealing with the e-ink screen refresh is really tricky and funky. I’ll go ahead and post the code for that to my git hub too, if you want to take a look, and a picture of the final product (magtag directory). The Waveshare AMOLED project is way better in every respect. It uses esphome (without home assistant). It’s super fast, very reliable, and easier to setup. The amoled display is gorgeous and only turns on the individual pixels needed, so it can be set to put no light, or nearly no light with a screen saver mode. Highly recommended.
I got access to my van for a bit and updated/finished the AC controls. You can find the updated flow in the latest release on my github. I also updated the labels a bit so it should all be really clear, hopefully. Everything but the room temp can be found at DGN 1FFE2. Here is a snip of the decode comments:
// RV-C Thermostat STATUS decoder → simple HVAC fan state
// DGN: 0x1FFE2
//
// RV-C fields used:
// Byte0: Instance
// Byte1:
// bits 0-3 = Operating mode
// bits 4-5 = Fan mode
// Byte2: Fan speed (uint8)
Encodes go to DGN 1FFE9 with the same bit layout. The room temp is decoded from 1FF9C.
Let me know how it works.
Can I get a copy of the decode G12 function, I looked on your GitHub but don’t see it?
Thanks
Here is a link to the flow: https://github.com/Erik98146/LaunchControl/releases/download/v0.90/LaunchControl-flow.json
Thank you Erik again…unfortunately I am missing some of the ui nodes from your json file so I couldn’t get it to deploy properly. If you don’t mind, I will just show it here:
I noticed your code mentioned destination address as 0x01? Is that interchangeable as source address, or are you able to “tell” your thermostat because it’s address is 0x01? Also what about priority?
// RV-C Thermostat Operating Mode Setter
// DGN: 0x1FEF9
// Input: true / false
// false -> Off (0)
// true -> Cool (1)
//
// Only modifies:
// Byte1 bits 0–3 = Operating Mode
// All other bytes = 0xFF (no-change)
const DGN = 0x1FEF9;
const INSTANCE = 0; // thermostat instance
const DEST_ADDR = 0x01;
function buildFrame(dgn, da, data) {
return {
canid: (dgn << 8) | (da & 0xFF),
ext: true,
rtr: false,
dlc: data.length,
data
};
}
// Normalize input → boolean
function toBool(v) {
if (v && typeof v === "object" && "value" in v) v = v.value;
if (typeof v === "boolean") return v;
if (typeof v === "number") return v !== 0;
if (typeof v === "string") {
const s = v.trim().toLowerCase();
if (s === "true" || s === "on" || s === "1") return true;
if (s === "false"|| s === "off"|| s === "0") return false;
}
return false;
}
const wantCool = toBool(msg.payload);
// Operating mode nibble only
const operatingMode = wantCool ? 1 : 0;
// Byte1: bits 0–3 used, rest untouched → set to 1s
// RV-C convention: 0xF0 means upper bits "no change"
const byte1 = (operatingMode & 0x0F) | 0xF0;
// Other bytes = no-change
const data = [
INSTANCE & 0xFF, // byte 0
byte1, // byte 1 (operating mode only)
0xFF, // fan speed unchanged
0xFF, 0xFF, // heat setpoint unchanged
0xFF, 0xFF // cool setpoint unchanged
];
const frame = buildFrame(DGN, DEST_ADDR, data);
return { payload: frame };
So I recorded the CAN traffic between my panel (0x9F) and G12 (0x9C) and this is what I observed:
When I goto the panel and turn on AC, Panel would send 6 commands total: 2 Thermostat command 1st and last, with 4 generic indicator command in between. Also in between are responds from G12, and I tried this 3 times and the sequence are always the same.
I am still testing but re-sending these 6 commands I was able to turn on AC via node-red. I am still working out the off part.
btw priority of all these messages are 6 for anyone wondering.
| SA: 0x9F | PGN: 0x1FEF9 | THERMOSTAT_COMMAND_1 | Data: 02 C1 FF FF FF FF FF FF | 1st Thermostat command from panel | |
|---|---|---|---|---|---|
| SA: 0x9F | PGN: 0x1FED9 | GENERIC_INDICATOR_COMMAND | Data: FF 96 67 0F 01 00 D1 FF | ||
| SA: 0x9C | PGN: 0x1FFE2 | THERMOSTAT_STATUS_1 | Data: 02 01 64 DF 24 25 25 00 | ||
| SA: 0x9F | PGN: 0x1FED9 | GENERIC_INDICATOR_COMMAND | Data: FF 96 67 0F FF FF D3 FF | ||
| SA: 0x9C | PGN: 0x15FCE | ??? | Data: 67 00 01 00 00 00 00 00 | ||
| SA: 0x9F | PGN: 0x1FED9 | GENERIC_INDICATOR_COMMAND | Data: FF 96 66 0F 00 00 D1 FF | ||
| SA: 0x9F | PGN: 0x1FED9 | GENERIC_INDICATOR_COMMAND | Data: FF 96 66 0F FF FF D3 FF | ||
| SA: 0x9C | PGN: 0x15FCE | ??? | Data: 66 00 C8 00 00 00 00 00 | ||
| SA: 0xEB | PGN: 0x1FFF7 | Data: C8 12 00 00 12 35 02 12 | |||
| SA: 0x9F | PGN: 0x1FEF9 | THERMOSTAT_COMMAND_1 | Data: 03 C0 FF FF FF FF FF FF | 2nd thermostat command from panel | |
| SA: 0x9C | PGN: 0x1FFE2 | Data: 03 00 00 25 25 25 25 00 | |||
| SA: 0xA0 | PGN: 0x1FFFD | Data: 01 77 07 01 00 A6 35 77 | |||
| SA: 0xA0 | PGN: 0x1FFFC | Data: 01 77 FF FF 7B 9F 04 00 | |||
| SA: 0x9F | PGN: 0x1FECA | Data: 05 84 FF FF FF FF FF FF | |||
| SA: 0xCF | PGN: 0x1FFE1 | AIR_CONDITIONER_STATUS | Data: A1 00 C8 C8 00 00 0C 00 | ||
| SA: 0xCF | PGN: 0x1FDC9 | Data: A1 00 00 00 00 00 00 00 | |||
| SA: 0xCF | PGN: 0x1FF9C | Data: A1 7C 26 00 00 00 00 00 | |||
| SA: 0xCF | PGN: 0x1FFE2 | THERMOSTAT_STATUS_1 | Data: A1 C0 00 FF FF AE 19 00 | ||
| SA: 0xCF | PGN: 0x1FEFA | Data: A1 FF FF 00 00 00 00 00 | |||
| SA: 0xFD | PGN: 0xFECA | Data: 05 FD FF FF FF FF FF FF | |||
| SA: 0x9C | PGN: 0x1FEF9 | THERMOSTAT_COMMAND_1 | Data: A1 10 C8 8A 24 AE 19 FF | thermostat command from G12 |
I should also mentioned that my motivation for this project is because my AC (Truma Aventa) on auto mode will keep fan on at all times and cycle compressor on/off. Not only is that noisy but it pushes the humidity level up fast whenever compressor is off, so I wanted to create a flow that would monitor a humidity sensor and turn on and off AC based on my humidity set points.
That’s a good reminder.. you need to add the flowfuse dashboard to the palette for the UI nodes. I’ll add that to the directions. 0x01 is the G12, which actually hosts the thermostat. Perhaps your G12 has a dest address of 0x9C. You might try starting with something simple like controlling a light first. The G12 essentially looks like a thermostat and handles everything a thermostat would, like thermostat hysteresis (which is why trying to directly switch outputs for the AC unit causes issues).
It sounds like your setup is a bit different than mine. I have learned that Firefly does some strange things that are not part of the RV-C spec. Check out the rvc-spec.yml file from this project. He has documented many of the strange things that Firefly does with the G12 that might point you in the right direction: rvc2mqtt/rvc2mqtt at unknown-dgns · dbergl/rvc2mqtt · GitHub
