Skip to content

Low Level Communication Details

io4edge devices communicate with the host application via TCP frames.

An io4edge device has:

  • A core component for device management
  • One or more functionblocks for hardware access, e.g. binary IO, analog IO, etc.

The core component and each functionblock use a separate TCP port on the io4edge device to communicate with the host application.

  • Core component: TCP port 9999
  • Functionblocks: TCP ports starting from 10000

Each TCP frame consists of a header and a payload. The header contains a magic word and the length. The payload contains the data as a marshalled protocol buffer (protobuf) message.

tcp-frame

Where

  • MAGIC_WORD: Constant 0xfeed. Byte 0: 0xFE, Byte 1: 0xED
  • LENGTH: length of payload in bytes (is encoded in little endian, i.e. least significant byte first)
  • PAYLOAD: Serialized Protocol Buffer message

Core Component Communication

The core component is responsible for device management tasks, such as reading the hardware ID, firmware version, setting persistent parameters, and performing firmware updates.

The core component uses the following Protocol Buffer definition for communication:

/*
Copyright © 2021 Ci4Rail GmbH <engineering@ci4rail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";

package io4edgeCoreApi;
option go_package = "core_api/v1alpha2";

enum CommandId {
  IDENTIFY_HARDWARE = 0;
  IDENTIFY_FIRMWARE = 1;
  LOAD_FIRMWARE_CHUNK = 2;
  PROGRAM_HARDWARE_IDENTIFICATION = 3;
  RESTART = 4;
  SET_PERSISTENT_PARAMETER = 5;
  GET_PERSISTENT_PARAMETER = 6;
  READ_PARTITION_CHUNK = 7;
  GET_RESET_REASON = 8;
}

enum Status {
  OK = 0;
  UNKNOWN_COMMAND = 1;
  ILLEGAL_PARAMETER = 2;
  BAD_CHUNK_SEQ = 3;
  BAD_CHUNK_SIZE = 4;
  NOT_COMPATIBLE = 5;
  INTERNAL_ERROR = 6;
  PROGRAMMING_ERROR = 7;
  NO_HW_INVENTORY = 8;
  THIS_VERSION_FAILED_ALREADY = 9;
}

// Individual Commands and Responses

// LoadFirmware
// Client sends sequence of CmdLoadFirmwareChunk commands, with increasing
// chunk numbers. Clients defines chunk size.
// Server must acknowledge each chunk with Response.
// Last chunk has is_last_chunk set to True, so server knows that programming
// has finished
message LoadFirmwareChunkCommand {
  uint32 chunk_number = 1;
  bool is_last_chunk = 2;
  bytes data = 3;
}

message ProgramHardwareIdentificationCommand {
  string signature = 1;
  string root_article = 2;
  uint32 major_version = 3;
  string serial_number = 4;
}

message SetPersistentParameterCommand {
  string name = 1;
  string value = 2;
}

message GetPersistentParameterCommand { string name = 1; }

// Request to read from partition at a specific offset
// server answers with a ReadPartitionResponse, server decides how many bytes
// it delivers. Client then advances its offset until it has got enough bytes or server
// returns zero length chunk
message ReadPartitionChunkCommand {
  string part_name = 1;
  uint32 offset = 2;
}

message IdentifyHardwareResponse {
  string root_article = 1;
  uint32 major_version = 2;
  string serial_number = 3;
}

message IdentifyFirmwareResponse {
  string name = 1;
  string version = 2;
}

message GetPersistentParameterResponse { string value = 1; }

message ReadPartitionChunkResponse {
  string part_name = 1;
  uint32 offset = 2;
  bytes data = 3;
}

message GetResetReasonResponse {
  string reason = 1;
}

// The common messages
message CoreCommand {
  CommandId id = 1;
  oneof data {
    LoadFirmwareChunkCommand load_firmware_chunk = 2;
    ProgramHardwareIdentificationCommand program_hardware_identification = 3;
    SetPersistentParameterCommand set_persistent_parameter = 4;
    GetPersistentParameterCommand get_persistent_parameter = 5;
    ReadPartitionChunkCommand read_partition_chunk = 6;
  }
}

message CoreResponse {
  CommandId id = 1;
  Status status = 2;
  bool restarting_now = 3;
  oneof data {
    IdentifyHardwareResponse identify_hardware = 4;
    IdentifyFirmwareResponse identify_firmware = 5;
    GetPersistentParameterResponse persistent_parameter = 6;
    ReadPartitionChunkResponse read_partition_chunk = 7;
    GetResetReasonResponse reset_reason = 8;
  }
}

Functionblock Communication

The functionblock encapsulates the hardware specific calls via some generic API calls. Each functionblock, no matter what hardware is accessed, looks the same from the outside. There are three APIs to communicate with a functionblock:

  • Configuration Control - configure the functionblock
  • Function Control - set or get data from the functionblock
  • Stream Control - start and stop data streaming

Each functionblock, e.g. for binaryIO or CAN, implements function specific actions for the generic API calls.

The following is the generic functionblock Protocol Buffer definition:

/*
Copyright © 2021 Ci4Rail GmbH <engineering@ci4rail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";

import "google/protobuf/any.proto";

package functionblock;
option go_package = "functionblock/v1alpha1";

// -------- Meta ------------
message Context {
  // A message identifying key for a command-response pairs, e.g. an UUID the
  // clients sends on the request.
  string value = 1;
}

// ------- Commands ---------
message Command {
  Context context = 1;
  oneof type {
    Configuration Configuration = 2;
    FunctionControl functionControl = 3;
    StreamControl streamControl = 4;
  }
}

// Configuration contains the function blocks high level configuration
message Configuration {
  oneof action {
    // Setting a new configuration
    google.protobuf.Any functionSpecificConfigurationSet = 10;
    // Getting the current configuration
    google.protobuf.Any functionSpecificConfigurationGet = 20;
    // Describe hardware capabilities
    google.protobuf.Any functionSpecificConfigurationDescribe = 30;
  }
}

// FunctionControl specifies the direct function control for getting and setting values
message FunctionControl {
  oneof action {
    google.protobuf.Any functionSpecificFunctionControlSet = 1;
    google.protobuf.Any functionSpecificFunctionControlGet = 2;
  }
}

// StreamControlStart specifies the start of a stream.
message StreamControlStart {
    // number of maximum samples transported in a single Stream Data Message
    // must be >= 1
    fixed32 bucketSamples = 1;

    // maximum interval in ms between two stream messages. If there are no or very few stream messages for a
    // certain time, the client is informed that the stream is still active and the existing data is transmitted.
    // must be >= 100 (ms)
    fixed32 keepaliveInterval = 2;

    // number of buffered samples for this stream
    // must be >= 1 and >= bucketSamples
    fixed32 bufferedSamples = 3;

    // function specific
    google.protobuf.Any functionSpecificStreamControlStart = 4;

    // low latency mode for stream: Sends samples as soon as possible, if currently no more buffered samples are ready.
    bool low_latency_mode = 5;
}

// StreamControlStart specifies the stop of a stream.
message StreamControlStop {
}

// StreamControl specifies if the stream shall be started or stopped
message StreamControl {
  oneof action {
    StreamControlStart start = 1;
    StreamControlStop stop = 2;
  }
}

// --------- Responses ------------
enum Status {
  OK = 0;
  UNSPECIFIC_ERROR = 1;
  UNKNOWN_COMMAND = 2;
  NOT_IMPLEMENTED = 3;
  WRONG_CLIENT = 4;
  INVALID_PARAMETER = 5;
  HW_FAULT = 6;
  STREAM_ALREADY_STARTED = 7;
  STREAM_ALREADY_STOPPED = 8;
  STREAM_START_FAILED = 9;
  TEMPORARILY_UNAVAILABLE=10;
}

message Error { string error = 1; }

message ConfigurationResponse {
  oneof action {
    // Setting a new configuration
    google.protobuf.Any functionSpecificConfigurationSet = 10;
    // Getting the current configuration
    google.protobuf.Any functionSpecificConfigurationGet = 20;
    // Describe hardware capabilities
    google.protobuf.Any functionSpecificConfigurationDescribe = 30;
  }
}

message FunctionControlResponse {
  oneof action {
    google.protobuf.Any functionSpecificFunctionControlSet = 1;
    google.protobuf.Any functionSpecificFunctionControlGet = 2;
  }
}

message StreamControlResponse {
}

message StreamData {
  // timestamp when the message has been sent out
  fixed64 deliveryTimestampUs = 1;

  // sample series sequence number (counted from 0, rolls over)
  fixed32 sequence = 2;

  // Function specific data type
  google.protobuf.Any functionSpecificStreamData = 10;
}

message Response {
  Context context = 1;
  Status status = 2;
  Error error = 3;
  oneof type {
    ConfigurationResponse Configuration = 4;
    FunctionControlResponse functionControl = 5;
    StreamControlResponse streamControl = 6;
    StreamData stream = 7;
  }
}

The functionblock specific messages are defined in the respective functionblock documentation. You must embed the serialized specific messages in the google.protobuf.Any fields of the generic functionblock messages.

Note that streaming data from the functionblock to the host application is done via the same TCP connection as the command-response communication. So you must have a kind of dispatcher in your host application to separate command responses from streaming data.