CAN Bus Core Pro

Da das schlechte Wetter naht, habe ich mich jetzt mal daran gesetzt den Canbus mit der Ecoflow Geschichte ans laufen zu bekommen.

Daraufhin dann gemerkt, dass es keien CAN nodes gibt. Also ein bisschen gegoogelt und Socketcan gefunden.

Ich habs aber bisher nicht ans laufen bekommen.

Übers Node Red bekomme ich bei der Installation eine relativ lange Fehlermeldung, (sitze Grade nicht am Rechner, daher nicht angehangen)

Nach relativ langen gequäle es mit chatgpt ans laufen zu bekommen klappt es immer noch nicht und ich Dreh mich im Kreis mit Neuinstallationen.

Hierbei wurde node.js npm und ne Menge c++ nachinstalliert.

Vermutlich ist das Image jetzt eh hin :smiley: (Mir grauts schon davor zigbee wieder ans laufen zu bekommen)

Wie ist der vorgesehene weg mit dem Core Pro den Can zu nutzen ? Ich habe zumindest in der Doku nichts gefunden.

Ist in irgendeiner Form eine Terminierung vorgesehen ? Die ecoflow fordert zwingend eine Terminierung mit 120Ohm.

Grüße

Auf dem Core Pro ist eine 120Ohm Terminierung standardmäßig gesetzt per Jumper.

Du musst in der /boot/firmware/config.txt das hier einfügen, um den CAN-bus zu aktivieren:

# activate CAN interface
dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25
dtoverlay=spi-bcm2835

Am besten ganz ans Ende, bzw. im Bereich von “[all]”. Neustarten dann nicht vergessen.
In NR mach ich es dann folgendermaßen:

Aus der Join-Node ganz am Ende kommt dann ein Object raus mit allen Daten, die ankommen auf dem CAN-bus, fertig interpretiert nach dem Victron BMS Protokoll in diesem Fall.
Der Flow geht noch weiter, mit Dashboard Nodes etc. und einem automatischen Reset/Neustart des CAN-bus, sollten 5min lang keine Daten mehr kommen. Ich hänge den kompletten Flow mit an ist noch nicht final, aber wird so bzw. so ähnlich ins Image kommen.

[{"id":"5434bb2220a4967c","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"ab82272997b3a62f","type":"exec","z":"5434bb2220a4967c","command":"candump can0","addpay":"","append":"","useSpawn":"true","timer":"","winHide":false,"oldrc":false,"name":"","x":360,"y":120,"wires":[["3f36b14dfbd14a5b","e9ad06d7282fb1b5"],[],[]]},{"id":"94fc24085f2d6b90","type":"inject","z":"5434bb2220a4967c","name":"start","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":150,"y":120,"wires":[["ab82272997b3a62f"]]},{"id":"3f36b14dfbd14a5b","type":"debug","z":"5434bb2220a4967c","name":"debug 11","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":540,"y":80,"wires":[]},{"id":"2393357f4d335765","type":"function","z":"5434bb2220a4967c","name":"get single frame","func":"const line = msg.payload.trim();\nconst match = line.match(/can0\\s+([0-9A-F]+)\\s+\\[(\\d+)\\]\\s+([0-9A-F\\s]+)/i);\n\nif (!match) {\n  // Ignore if incoming data is not a valid CAN frame\n  return null;\n}\n\nconst canid = parseInt(match[1], 16);\nconst dlc = parseInt(match[2], 10);\nconst bytes = match[3].trim().split(/\\s+/).map(x => parseInt(x, 16));\n\nmsg.payload = {\n  timestamp: Date.now(),\n  ext: false,\n  canid,\n  dlc,\n  rtr: false,\n  data: bytes,\n  rawData: bytes.map(b => `0x${b.toString(16).padStart(2, \"0\")}`)\n};\n\n// Set the topic to the incoming CAN-Id\nmsg.topic = canid\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":120,"wires":[["fa9dcd9c440a5c0c","aa3b32fc48cea7d7","e0f8a3d625bcb2d2"]]},{"id":"e9ad06d7282fb1b5","type":"split","z":"5434bb2220a4967c","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":530,"y":120,"wires":[["2393357f4d335765"]]},{"id":"fa9dcd9c440a5c0c","type":"debug","z":"5434bb2220a4967c","name":"Parsed Output","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":880,"y":80,"wires":[]},{"id":"aa3b32fc48cea7d7","type":"function","z":"5434bb2220a4967c","name":"CAN-bus BMS Protocol Victron (combined CAN-IDs)","func":"// Combined CAN-BMS parser (Victron spec)\n// Emits ONE msg per incoming frame with a small, well-named object.\n// Downstream: use a Join node (manual mode) to merge objects and emit after 0.5s idle.\n\nconst canid = msg.payload?.canid ?? msg.canid;\nconst b = Buffer.isBuffer(msg.payload?.data) ? msg.payload.data\n        : Buffer.isBuffer(msg.data)           ? msg.data\n        : Buffer.from(msg.payload?.data || []);\n\nif (typeof canid !== 'number' || b.length === 0) return null;\n\n// helpers\nconst u16LE = (off) => b.readUInt16LE(off);\nconst i16LE = (off) => b.readInt16LE(off);\nconst u32LE = (off) => b.readUInt32LE(off);\n\nconst toAscii = (buf8) => Buffer.from(buf8).toString('ascii').replace(/\\u0000+$/g, '').trim();\nconst toAscii7 = (buf8) => Buffer.from([...buf8].map(x => x & 0x7F)).toString('ascii').replace(/\\u0000+$/g, '').trim();\nconst cleanAscii = (buf8) => {\n  let s = toAscii(buf8);\n  if (/[^ -~]/.test(s)) s = toAscii7(buf8);\n  return s;\n};\n\n// build one small object per frame; keep unique key names so Join can merge cleanly\nlet out = null;\n\n// ---------------- core frames ----------------\nswitch (canid) {\n  case 0x351: {\n    const chargeVoltageLimit_V   = u16LE(0) * 0.1;   // CVL\n    let   maxChargeCurrent_A     = Math.abs(i16LE(2)) * 0.1; // CCL (positive)\n    let   maxDischargeCurrent_A  = Math.abs(i16LE(4)) * 0.1; // DCL (positive)\n    const batteryDischargeVoltageMin_V = u16LE(6) * 0.1;        // not used by Victron\n\n    flow.set('last_cvl_V', Number(chargeVoltageLimit_V.toFixed(1)));\n    \n    out = {\n      chargeVoltageLimit_V:      Number(chargeVoltageLimit_V.toFixed(1)),\n      maxChargeCurrent_A:        Number(maxChargeCurrent_A.toFixed(1)),\n      maxDischargeCurrent_A:     Number(maxDischargeCurrent_A.toFixed(1)),\n      batteryDischargeVoltageMin_V: Number(batteryDischargeVoltageMin_V.toFixed(1))\n    };\n    break;\n  }\n\n  case 0x355: {\n    const soc_percent = u16LE(0);\n    const soh_percent = b.length >= 4 ? u16LE(2) : null;\n    const highres_soc_percent = b.length >= 6 ? u16LE(4) * 0.01 : null;\n    out = {\n      soc_percent,\n      ...(soh_percent !== null ? { soh_percent } : {}),\n      ...(highres_soc_percent !== null ? { highres_soc_percent: Number(highres_soc_percent.toFixed(2)) } : {})\n    };\n    break;\n  }\n\n  case 0x356: {\n    const voltage_V    = i16LE(0) * 0.01;\n    const current_A    = i16LE(2) * 0.1;  // negative = discharge\n    const temperature_C= i16LE(4) * 0.1;\n    // remember last V & T for alarm plausibility\n    flow.set('last_voltage_V', Number(voltage_V.toFixed(2)));\n    flow.set('last_temperature_C', Number(temperature_C.toFixed(1)));\n    \n    out = {\n      voltage_V:     Number(voltage_V.toFixed(2)),\n      current_A:     Number(current_A.toFixed(2)),\n      temperature_C: Number(temperature_C.toFixed(1))\n    };\n    break;\n  }\n\n  case 0x35A: {\n  // --- helpers ---\n    const pick2bits = (byte, pairIdx, msbFirst) => {\n      // pairIdx 0..3\n      if (!msbFirst) {\n        // LSB-first: pairs are bits (0..1), (2..3), (4..5), (6..7)\n        return ((b[byte] ?? 0) >> (pairIdx * 2)) & 0b11;\n      } else {\n        // MSB-first: pairs are bits (6..7), (4..5), (2..3), (0..1)\n        const shift = 6 - (pairIdx * 2);\n        return ((b[byte] ?? 0) >> shift) & 0b11;\n      }\n    };\n\n    const decode = (code, reversed) => {\n      // normal: 10 = active, 01 = ok; reversed flips those\n      if (!reversed) return code === 0b10 ? 'active' : (code === 0b01 ? 'ok' : 'unsupported');\n      else           return code === 0b01 ? 'active' : (code === 0b10 ? 'ok' : 'unsupported');\n    };\n\n    const build = (msbFirst, reversed) => {\n      const P = (byte, idx) => decode(pick2bits(byte, idx, msbFirst), reversed);\n      const alarms = {\n        highVoltage:       P(0, 1),\n        lowVoltage:        P(0, 2),\n        highTemp:          P(0, 3),\n        lowTemp:           P(1, 0),\n        highTempCharge:    P(1, 1),\n        lowTempCharge:     P(1, 2),\n        highCurrent:       P(1, 3),\n        highChargeCurrent: P(2, 0),\n        contactor:         P(2, 1),\n        shortCircuit:      P(2, 2),\n        bmsInternal:       P(2, 3),\n        cellImbalance:     P(3, 0),\n      };\n      const warnings = {\n        highVoltage:       P(4, 1),\n        lowVoltage:        P(4, 2),\n        highTemp:          P(4, 3),\n        lowTemp:           P(5, 0),\n        highTempCharge:    P(5, 1),\n        lowTempCharge:     P(5, 2),\n        highCurrent:       P(5, 3),\n        highChargeCurrent: P(6, 0),\n        contactor:         P(6, 1),\n        shortCircuit:      P(6, 2),\n        bmsInternal:       P(6, 3),\n        cellImbalance:     P(7, 0),\n      };\n      const sysPair = pick2bits(7, 1, msbFirst); // byte7 bits 2–3\n      const system_status = decode(sysPair, reversed);\n\n      const anyActive = (o)=> Object.values(o).some(v=>v==='active');\n      return {\n        alarms, warnings, system_status,\n        anyAlarmActive:   anyActive(alarms),\n        anyWarningActive: anyActive(warnings),\n        _mode: { msbFirst, reversed }\n      };\n    };\n\n    // --- candidate decodes ---\n    const cand = [\n      build(false, false), // LSB + normal\n      build(false, true ), // LSB + reversed\n      build(true , false), // MSB + normal\n      build(true , true ), // MSB + reversed\n    ];\n\n    // --- plausibility scoring ---\n    const V  = flow.get('last_voltage_V');         // set in 0x356 case (we already do this)\n    const T  = flow.get('last_temperature_C');\n    const CVL= flow.get('last_cvl_V');             // capture in 0x351 case (see note below)\n    // infer nominal from CVL if available (very rough thresholds)\n    const nominal = (CVL && CVL < 20) ? 12 : (CVL && CVL < 65) ? 48 : null;\n\n    function score(c) {\n      let s = 0;\n      // Voltage plausibility (rough bands; tweak if needed)\n      if (typeof V === 'number') {\n        if (nominal === 12) {\n          if (c.alarms.highVoltage === 'active' && V < 14.6) s -= 2;\n          if (c.alarms.lowVoltage  === 'active' && V > 11.0) s -= 2;\n        } else if (nominal === 48) {\n          if (c.alarms.highVoltage === 'active' && V < 58.4) s -= 2;\n          if (c.alarms.lowVoltage  === 'active' && V > 44.0) s -= 2;\n        }\n      }\n      // Temperature plausibility\n      if (typeof T === 'number') {\n        if (c.alarms.highTemp === 'active' && T < 45) s -= 2;\n        if (c.alarms.lowTemp  === 'active' && T > 0)  s -= 2;\n      }\n      // Prefer fewer \"active\" overall\n      const act = Object.values(c.alarms).filter(v => v === 'active').length;\n      s -= act * 0.5;\n      return s;\n    }\n\n    // force override if set (per brand)\n    const forced = flow.get('bms_alarm_encoding'); // e.g. 'lsb-normal' | 'lsb-rev' | 'msb-normal' | 'msb-rev'\n    const forcedMap = {\n      'lsb-normal': 0, 'lsb-rev': 1, 'msb-normal': 2, 'msb-rev': 3\n    };\n\n    let chosen = cand[0];\n    if (forced && forcedMap.hasOwnProperty(forced)) {\n      chosen = cand[forcedMap[forced]];\n    } else {\n      // pick best by score\n      let best = -Infinity, bestIdx = 0;\n      cand.forEach((c, i) => { const sc = score(c); if (sc > best) { best = sc; bestIdx = i; }});\n      chosen = cand[bestIdx];\n    }\n\n    // emit & annotate which mapping won\n    out = {\n      ...chosen,\n      alarm_encoding: forced || (\n        chosen._mode.msbFirst\n          ? (chosen._mode.reversed ? 'msb-rev' : 'msb-normal')\n          : (chosen._mode.reversed ? 'lsb-rev' : 'lsb-normal')\n      )\n    };\n    delete out._mode;\n\n    break;\n  }\n\n  // ---------------- identity & meta ----------------\n  case 0x35E: {\n    const bms_name_fallback = cleanAscii(b.slice(0,8));\n    out = {bms_name_fallback };\n    break;\n  }\n\n  case 0x35F: {\n    const model_id             = u16LE(0);\n    const firmware_raw_BE      = b.readUInt16BE(2); // MSB first\n    const fwMajor              = (firmware_raw_BE >> 8) & 0xFF;\n    const fwMinor              = firmware_raw_BE & 0xFF;\n    const firmware_version     = `${fwMajor}.${fwMinor}`;\n    const onlineCapacity_Ah    = u16LE(4);\n    // stash online for later delta vs installed\n    flow.set('onlineCapacity_Ah', onlineCapacity_Ah);\n    out = {\n      model_id,\n      firmware_version,\n      firmware_raw_BE: firmware_raw_BE,\n      onlineCapacity_Ah\n    };\n    break;\n  }\n\n  case 0x370:\n  case 0x371: {\n    // 16-char name: 0x370 first half, 0x371 second half\n    const part = cleanAscii(b.slice(0, 8));\n\n    if (canid === 0x370) flow.set('name370', part);\n    if (canid === 0x371) {\n      flow.set('name371', part);\n\n      const p1 = flow.get('name370') || '';\n      const p2 = part; // freshly received\n      const deviceName_16 = (p1 + p2).slice(0, 16).trim();\n\n      // only emit when full name available\n      out = {\n        bms_name_16: deviceName_16,\n        bms_name_part1: p1,\n        bms_name_part2: p2\n      };\n    }\n    break;\n  }\n\n  // ---------------- module status & cell info ----------------\n  case 0x372: {\n    const nz = (v)=> v===0xFFFF ? null : v;\n    out = {\n      modules_ok:                 nz(u16LE(0)),\n      modules_blocking_charge:    nz(u16LE(2)),\n      modules_blocking_discharge: nz(u16LE(4)),\n      modules_offline:            nz(u16LE(6))\n    };\n    break;\n  }\n\n  case 0x373: {\n    const min_cell_mV   = u16LE(0);\n    const max_cell_mV   = u16LE(2);\n    const lowest_temp_K = u16LE(4);\n    const highest_temp_K= u16LE(6);\n    const toC = (K)=> (typeof K==='number') ? (K - 273.15) : null;\n    out = {\n      min_cell_mV,\n      max_cell_mV,\n      lowest_temp_K,\n      highest_temp_K,\n      lowest_temp_C:  Number(toC(lowest_temp_K).toFixed(1)),\n      highest_temp_C: Number(toC(highest_temp_K).toFixed(1))\n    };\n    break;\n  }\n\n  case 0x374:\n  case 0x375:\n  case 0x376:\n  case 0x377: {\n    const labelMap = {\n      0x374: 'min_cell_voltage_id',\n      0x375: 'max_cell_voltage_id',\n      0x376: 'min_cell_temp_id',\n      0x377: 'max_cell_temp_id'\n    };\n    const idTxt = cleanAscii(b.slice(0,8));\n    out = {[labelMap[canid]]: idTxt };\n    break;\n  }\n\n  // ---------------- energy & capacity ----------------\n  case 0x378: {\n    const energy_in_100Wh  = u32LE(0);\n    const energy_out_100Wh = u32LE(4);\n    out = {\n      energy_in_100Wh,\n      energy_out_100Wh,\n      energy_in_kWh:  Number((energy_in_100Wh  * 0.1).toFixed(1)),\n      energy_out_kWh: Number((energy_out_100Wh * 0.1).toFixed(1))\n    };\n    break;\n  }\n\n  case 0x379: {\n    const installedCapacity_Ah = u16LE(0);\n    flow.set('installedCapacity_Ah', installedCapacity_Ah);\n\n    const online = flow.get('onlineCapacity_Ah');\n    const offlineCapacity_Ah = (typeof online === 'number')\n      ? Math.max(0, installedCapacity_Ah - online)\n      : null;\n\n    out = {\n      installedCapacity_Ah,\n      ...(offlineCapacity_Ah !== null ? { offlineCapacity_Ah } : {})\n    };\n    break;\n  }\n\n  // ---------------- serial & family ----------------\n  case 0x380:\n  case 0x381: {\n    const part = cleanAscii(b.slice(0,8));\n    if (canid === 0x380) flow.set('sn1', part);\n    if (canid === 0x381) flow.set('sn2', part);\n    const sn = ((flow.get('sn1') || '') + (flow.get('sn2') || '')).slice(0,16).trim();\n    out = {\n      serial_number_part: part,\n      ...(sn ? { serial_number: sn } : {})\n    };\n    break;\n  }\n\n  case 0x382: {\n    const battery_family = cleanAscii(b.slice(0,8));\n    out = {battery_family };\n    break;\n  }\n}\n\n// if we recognized the canid, emit; else ignore\nif (!out) return null;\nmsg.topic = `0x${canid.toString(16)}`;  // good for debugging in Debug node\nmsg.payload = out;                       // no .source anywhere\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1000,"y":120,"wires":[["7c47df8d31819221"]]},{"id":"7c47df8d31819221","type":"join","z":"5434bb2220a4967c","name":"","mode":"custom","build":"merged","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":false,"accumulate":false,"timeout":"0.5","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":1270,"y":120,"wires":[["d460a6880c9dc87d","1dc5600ba9312622"]]},{"id":"d460a6880c9dc87d","type":"debug","z":"5434bb2220a4967c","name":"Parsed Werte","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":80,"wires":[]},{"id":"1dc5600ba9312622","type":"function","z":"5434bb2220a4967c","name":"add custom calculations & order","func":"// Post-Join: derive remaining capacity from SoC, add timestamps, stale detection\n\n// --- Energy helpers (0x378) ---\nfunction u32Delta(prev, cur) {\n  if (typeof prev !== 'number') return 0;\n  if (cur >= prev) return cur - prev;           // normal\n  const MAX = 0xFFFFFFFF;                        // rollover\n  return (MAX - prev + 1) + cur;\n}\n\n// --- Config ---\nconst STALE_THRESHOLD_MS = 5000; // >5s without a joined update => stale\nconst ROUND_AH = 1;\n\n// --- Input ---\nconst p = msg.payload || {};\nconst soc = (typeof p.soc_percent === 'number') ? p.soc_percent : null;\n\nconst bmsInstalled = (typeof p.installedCapacity_Ah === 'number') ? p.installedCapacity_Ah : null; // 0x379\nconst bmsOnline    = (typeof p.onlineCapacity_Ah    === 'number') ? p.onlineCapacity_Ah    : null; // 0x35F\n\n// --- Derivations ---\nlet remaining_fromInstalled_Ah = null;\nlet remaining_fromOnline_Ah    = null;\nlet usedCapacity_Ah_est        = null;\n\nif (typeof soc === 'number' && typeof bmsInstalled === 'number') {\n  remaining_fromInstalled_Ah = +(bmsInstalled * (soc / 100)).toFixed(ROUND_AH);\n  usedCapacity_Ah_est        = +((bmsInstalled - remaining_fromInstalled_Ah)).toFixed(ROUND_AH);\n}\n\nif (typeof soc === 'number' && typeof bmsOnline === 'number') {\n  // If your BMS reports online ~ installed*SoC, this will mirror that.\n  // Keep it explicit so semantics stay clear.\n  remaining_fromOnline_Ah = +(bmsOnline * (soc / 100)).toFixed(ROUND_AH);\n}\n\n// Health-style utilization: how much of installed capacity is currently online\nlet capacity_utilization_percent = null;\nif (typeof bmsInstalled === 'number' && bmsInstalled > 0 && typeof bmsOnline === 'number') {\n  capacity_utilization_percent = +((bmsOnline / bmsInstalled) * 100).toFixed(1);\n}\n\n// --- Timestamps ---\nconst nowMs  = Date.now();\n\nconst timestamp_ms  = nowMs;\nconst timestamp_iso = new Date(nowMs).toISOString();\n\n// --- Instantaneous power ---\nconst voltage_V = (typeof p.voltage_V === 'number') ? p.voltage_V : null;\nconst current_A = (typeof p.current_A === 'number') ? p.current_A : null;\nlet power_W = null, power_state = 'Unknown';\nif (typeof voltage_V === 'number' && typeof current_A === 'number') {\n  power_W = Math.round(voltage_V * current_A * 10) / 10;\n  const absPower = Math.abs(power_W);\n  power_W = absPower > 1000 ? Math.round(power_W) : Math.round(power_W * 10) / 10;\n  power_state = current_A > 0.1 ? 'Charging' : current_A < -0.1 ? 'Discharging' : 'Idle';\n}\n\n// --- Output (single assignment, ordered & grouped) ---\nmsg.payload = {\n  meta: {\n    timestamp_ms,\n    timestamp_iso,\n\n    model_id: p.model_id,\n    firmware_version: p.firmware_version,\n    firmware_raw_BE: p.firmware_raw_BE,\n    serial_number: p.serial_number,\n    serial_number_part: p.serial_number_part,\n    battery_family: p.battery_family,\n    bms_name_fallback: p.bms_name_fallback,\n    bms_name_16:   p.bms_name_16 ?? null,\n    bms_name_part1: p.bms_name_part1 ?? null,\n    bms_name_part2: p.bms_name_part2 ?? null\n  },\n\n  capacity: {\n    // BMS truth\n    installed_Ah: p.installedCapacity_Ah ?? null,\n    online_Ah:    p.onlineCapacity_Ah ?? null,\n    offline_Ah:   p.offlineCapacity_Ah ?? null,\n\n    // Effective & override\n    bmsInstalled_Ah:  (typeof bmsInstalled === 'number') ? bmsInstalled : null,\n\n    // SoC-derived estimates\n    remaining_fromInstalled_Ah: remaining_fromInstalled_Ah,\n    remaining_fromOnline_Ah:    remaining_fromOnline_Ah,\n    used_est_Ah:                usedCapacity_Ah_est,\n\n    // Health style\n    capacity_utilization_percent,\n    soc_percent: p.soc_percent,\n    soh_percent: p.soh_percent,\n    highres_soc_percent: p.highres_soc_percent\n  },\n\n  electrical: {\n    voltage_V: p.voltage_V,\n    current_A: p.current_A,\n    chargeVoltageLimit_V: p.chargeVoltageLimit_V,\n    maxChargeCurrent_A: p.maxChargeCurrent_A,\n    maxDischargeCurrent_A: p.maxDischargeCurrent_A,\n    batteryDischargeVoltageMin_V: p.batteryDischargeVoltageMin_V,\n\n    // Instantaneous power and state\n    power_W: power_W,\n    power_state: power_state,\n  },\n\n  thermal: {\n    temperature_C: p.temperature_C,\n    lowest_temp_K: p.lowest_temp_K,\n    highest_temp_K: p.highest_temp_K,\n    lowest_temp_C: p.lowest_temp_C,\n    highest_temp_C: p.highest_temp_C\n  },\n\n  cell_info: {\n    min_cell_mV: p.min_cell_mV,\n    max_cell_mV: p.max_cell_mV,\n    min_cell_voltage_id: p.min_cell_voltage_id,\n    max_cell_voltage_id: p.max_cell_voltage_id,\n    min_cell_temp_id: p.min_cell_temp_id,\n    max_cell_temp_id: p.max_cell_temp_id\n  },\n\n  module_status: {\n    modules_ok: p.modules_ok,\n    modules_blocking_charge: p.modules_blocking_charge,\n    modules_blocking_discharge: p.modules_blocking_discharge,\n    modules_offline: p.modules_offline\n  },\n\n  alarms: {\n    summary: {\n      anyAlarmActive: p.anyAlarmActive,\n      anyWarningActive: p.anyWarningActive,\n      system_status: p.system_status,\n      alarm_encoding: p.alarm_encoding ?? null,\n    },\n    details: {\n      alarms: p.alarms,\n      warnings: p.warnings\n    }\n  }\n};\n\nglobal.set(\"canbus_BMS\", msg.payload)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1470,"y":120,"wires":[["572560a665797336","e8bcb5ba0696edf2","2a2a19d3b39d2c08","d12df3ae2cd37871","3058cad586a49838","0b0cf1f63163411e","7326fad341383309","531e3e3cf679a0b6","61cbde4d55a8083a","cace18ac4081439e","52a6d3f36e3e61ce"]]},{"id":"572560a665797336","type":"debug","z":"5434bb2220a4967c","name":"Parsed","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1720,"y":80,"wires":[]},{"id":"e8bcb5ba0696edf2","type":"ui_template","z":"5434bb2220a4967c","group":"255107b99d3a7830","name":"SoC Template","order":1,"width":0,"height":0,"format":"<style>\n  .battery-container {\n    margin-top: 5px;\n    width: 100%;\n    max-width: 300px;\n    /* Adjust width as needed */\n    height: 10px;\n    /* Line height */\n    background: #2d3136;\n    /* Base line color */\n    position: relative;\n    border-radius: 5px;\n    overflow: hidden;\n  }\n\n  .battery-fill {\n    height: 100%;\n    width: 0%;\n    transition: width 0.5s ease-in-out;\n    background: linear-gradient(90deg, rgba(0, 255, 0, 0.7), rgba(0, 255, 0, 1), rgba(0, 255, 0, 0.7));\n    box-shadow: 0px 0px 10px rgba(0, 255, 0, 0.7);\n  }\n\n  .battery-percentage {\n    margin-top: 5px;\n    font-size: 16px;\n    font-weight: bold;\n    text-align: center;\n  }\n</style>\n\n<div class=\"battery-container\">\n  <div id=\"batteryFill\" class=\"battery-fill\"></div>\n</div>\n<div class=\"battery-percentage\" id=\"batteryPercentage\">0%</div>\n\n<script>\n  (function(scope) {\n    scope.$watch('msg.payload.capacity.soc_percent', function(soc) {\n      if (typeof soc === 'number' && soc >= 0 && soc <= 100) {\n        let fill = document.getElementById(\"batteryFill\");\n        let percentText = document.getElementById(\"batteryPercentage\");\n\n        fill.style.width = soc + \"%\";\n        percentText.innerHTML = `\n          <span style=\"float: left; margin-left: 5px;\">State of Charge</span> \n          <span style=\"float: right; margin-right: 5px;\">${soc}%</span>\n      `;\n\n        // Change glow color based on SOC level\n        if (soc < 20) {\n          fill.style.background = \"linear-gradient(90deg, rgba(255,0,0,0.7), rgba(255,0,0,1), rgba(255,0,0,0.7))\";\n          fill.style.boxShadow = \"0px 0px 20px rgba(255, 0, 0, 1), 0px 0px 40px rgba(255, 0, 0, 0.8), 0px 0px 60px rgba(255, 0, 0, 0.6)\";\n\n        } else if (soc < 50) {\n          fill.style.background = \"linear-gradient(90deg, rgba(255,165,0,0.7), rgba(255,165,0,1), rgba(255,165,0,0.7))\";\n          fill.style.boxShadow = \"0px 0px 20px rgba(255, 165, 0, 1), 0px 0px 40px rgba(255, 165, 0, 0.8), 0px 0px 60px rgba(255, 165, 0, 0.6)\";\n        } else {\n          fill.style.background = \"linear-gradient(90deg, rgba(0,255,0,0.7), rgba(0,255,0,1), rgba(0,255,0,0.7))\";\n          fill.style.boxShadow = \"0px 0px 20px rgba(0, 255, 0, 1), 0px 0px 40px rgba(0, 255, 0, 0.8), 0px 0px 60px rgba(0, 255, 0, 0.6)\";\n        }\n      }\n    });\n  })(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"md-card","x":1740,"y":160,"wires":[[]]},{"id":"3058cad586a49838","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":2,"width":"2","height":"1","name":"power_w","label":"","format":"{{msg.payload.electrical.power_W}} W","layout":"col-center","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":1720,"y":280,"wires":[]},{"id":"0b0cf1f63163411e","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":5,"width":"0","height":"0","name":"charging state","label":"Charging state:","format":"{{msg.payload.electrical.power_state}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":1740,"y":320,"wires":[]},{"id":"e03dbd91c52a4eb7","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":6,"width":0,"height":0,"name":"ttgo","label":"{{msg.payload.ttg.labelBadge}}","format":"{{msg.payload.ttg.pretty}}","layout":"col-center","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":2350,"y":120,"wires":[]},{"id":"2a2a19d3b39d2c08","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":3,"width":"2","height":"1","name":"current_V","label":"","format":"{{msg.payload.electrical.voltage_V}} V","layout":"col-center","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1720,"y":200,"wires":[]},{"id":"d12df3ae2cd37871","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":4,"width":"2","height":"1","name":"current_A","label":"","format":"{{msg.payload.electrical.current_A}} A","layout":"col-center","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1720,"y":240,"wires":[]},{"id":"531e3e3cf679a0b6","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":8,"width":"2","height":"1","name":"current temp","label":"Temp","format":"{{msg.payload.thermal.temperature_C}}°C","layout":"col-center","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":1730,"y":400,"wires":[]},{"id":"7326fad341383309","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":7,"width":"2","height":"1","name":"lowest Temp","label":"Lowest","format":"{{msg.payload.thermal.lowest_temp_C}}°C","layout":"col-center","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1730,"y":360,"wires":[]},{"id":"61cbde4d55a8083a","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":9,"width":"2","height":"1","name":"highest temp:","label":"Highest","format":"{{msg.payload.thermal.highest_temp_C}}°C","layout":"col-center","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1740,"y":440,"wires":[]},{"id":"cace18ac4081439e","type":"function","z":"5434bb2220a4967c","name":"caclulate effective Ah","func":"// Capacity Model Builder (robust)\n// - Infers full capacity from online_Ah & SoC, with smoothing + hysteresis\n// - Chooses between installed vs inferred full without flip-flopping\n// - Derives remaining_Ah consistently from chosen full & SoC\n// - Cross-checks with provided \"remaining\" fields if present\n\nconst p = msg.payload || {};\nconst cap = p.capacity || {};\n\n// Inputs\n// use high resolution soc percentage if available\nlet soc = (typeof cap.highres_soc_percent === 'number' && cap.highres_soc_percent != 0) ? cap.highres_soc_percent : (typeof cap.soc_percent === 'number') ? cap.soc_percent : null;\nconst onlineAh = (typeof cap.online_Ah === 'number') ? cap.online_Ah : null; // measured usable at current SoC\nconst installedAh = (typeof cap.installed_Ah === 'number') ? cap.installed_Ah : null; // nominal from device\n\nconst remOnline = (typeof cap.remaining_fromOnline_Ah === 'number') ? cap.remaining_fromOnline_Ah : null;\nconst remInstall = (typeof cap.remaining_fromInstalled_Ah === 'number') ? cap.remaining_fromInstalled_Ah : null;\n\n// Config\nconst TOL_REL_SWITCH = 0.06; // 6% to switch to inferred\nconst TOL_REL_BACK = 0.04; // 4% to switch back to installed (hysteresis)\nconst SOC_MIN_FOR_INFER = 5;  // avoid inferring full at very low SoC (unstable)\nconst SOC_MAX_FOR_INFER = 95; // avoid inferring full near 100%\nconst EMA_ALPHA = 0.25;       // smooth inferred full capacity\nconst ROUND_OUT = 1;          // dashboard rounding (decimals)\n\n// Clamp SoC for safety\nif (typeof soc === 'number') {\n  if (!isFinite(soc)) soc = null;\n  else soc = Math.min(100, Math.max(0, soc));\n}\n\n// 1) Inferred full from SoC & onlineAh (no rounding yet)\nlet inferredFullAh = null;\nif (typeof soc === 'number' && typeof onlineAh === 'number'\n  && soc >= SOC_MIN_FOR_INFER && soc <= SOC_MAX_FOR_INFER && soc > 0) {\n  inferredFullAh = onlineAh / (soc / 100);\n}\n\n// Smooth inferred full to reduce jitter (keep per-flow)\nlet infFullEma = flow.get('infFullEma');\nif (typeof inferredFullAh === 'number') {\n  infFullEma = (typeof infFullEma === 'number')\n    ? (EMA_ALPHA * inferredFullAh + (1 - EMA_ALPHA) * infFullEma)\n    : inferredFullAh;\n  flow.set('infFullEma', infFullEma);\n} else if (infFullEma != null) {\n  // keep last EMA if current inference is not valid\n  inferredFullAh = infFullEma;\n}\n\n// 2) Choose effective full with hysteresis\nlet priorChosen = flow.get('capacityFullSource'); // 'installed' | 'inferred' | null\nlet effectiveFullAh = null;\nlet fullSource = null;\n\nfunction relDiff(a, b) {\n  if (a == null || b == null) return null;\n  const denom = Math.max(Math.abs(a), 1e-6);\n  return Math.abs(a - b) / denom;\n}\n\nconst diffRel = (typeof installedAh === 'number' && typeof inferredFullAh === 'number')\n  ? relDiff(inferredFullAh, installedAh)\n  : null;\n\nif (installedAh == null && inferredFullAh != null) {\n  effectiveFullAh = inferredFullAh; fullSource = 'inferred_only';\n} else if (inferredFullAh == null && installedAh != null) {\n  effectiveFullAh = installedAh; fullSource = 'installed_only';\n} else if (inferredFullAh != null && installedAh != null) {\n  // Hysteresis around the switch threshold\n  if (priorChosen === 'inferred') {\n    if (diffRel != null && diffRel <= TOL_REL_BACK) {\n      effectiveFullAh = installedAh; fullSource = 'installed_preferred';\n    } else {\n      effectiveFullAh = inferredFullAh; fullSource = 'inferred_preferred';\n    }\n  } else { // prior installed or null\n    if (diffRel != null && diffRel > TOL_REL_SWITCH) {\n      effectiveFullAh = inferredFullAh; fullSource = 'inferred_preferred';\n    } else {\n      effectiveFullAh = installedAh; fullSource = 'installed_preferred';\n    }\n  }\n} else {\n  effectiveFullAh = null; fullSource = 'none';\n}\n\n// Persist chosen class for next time (collapsing *_preferred/only into a stable tag)\nif (fullSource.startsWith('inferred')) flow.set('capacityFullSource', 'inferred');\nelse if (fullSource.startsWith('installed')) flow.set('capacityFullSource', 'installed');\n\n// 3) Remaining Ah from chosen full & SoC (primary), with cross-checks\nlet remainingAh = null;\nif (typeof effectiveFullAh === 'number' && typeof soc === 'number') {\n  remainingAh = effectiveFullAh * (soc / 100);\n}\n\n// If BMS gave explicit remaining values, use as sanity hints (not override by default)\nlet remainingHints = {};\nif (typeof remOnline === 'number') remainingHints.remOnline = remOnline;\nif (typeof remInstall === 'number') remainingHints.remInstall = remInstall;\n\n// Optional: if you want to trust “remaining_fromInstalled” when you chose installed:\nif (remainingAh == null && fullSource.includes('installed') && typeof remInstall === 'number') {\n  remainingAh = remInstall;\n}\n// Optional: if you want to trust “remaining_fromOnline” when you chose inferred:\nif (remainingAh == null && fullSource.includes('inferred') && typeof remOnline === 'number') {\n  remainingAh = remOnline;\n}\n\n// 4) Deficit to full\nlet deficitToFullAh = null;\nif (typeof effectiveFullAh === 'number' && typeof remainingAh === 'number') {\n  deficitToFullAh = Math.max(0, effectiveFullAh - remainingAh);\n}\n\n// 5) Output (rounded for presentation only)\nconst capacity_model = {\n  effective_full_Ah: (typeof effectiveFullAh === 'number') ? +effectiveFullAh.toFixed(ROUND_OUT) : null,\n  remaining_Ah: (typeof remainingAh === 'number') ? +remainingAh.toFixed(ROUND_OUT) : null,\n  deficit_to_full_Ah: (typeof deficitToFullAh === 'number') ? +deficitToFullAh.toFixed(ROUND_OUT) : null,\n  source: {\n    chosen: (fullSource || 'none'),\n    inferred_full_Ah: (typeof inferredFullAh === 'number') ? +inferredFullAh.toFixed(ROUND_OUT) : null,\n    installed_full_Ah: installedAh,\n    diff_percent: (diffRel != null) ? +(diffRel * 100).toFixed(1) : null,\n    tolerance_switch_percent: +(TOL_REL_SWITCH * 100).toFixed(1),\n    tolerance_back_percent: +(TOL_REL_BACK * 100).toFixed(1),\n    soc_used_percent: soc,\n    hints: remainingHints\n  }\n};\n\nmsg.payload = { ...p, capacity_model };\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1760,"y":120,"wires":[["64e5df544a7bd8c8","0e20759b05ac12bb"]]},{"id":"e1d5045a0a309fc2","type":"debug","z":"5434bb2220a4967c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":2150,"y":80,"wires":[]},{"id":"64e5df544a7bd8c8","type":"function","z":"5434bb2220a4967c","name":"actual ttgo","func":"// Unified TTG/TTF estimator (single result depending on mode)\n// Adds: pretty formatter for long durations + badge string\n// Keeps: 'hhmm' for exact debugging\n\n// --- Config ---\nconst IDLE_THRESHOLD_A = 0.15;   // |I| below => idle\nconst ROUND_HOURS      = 2;     // decimals for hours output\nconst RESERVE_SOC_PCT  = 0;     // e.g. 10 -> keep 10% in tank for TTE\nconst MAX_DAYS_CLAMP   = 120;   // show as >120d if exceeded (optional clamp)\n\n// --- Inputs ---\nconst p  = msg.payload || {};\nconst cm = p.capacity_model || {};\nconst I  = (typeof p.electrical?.current_A === 'number') ? p.electrical.current_A : null;\n\n// Optional: smooth current with EMA so TTG doesn't jump around\nconst EMA_ALPHA = 0.3;\nlet I_ema = flow.get('I_ema');\nif (typeof I === 'number') {\n  I_ema = (typeof I_ema === 'number') ? (EMA_ALPHA * I + (1 - EMA_ALPHA) * I_ema) : I;\n  flow.set('I_ema', I_ema);\n}\nconst I_used = (typeof I_ema === 'number') ? I_ema : I;\n\n// Capacity bases from model\nlet remainingAh     = (typeof cm.remaining_Ah === 'number')      ? cm.remaining_Ah      : null;\nconst effectiveFull = (typeof cm.effective_full_Ah === 'number') ? cm.effective_full_Ah : null;\nconst deficitToFull = (typeof cm.deficit_to_full_Ah === 'number')? cm.deficit_to_full_Ah: null;\n\n// Optional reserve: reduce remaining by reserve portion of full cap\nif (RESERVE_SOC_PCT > 0 && typeof effectiveFull === 'number' && typeof remainingAh === 'number') {\n  const reserveAh = effectiveFull * (RESERVE_SOC_PCT / 100);\n  remainingAh = Math.max(0, remainingAh - reserveAh);\n}\n\n// Helpers\nfunction toHhMm(hours) {\n  if (typeof hours !== 'number' || !isFinite(hours) || hours < 0) return null;\n  const totalMin = Math.round(hours * 60);\n  const h = Math.floor(totalMin / 60);\n  const m = totalMin % 60;\n  return `${h}:${m.toString().padStart(2,'0')}`;\n}\n\n// Compact human formatter for long durations\nfunction fmtDuration(hours) {\n  if (hours == null || !isFinite(hours)) return '–';\n  if (hours <= 0) return '0m';\n\n  const totalMin   = Math.round(hours * 60);\n  const totalHours = Math.floor(totalMin / 60);\n  const mins       = totalMin % 60;\n  const days       = Math.floor(totalHours / 24);\n  const hoursLeft  = totalHours % 24;\n  const weeks      = Math.floor(days / 7);\n  const daysLeft   = days % 7;\n\n  // clamp very large values\n  if (totalHours > 24 * MAX_DAYS_CLAMP) return `>${MAX_DAYS_CLAMP}d`;\n\n  if (totalHours < 24)       return `${totalHours}:${mins.toString().padStart(2,'0')}`; // 7:15\n  if (days < 14)             return `${days}d${hoursLeft ? ` ${hoursLeft}h` : ''}`;     // 5d 6h\n  if (weeks < 26)            return `${weeks}w${daysLeft ? ` ${daysLeft}d` : ''}`;      // 3w 2d\n  const monthsApprox = Math.round(days / 30);\n  return `~${monthsApprox}mo`;\n}\n\nfunction iconFor(mode) {\n  return mode === 'discharging' ? '🔻'\n       : mode === 'charging'    ? '⚡'\n       : mode === 'full'        ? '✅'\n       : '⏸';\n}\n\n// --- Compute unified result ---\nlet mode  = 'idle';\nlet hours = null;\nlet hhmm  = null;    // exact format (keep for debugging)\nlet label = 'Idle';\nlet note  = null;\n\nif (typeof I_used === 'number') {\n  if (I_used < -IDLE_THRESHOLD_A) {\n    // Discharging → Time To Empty\n    if (typeof remainingAh === 'number' && remainingAh > 0) {\n      hours = +((remainingAh / Math.abs(I_used))).toFixed(ROUND_HOURS);\n      hhmm  = toHhMm(hours);\n      mode  = 'discharging';\n      label = 'Time to 0%';\n    } else {\n      mode = 'discharging'; label = 'Time to 0%'; note = 'No remainingAh available';\n    }\n  } else if (I_used > IDLE_THRESHOLD_A) {\n    // Charging → Time To Full\n    if (typeof deficitToFull === 'number' && deficitToFull > 0) {\n      hours = +((deficitToFull / I_used)).toFixed(ROUND_HOURS);\n      hhmm  = toHhMm(hours);\n      mode  = 'charging';\n      label = 'Time to full';\n    } else if (deficitToFull === 0) {\n      mode = 'full'; label = 'Battery full'; hours = 0; hhmm = '0:00';\n    } else {\n      mode = 'charging'; label = 'Time to full'; note = 'No deficitToFull available';\n    }\n  } else {\n    // Idle band\n    mode = 'idle'; label = 'Idle';\n  }\n} else {\n  note = 'Missing current_A';\n}\n\n// Pretty string + badge (visual)\nlet pretty = fmtDuration(hours);\nconst labelBadge = `${label.toLocaleLowerCase().includes(\"idle\") ? \"\" : iconFor(mode)} ${label.toLocaleLowerCase().includes(\"idle\") ? \"\" : label} ${label.toLocaleLowerCase().includes(\"idle\") ? \"\" : iconFor(mode)}`\npretty = `${pretty && pretty !== '–' ? `${pretty}` : ''}`\nlet badge  = `${iconFor(mode)} ${label}${pretty && pretty !== '–' ? `: ${pretty}` : ''} ${iconFor(mode)}`;\nbadge = badge.toLocaleLowerCase().includes(\"idle\") ? \"\" : badge\n\n// Attach under msg.payload.ttg (single object)\nmsg.payload = {\n  ...p,\n  ttg: {\n    mode,                 // 'discharging' | 'charging' | 'idle' | 'full'\n    label,                // human-friendly label\n    hours,                // number (may be null)\n    hhmm,                 // exact \"H:MM\" (kept for debug/exports)\n    pretty,               // compact human-readable (days/weeks)\n    labelBadge,           // extra value for text node label\n    badge,                // e.g. \"Time to full: 5d 6h\"\n\n    // context for debugging:\n    current_A: I_used,\n    base_remaining_Ah: remainingAh,\n    deficit_to_full_Ah: deficitToFull,\n    idle_threshold_A: IDLE_THRESHOLD_A,\n    reserve_soc_percent: RESERVE_SOC_PCT || 0,\n    clamp_days: MAX_DAYS_CLAMP,\n    note\n  }\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1970,"y":120,"wires":[["e1d5045a0a309fc2","e3dc39043fb097c4"]]},{"id":"52a6d3f36e3e61ce","type":"ui_template","z":"5434bb2220a4967c","group":"255107b99d3a7830","name":"debug CAN BMS","order":9,"width":0,"height":0,"format":"<style>\n  .popup-debug-ovl {\n    display: none;\n    position: fixed;\n    z-index: 9999;\n    inset: 0;\n    background: rgba(0, 0, 0, 0.6);\n    justify-content: center;\n    align-items: center;\n  }\n\n  .popup-debug-ovl.is-open {\n    display: flex;\n  }\n\n  .popup-debug-box {\n    background: var(--nrdb-card-bg, #1e1e1e);\n    color: var(--nrdb-text, #eee);\n    border-radius: 8px;\n    width: 90%;\n    max-width: 800px;\n    max-height: 80%;\n    overflow: auto;\n    padding: 16px;\n    box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5);\n    font-family: ui-monospace, Menlo, Consolas, monospace;\n    white-space: pre-wrap;\n  }\n\n  .popup-debug-hdr {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin-bottom: 8px;\n  }\n\n  .popup-debug-title {\n    margin: 0;\n    font-size: 16px;\n  }\n\n  .popup-debug-close {\n    cursor: pointer;\n    font-size: 20px;\n    line-height: 1;\n  }\n</style>\n\n<!-- This <md-button> mirrors the stock dashboard button structure/classes -->\n<md-button class=\"md-raised md-button md-ink-ripple md-primary\" type=\"button\" ng-click=\"popupOpen = true\"\n  aria-label=\"{{msg.debugButtonAria || 'Show all values'}}\">\n  <ui-icon ng-show=\"msg.debugButtonIcon\" icon=\"{{msg.debugButtonIcon}}\"></ui-icon>\n  <span ng-bind-html=\"(msg.debugButtonLabel || 'Debug CAN-bus BMS')\"></span>\n  <div class=\"md-ripple-container\" style></div>\n</md-button>\n\n<!-- Modal popup -->\n<div class=\"popup-debug-ovl\" ng-class=\"{'is-open': popupOpen}\" ng-click=\"popupOpen=false\">\n  <div class=\"popup-debug-box\" ng-click=\"$event.stopPropagation()\">\n    <div class=\"popup-debug-hdr\">\n      <h4 class=\"popup-debug-title\" ng-bind=\"msg.debugTitle || 'CAN-bus BMS Data'\"></h4>\n      <span class=\"popup-debug-close\" ng-click=\"popupOpen=false\" aria-label=\"Close\">&times;</span>\n    </div>\n    <pre>{{msg.payload | json:2}}</pre>\n  </div>\n</div>\n\n<script>\n(function(scope){\n  const btn   = document.getElementById(\"showDebugBtn\");\n  const popup = document.getElementById(\"debugPopup\");\n  const close = document.getElementById(\"closeDebug\");\n  const content = document.getElementById(\"debugContent\");\n\n  btn.onclick = () => popup.style.display = \"flex\";\n  close.onclick = () => popup.style.display = \"none\";\n  popup.onclick = (e) => { if (e.target === popup) popup.style.display = \"none\"; };\n\n  scope.$watch('msg', function(msg){\n    if(!msg || !msg.payload) return;\n    content.textContent = JSON.stringify(msg.payload, null, 2);\n  });\n})(scope);\n</script>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1750,"y":480,"wires":[[]]},{"id":"27939ff7ae5d324a","type":"function","z":"5434bb2220a4967c","name":"latest data age","func":"// Node-RED Function node: pretty-print data age if canBus == true\n\n\nglobal.set(\"canBus\", true) // Remove this line for production!\n\n// Only run if canBus flag is true\nif ((global.get(\"canBus\") || false) !== true) {\n    return null; // stop the flow\n}\n\n// Get saved timestamp safely\nconst bms = global.get('canbus_BMS');\nlet ts = bms && bms.meta && bms.meta.timestamp_ms;\n\n// Validate: undefined/null/NaN -> \"unknown\"\nif (ts === undefined || ts === null) {\n    msg.payload = \"unknown\";\n    return msg;\n}\n\n// Accept strings like \"1699999999999\"\nif (typeof ts === \"string\" && ts.trim() !== \"\") {\n    ts = Number(ts);\n}\n\nif (!Number.isFinite(ts)) {\n    msg.payload = \"unknown\";\n    return msg;\n}\n\n// Heuristic: if looks like seconds, convert to ms\nif (ts < 1e11) { // definitely seconds, not ms\n    ts = ts * 1000;\n}\n\n// Compute difference\nconst now = Date.now();\nlet diffMs = now - ts;\nif (diffMs < 0) diffMs = 0; // clamp future timestamps\n\n// Pretty-print helper\nfunction pretty(ms) {\n    if (ms < 1000) return `${ms}ms ago`;\n    const sec = Math.floor(ms / 1000);\n    if (sec < 60) return `${sec}s ago`;\n    const m = Math.floor(sec / 60);\n    if (m < 60) return `${m}m ${sec % 60}s ago`;\n    const h = Math.floor(m / 60);\n    if (h < 24) return `${h}h ${m % 60}m ago`;\n    const d = Math.floor(h / 24);\n    const remH = h % 24;\n    return `${d}d ${remH}h ago`;\n}\nmsg.payload = pretty(diffMs);\n\n// Optional: Node status indicator\nlet color = \"green\";\nif (diffMs >= 10000 && diffMs < 60000) color = \"yellow\"; // delayed\nelse if (diffMs >= 60000) color = \"red\";                  // dropped\nnode.status({ fill: color, shape: \"dot\", text: msg.payload });\n\nmsg.payload = color != \"green\" ? `<font color=\"${color}\">${msg.payload}` : msg.payload\n\n// Create key to indicate if a CAN-bus restart is needed\nlet restartNeeded = false\nif (diffMs >= 300000) restartNeeded = true\n\n// Build age output\nmsg.age = { lastTimestampMs: ts, diffMs , restartNeeded};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1740,"y":520,"wires":[["7efdce795477b955","b61a54a078f39ab3","aa606f46a214a379"]]},{"id":"e299846e2382bcdc","type":"inject","z":"5434bb2220a4967c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":"15","topic":"","payload":"","payloadType":"date","x":1530,"y":520,"wires":[["27939ff7ae5d324a"]]},{"id":"7efdce795477b955","type":"debug","z":"5434bb2220a4967c","name":"debug 15","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1940,"y":560,"wires":[]},{"id":"d29c3c2ffda9035d","type":"inject","z":"5434bb2220a4967c","name":"kill","props":[{"p":"payload"},{"p":"kill","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":160,"wires":[["ab82272997b3a62f"]]},{"id":"b61a54a078f39ab3","type":"ui_text","z":"5434bb2220a4967c","group":"255107b99d3a7830","order":9,"width":"0","height":"0","name":"latest data age","label":"Latest data age:","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1960,"y":520,"wires":[]},{"id":"e0d011e9f02da4f9","type":"link out","z":"5434bb2220a4967c","name":"can bus data age","mode":"link","links":["9ad588551b99c6b9"],"x":2085,"y":480,"wires":[]},{"id":"9ad588551b99c6b9","type":"link in","z":"5434bb2220a4967c","name":"inject can bus","links":["e0d011e9f02da4f9"],"x":145,"y":280,"wires":[["cbcc044ba366a677"]]},{"id":"aa606f46a214a379","type":"function","z":"5434bb2220a4967c","name":"restart needed?","func":"if (msg.age.hasOwnProperty(\"restartNeeded\") && msg.age.restartNeeded == true) {\n    return msg;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1960,"y":480,"wires":[["e0d011e9f02da4f9"]]},{"id":"cbcc044ba366a677","type":"function","z":"5434bb2220a4967c","name":"set cmd","func":"if ((global.get(\"canBus\") || false) == true) {\n    const bitrate = global.get(\"canBusBitrate\") || 500000\n    msg.payload = `sudo ip link set can0 down && sleep 1 && sudo ip link set can0 type can bitrate ${bitrate} restart-ms 100 && sleep 1 && sudo ip link set can0 up`\n    flow.set(\"canbusRestartTs\", Date.now())\n    return msg;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":280,"wires":[["c117be479c9d69e4","74b7bb488faf835d","33509cdce3f81b0b"]]},{"id":"c117be479c9d69e4","type":"exec","z":"5434bb2220a4967c","command":"","addpay":"payload","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"reset can interface","x":450,"y":280,"wires":[["ab82272997b3a62f"],[],[]]},{"id":"74b7bb488faf835d","type":"debug","z":"5434bb2220a4967c","name":"debug 17","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":420,"y":320,"wires":[]},{"id":"0e20759b05ac12bb","type":"debug","z":"5434bb2220a4967c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1970,"y":80,"wires":[]},{"id":"e3dc39043fb097c4","type":"delay","z":"5434bb2220a4967c","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"2","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"allowrate":false,"outputs":1,"x":2180,"y":120,"wires":[["e03dbd91c52a4eb7"]]},{"id":"2a55fff0d27c23fd","type":"mqtt out","z":"5434bb2220a4967c","name":"","topic":"test/can","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"0ddb07c65cbb68bc","x":1000,"y":180,"wires":[]},{"id":"aa6fad8be1d0e4d2","type":"mqtt in","z":"5434bb2220a4967c","name":"","topic":"test/can","qos":"2","datatype":"auto-detect","broker":"0ddb07c65cbb68bc","nl":false,"rap":true,"rh":0,"inputs":0,"x":850,"y":280,"wires":[["d57e491d10b1ece9"]]},{"id":"d57e491d10b1ece9","type":"debug","z":"5434bb2220a4967c","name":"Parsed Output","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":280,"wires":[]},{"id":"e0f8a3d625bcb2d2","type":"join","z":"5434bb2220a4967c","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":false,"accumulate":false,"timeout":"0.5","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":850,"y":180,"wires":[["040a1f72620b7099","2a55fff0d27c23fd"]]},{"id":"040a1f72620b7099","type":"debug","z":"5434bb2220a4967c","name":"Parsed Output","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":240,"wires":[]},{"id":"33509cdce3f81b0b","type":"change","z":"5434bb2220a4967c","name":"","rules":[{"t":"set","p":"kill","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":280,"y":240,"wires":[["ab82272997b3a62f"]]},{"id":"255107b99d3a7830","type":"ui_group","name":"CAN BMS","tab":"deee53a800de461d","order":12,"disp":true,"width":6,"collapse":true,"className":""},{"id":"0ddb07c65cbb68bc","type":"mqtt-broker","name":"","broker":"http://localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""},{"id":"deee53a800de461d","type":"ui_tab","name":"Info","icon":"mi-info","order":1,"disabled":false,"hidden":false}]

Wenn du es erstmal in der Konsole probieren möchtest:

Um den CAN-bus zu starten auf dem RPI benutzt du die folgenden Befehle:

sudo ip link set can0 down
sudo ip link set can0 type can bitrate 500000 restart-ms 100
sudo ip link set can0 up

Die Bitrate musst du natürlich entsprechend anpassen. Mit candump can0 in der Konsole solltest du dann die CAN-Frames sehen können.

Die Exec-Node im Flow macht das Gleiche, die Frames werden dann aufgeteilt in einzelne Frames (falls die Exec-Node mehrere aufeinmal ausgibt), dann durch das Protokoll interpretiert und wieder zusammengefügt, sodass ein einzelnes Object am Ende entsteht und wir damit weiterarbeiten können. Lass dich nicht verwirren von allem was im Flow danach kommt, da wird noch einiges dazugerechnet für QoL/UI/UX usw. :smiley:

Für das Umschreiben der Frames/Bytes lohnt es sich eine KI deiner Wahl zu befragen, die können das recht gut.

EDIT: Mit der Socketcan-Node hatte ich auch mal rumgespielt, das Problem dabei war aber, dass der CAN-bus erst aktiviert sein muss mit can0 up, dann kann erst NR gestartet werden damit es funktioniert. Also müsste man bei jedem Neustart des CAN-bus auch NR neustarten.

Moin,
Danke für die schnelle Hilfe :slight_smile:
Ich hab grade ein neues Image aufgesetzt, da ich mir mein altes beim Versuch Socketcan zu installieren zerschossen habe (Kein Backup, kein Mitleid :D) Node Red läuft zwar noch, aber alle Nodes sind nichtmehr installiert, hab da wohl nen Packetmanager zerstört. Egal :smiley:

Wo stelle ich denn die Bitrate über Nodered ein ? konnte da in dem Flow nichts finden.
Konsole ist für mich immer eher das notwenige Übel :stuck_out_tongue:
Ist das der Teil hier in der Config ?

oscillator=16000000

Der Rest wirklich erstmal verständlich :slight_smile:

Das mit dem Abschlusswiderstand ist Gold wert, hatte mir schon eine fliegende Konstruktion gebastelt, um das ganze Steckbar zur halten, so muss ich mir nur ein Patchkabel Crimpen.

oscillator=16000000

Das ist die Frequenz des MCP2515, also 16MHz. Die Bitrate wird in dem Flow beim Neustarten gesetzt, in der Function-node “set-cmd” vor dem “reset can interface” links unten. Hab die dort als globale Variable drin mit fallback zu 500000, damit man sie später aus dem Dashboard heraus einstellen kann.
Initial wird der CAN-bus aber noch gar nicht gestartet in dem Flow, du musst also mindestens einmal auf der Konsole starten, dann in NR die “candump can0” Exec-Node triggern, dann sollten dort Daten ankommen

EDIT: Oder du hängst die vor das “set cmd” einfach ein Inject, dann kannst du es auch aus NR triggern.

1 Like

Ich habe das mal soweit alles befolgt.

Zur nutzung von Candump musste ich noch Canutils nachinstallieren, vorher war der Befehl unbekannt.
jedoch bekomme ich weder über candumb can0 in der Konsole, noch über Nodered irgendwas rein.

Was ich jetzt gemacht habe:
-Den Canport in der Config aktiviert

-Can utils nachinstalliert.

-Neustart

-Baudrate auf 250k gestellt (Ecoflow Standard)

Wenn ich jetzt Candumb can0 eingebe in der Konsole passiert nichts weiter.

Hab noch kein oszi drangehangen, auf dem Can scheint aber einiges los zu sein, wenn ich mir den Pegel mit einem Multimeter angucke.

Laut Pinout des Core Pro hängt der Can auf Pin7 (High) und Pin8 (Low)

Ecoflow Pin1 (High) Pin2(Low)

Was ergeben diese Befehle:

dmesg | grep -i can
ip -details link show can0

Vielleicht auch noch

dmesg | grep spi > zeigt, ob SPI-Kommunikation sauber läuft.
lsmod | grep mcp251x > Treiber geladen?
sudo ethtool -i can0 > zeigt verwendeten Treiber.

mach dir mal eine zweite Konsole auf, in der ersten lässt du candump can0 laufen, in der zweiten schickst du selber einen Frame:

cansend can0 123#11223344

Kannst du den in der ersten sehen? Das geht nur, wenn mindestens ein zweites Gerät auf dem CAN-bus hängt.
Ist ansonsten dieVerkabelung korrekt? High auf High, Low auf Low? Die EcoFlow ist auch entsprechend terminiert?

dmesg | grep -i can
[ 3.636410] CAN device driver interface
[ 3.674777] mcp251x spi0.0 can0: MCP2515 successfully initialized.
[ 3.994791] vc4-drm axi:gpu: [drm] Cannot find any crtc or sizes
[ 3.997426] vc4-drm axi:gpu: [drm] Cannot find any crtc or sizes
[ 4.001667] vc4-drm axi:gpu: [drm] Cannot find any crtc or sizes
[ 5.048148] zram: Can’t change algorithm for initialized device
[ 24.030044] ieee80211 phy0: brcmf_escan_timeout: timer expired

ip -details link show can0
3: can0: <NOARP,ECHO> mtu 16 qdisc noop state DOWN mode DEFAULT group default qlen 10
link/can promiscuity 0 allmulti 0 minmtu 0 maxmtu 0
can state STOPPED restart-ms 0
mcp251x: tseg1 3..16 tseg2 2..8 sjw 1..4 brp 1..64 brp_inc 1
clock 8000000 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 parentbus spi parentdev spi0.0

Nach Up dann:

ip -details link show can0
3: can0: <NOARP,UP,LOWER_UP,ECHO> mtu 16 qdisc pfifo_fast state UP mode DEFAULT group default qlen 10
link/can promiscuity 0 allmulti 0 minmtu 0 maxmtu 0
can state ERROR-PASSIVE restart-ms 100
bitrate 250000 sample-point 0.875
tq 250 prop-seg 6 phase-seg1 7 phase-seg2 2 sjw 1 brp 2
mcp251x: tseg1 3..16 tseg2 2..8 sjw 1..4 brp 1..64 brp_inc 1
clock 8000000 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 parentbus spi parentdev spi0.0

dmesg | grep spi
[ 3.674777] mcp251x spi0.0 can0: MCP2515 successfully initialized.

lsmod | grep mcp251x
mcp251x 49152 0
can_dev 81920 1 mcp251x

sudo ethtool -i can0
driver: mcp251x
version: 6.12.34+rpt-rpi-2712
firmware-version:
expansion-rom-version:
bus-info: spi0.0
supports-statistics: no
supports-test: no
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: no

Selbst gesendete Nachrichten kommen nicht an.

Edit: Fehlersuche deutet auf einen Fehler auf Hardwarebene des Bus selber. Ich checke morgen nochmal die Verkabelung. Evtl muss der Canbus vom Ecoflow erst aufgeweckt werden, sollte sich das so geben, würde ich dann im anderen Thread weiter machen.

iIn den ip -details steht clock 8000000? Also 8MHz… Hast du in der config.txt richtig die 16MHz stehen?
Wenn wir aber davon ausgehen dass alles andere Hardwaremäßig richtig ist und Ecoflow 250kbit/s braucht, der MCP2515 aber eigentlich 16MHz macht, funktioniert dass dann wenn du bei 8MHz den CAN-bus mit 500kbit/s startest? :smiley:
Es gibt ansonsten noch einen Software-Loopback, dann kannst du den CAN-bus in der Konsole testen ohne dass ein zweites Gerät angeschlossen ist:

sudo ip link set can0 up type can bitrate 250000 loopback on

Sitzt du noch im Büro ? :smiley:

Ja ist korrekt

Habs testweise mal auf 32000000 gestellt. die 8000000 bleiben 8000000 unter ip

Loopback funktioniert

Edit: Laut der Quelle: Ecoflow doku basiert der Can auf J1939

Pin Definition
1 CAN_H Bus Line
2 CAN_L Bus Line
3 CAN_GND
4-8 Reserved

CAN_H = 3.5V, CAN_L = 1.5V, Vdiff = 2.0V

Bin grade etwas über die Spannungen verwirrt, hatte da was bei 5V im Kopf. und die Masse verwirrt mich, die ist ja nicht notwendig im Can, vllt aber für das Ecoflow system. Werde die morgen mal durchmessen auf die 12V DC Seite.
Hatte auch jetzt evtl noch einen trigger Eingang erwartet, jedoch scheinen nach dem Plan keine weiteren Pins belegt.

Ne, da war ich schon zuhause :smiley:

Die 8MHz zeigt er bei mir auch an, denke mal das ist eher was kosmetisches auf Kernel Ebene oder so.

Also intern ist der MCP2515 in Ordnung wenn das mit dem Loopback funktioniert.
Scheint mir ein Hardwareproblem zu sein. Kannst du mal versuchen eine gemeinsame Masse herzustellen?

:smiley:

Also, ich hab jetzt die Masse von Pin 3 des RJ45 der Ecoflow (Can Masse)
Auf den Schirm gelegt (Gleiches Potential wie 12V GND/ CorePro GND)

sobald ich den Can auf UP setze, springt er von “Stopped” auf “Error Passive”

Die Verkabelung muss passen. :thinking:

Was mir aufgefallen ist, bei Stromlos habe ich am Core 120Ohm
Die Ecoflow hat 2xRJ45 für den CAN hier geht mit dem Multimeter gemessen auch was ab. (Am Core nichts, was ja logisch ist denke ich )
Für beide RJ45 gibt es einen Abschlusswiederstand als Stecker.
Messe ich jetzt in die Ecoflow rein, habe ich mit einem gesteckten Abschlusswiderstand bereits 60 Ohm am enderen RJ45 (Ohne das der Core Angeschlossen ist)
Lasse ich den Abschlusswiderstand weg, hab ich 111Ohm.

Wenn ich alles stromlos zusammenstecke (Core und Ecoflow ohne zusätzlichen Widerstand) und dann Messe, dann habe ich 54Ohm. Wundert mich trozdem irgendwie.

Funktionieren tut es trozdem nicht :smiley: Ergebniss wie oben, sofort Error Passive
.
Das hier stimmt ?


Edit: Was ich jetzt noch gemacht habe ist den GND Ecoflow Pin 3 auf Pin 3 des Core.

Sachemol, hier ist doch der Bus kurzgeschlossen ?

Betriebszustand ist aktuell,

Ecoflow an, Corepro an, Canbus nicht gestartet.

Und irgendwie sehe ich da weder 250 noch 500kbit/s?

Das oszi steht auf 1ys/div, dann haben wir da noch 1,5 oder 3ys/ bit?

Das sieht schon besser aus, das ist jetzt nur der Core Pro mit einem Abschlusswiderstand.

Über Cansend habe ich einen Frame geschickt auf 250kbit/s. Was mich hier wundert, Can High und Low sind nicht ganz symmetrisch, sondern driften ab. Zumindest bei höherer t/div. Bei gleicher Einstellung am oszi bekommen ich bin Ecoflow nur rauschen/flackern mit ein bisschen Can. Stecke ich den Core und den Ecoflow zusammen, habe ich das Bild aus dem letzten Post.

Interessant ist auch, das Cansend scheinbar dauerhaft ist, das Bild zumindest bleibt stehen.

Das der Transeiver kaputt ist, glaub ich eigentlich nicht. Ich werde gleich mal messen, ob der Canbus extern, der gleiche ist, wie der Canbus der Batterien.

Hast Du mal versucht 125kbit bei EcoFlow anzunehmen? Außerdem mag Ecoflow seine eigene Pin-Belegung auf dem RJ45 haben.

Hast Du mal gecheckt, auf welchen Pins der 120Ohm Widerstad im Ecoflow Abschlussstecker sitzt?

Es läuft!

Der Can läuft auf 1 Mbit :woozy_face:

Über Candumb werde ich jetzt Totgeprügelt mit Nachrichten.

Nacher geht’s weiter.

1 Like

wie bist da jetzt drauf gekommen?

jetzt musst nur noch die Datensätze filtern und in die richtigen variablen übergeben .. in 5 min. ist fertig :joy:

:thinking: Da lässt wieder eine den Chef raushängen :rofl: :rofl:

:smiley:

Ich hätte erst derbe Störungen auf dem Bus.
(Sinus mit 50 Hz, auf dem der Canbus schwung ?) super Merkwürdig.
Heute war das einfach weg vermutlich hatte ich den Bus irgendwie in Störung bekommen, dann konnte ich das am Oszi über die Zeitdauer zwischen zwei Flankenwechseln sehen, bzw umrechnen in Kbit/s. und das ergebnis war eben 1000 :smiley:

Was mir jetzt aber viel größere Sorgen bereitet, das vermutlich diese Dokumentation dann nicht den Ecoflow Bus beschreibt, sondern den RVC Bus des PowerLink :confused:

Glückwunsch - da war meine Idee der anderen Bitrate ja nicht so verkehrt. - Nur in die falsche Richtung.

Da Ecoflow weder bei OpenCan noch bei DeviceNet registriert ist, beginnt jetzt ein sehr mühsames debudding :frowning:

Japp! - J1939 - Das wird nicht viel mit der Kommunikation auf dem Ecoflow-Can gemein haben…