Skip to content

Live data processing

The resol-vbus-core provides classes and functions to encoded and decode VBus data primitives from and to their "on-the-wire" representation as specified by the VBus Protocol Specification.

Encoding VBus data primitives

The three VBus data primitives Packet, Datagram and Telegram can be encoded by using their respective liveEncode___ functions:

The functions take their respective primitive as input and produce the "on-the-wire" representation as a Uint8Array buffer of bytes:

typescript
import { Packet, liveEncodePacket } from 'resol-vbus-core';

const frameData = new Uint8Array(27 * 4);
// TODO: fill `frameData` here

const packet = new Packet({
  destinationAddress: 0x0010,
  sourceAddress: 0x1911,
  command: 0x0100,
  frameCount: 27,
  frameData,
});

const packetAsBytes = liveEncodePacket(packet);
// TODO: send `packetAsBytes` on the wire

The library also exports a helper function liveEncode which take any of the three VBus data primitives.

Using the LiveEncoder class

The LiveEncoder class implements some best-practises for sending VBus data primitives over the wire:

  • Transmission time tracking
  • Power management
  • Suspends with optional timeouts
  • Idle management

Transmission time tracking

The VBus is a serial connection with 9.600 bits per second by default. Sending a byte of data takes roughly 1.042 milliseconds to transmit. The queue() instance function of the LiveEncoder class tracks the amount of data sent and estimates when the transmission should have completed. This prevents overwhelming the VBus with too much data while is it still working through a backlog of unsent data.

See queue() instance function API and baudrate constructor options API for details.

Power management

As lined out in the VBus specification the VBus is not only used to transport data, but also electrical power. When no data is transmitted, the VBus supply voltage can be used to charge up internal circuitry that provides enough energy for the phases when data is transmitted and VBus power supply fluctuates.

The LiveEncoder class has mechanism to split larger chunks of data into smaller ones and send each of them followed by a small break to allow attached devices to recharge if necessary.

See the following constructor option APIs for details:

Suspends with optional timeouts

The LiveEncoder class supports being suspended in two ways:

  • indefinetly using suspend()
  • for a certain time using suspendWithTimeout()

Both can be canceled using resume() which is necessary to send data.

The indefinite variant is useful for implementing a VBus slave that passively listens for a request from another device to answer that and go back to its passive state. This implementation would call suspend() right after initializing the LiveEncoder. Once the request to send an answer was received, resume() the encoder, queue() the answer and immediately suspend() again, which will automatically suspend after the data was transmitted.

The timeout-based variant is useful for implementing a VBus master that want to suspend for some time to give a requested device enough time to send an answer. In order to do that the master uses queue() to send the request and immediately suspendWithTimeout() which will automatically suspend after the data was transmitted. If the answer is received, the timeout can be prematurely canceled using resume().

See the following instance function APIs for details:

Idle management

The LiveEncoder is considered idle, if:

  • no data has been queued for transmission
  • the optional energy management subsystem is not gaining energy currently
  • the LiveEncoder is not suspended

If the LiveEncoder is idle, it calls the onIdle callback which can be used on a VBus master implementation to drive forward the communication statemachie, for example sending the next piece of information.

See the onIdle() constructor option API and the isIdle() instance function API for details.

Decoding VBus data primitives

The library provides two ways of decoding VBus data primitives that are encoded in their live "on-the-wire" representation:

  • using the liveDecodePacket, liveDecodeDatagram and liveDecodeTelegram functions
  • using the LiveDecoder class

The liveDecode___ functions for trusted data

The liveDecode___ functions can be used to decode a Uint8Array which is known to contain the respective live representation. Passing it an buffer containing incompatible data is undefined behaviour. If you do not trust the Uint8Array data it is safer to use the LiveDecoder class instead.

typescript
const { liveDecodeDatagram } from 'resol-vbus-core';

const bytes = new Uint8Array([
  0xAA, 0x20, 0x00, 0x11, 0x19, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
]);

const dgram = liveDecodeDatagram(bytes);
// TODO: do something with the datagram here

See the liveDecodePacket() function API, the liveDecodeDatagram() function API and liveDecodeTelegram() function API for details.

The LiveDecoder for everything else

The LiveDecoder class uses an internal buffer to buffer incoming data until a VBus data primitive can be decoded from it. It will then call the respective callbacks provided to the constructor of the LiveDecoder to inform its consumer about the decoded data.

typescript
const { LiveDecoder } from 'resol-vbus-core';

const bytes = new Uint8Array([
  0xAA, 0x20, 0x00, 0x11, 0x19, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
]);

const decoder = new LiveDecoder({
  onDatagram(dgram) {
    // TODO: do something with the datagram here
  },
});

decoder.decode(bytes);

See the LiveDecoder class API for details.

Performing two-way communication

The LiveTransceiver class combines both a LiveEncoder and a LiveDecoder to make two-way communication easier.

The encoder part of the LiveTransceiver starts suspended making it a passively listening VBus slave by default.

See LiveTransceiver class API for details.

Handling I/O

The LiveTransceiver class itself does not handle the raw I/O over something like serial port or network socket. It is up to the consumer to make sure data is flowing in and out of the LiveTransceiver.

Incoming data must be passed to the decode() instance function. It will try to decode VBus data primitives from it and call the respective callbacks if successful. See the LiveDecoder documentation for details.

Outgoind data is provided by the onTransmit callback passed in as a constructor option. Whenever the integrated LiveEncoder has assembled a block of data to transmit, the onTransmit callback is called. The consumer must take the appropriate steps to actually send the data using the underlying I/O subsystem.

The library resol-vbus-core-nodejs provides a specialized NetLiveTransceiver class that opens and maintains a net.Socket TCP connection and takes care of the necessary I/O handling specified above.

typescript
import { LiveTransceiver } from 'resol-vbus-core';

const tx = new LiveTransceiver({
  onTransmit(buffer) {
    // TODO: send this buffer using the I/O of your choice (serial port, network socket, ...)
  },
});

// TODO: whenever the underlying I/O has received some data, pass it on to `decode()`
const incomingData = new Uint8Array(/* ... */);
tx.decode(incomingData);

See the onTransmit constructor option API and the decode() instance function API for details.

Responding to a request

Since the LiveEncoder used inside the LiveTransceiver is automatically suspended on start, consumers must explicitly call resume() for using queue() to queue new data for transmission.

Assume your VBus slave simulates an extension module and waits for a Packet request from the main controller providing you with fresh output values and requesting you in turn to send your measurement values:

typescript
import { LiveTransceiver, Packet } from 'resol-vbus-core';

const tx = new LiveTransceiver({
  onPacket(packet) {
    if (packet.destinationAddress === 0x6651 && packet.command === 0x0300) {
      // TODO: evaluate the payload in `packet.frameData` containing the output values to apply

      // TODO: construct answer
      const frameData = new Uint8Array(6 * 4);
      // ...

      const answer = new Packet({
        destinationAddress: packet.sourceAddress,
        sourceAddress: packet.destinationAddress,
        command: 0x0100,
        frameCount: 6,
        frameData,
      });

      // `resume()` to be able to use `queue()`
      tx.getEncoder().resume();

      // `queue()` your answer for transmission
      tx.getEncoder().queue(answer);

      // `suspend()` will automatically suspend after the transmission is done
      tx.getEncoder().suspend();
    }
  },

  onTransmit(buffer) {
    // TODO: see above for notes about handling outgoing I/O
  },
});

// TODO: see above for notes about handling incoming I/O

Assume the VBus master role

The VBus master drives the communication forward. It is in control of who sends what data when.

Using the LiveTransceiver this can be achieved by calling resume() right after initialization and providing an onIdle() callback that gets called the the VBus master is allowed to send the next information.

typescript
import { LiveTransceiver, Packet } from 'resol-vbus-core';

let phase = 0;

const tx = new LiveTransceiver({
  onIdle() {
    let newPhase = phase + 1;
    if (phase === 0) {
      const packet = new Packet({
        destinationAddress: 0x0010,
        sourceAddress: myAddress,
        command: 0x0100,
        // TODO: set `frameCount` and `frameData` with your actual data to transmit
      });

      // queue the packet for transmission
      tx.getEncoder().queue(packet);

      // automatically suspend for 100 milliseconds after the transmission completed, will generate next `onIdle()` afterwards
      tx.getEncoder().suspendWithTimeout(100 /* milliseconds */);
    } else if (phase === 1) {
      const packet = new Packet({
        destinationAddress: 0x6651,
        sourceAddress: myAddress,
        command: 0x0300,
        // TODO: set `frameCount` and `frameData` with your actual data to transmit
      });

      tx.getEncoder().queue(packet);
      tx.getEncoder().suspendWithTimeout(100);
    } else if (phase === 2) {
      // ...
    } else {
      newPhase = 0;
    }

    phase = newPhase;
  },

  onTransmit() {
    // TODO: see above for notes about handling outgoing I/O
  }
});

// resume directly to start emitting `onIdle()` callbacks whenever it is "allowed" to send
tx.getEncoder().resume();

// TODO: see above for notes about handling incoming I/O