Kotlin communicates using the LCM (Lightweight Communications and Marshalling) protocol

Kotlin uses LCM (Lightweight Communications and Marshalling) protocol communication

Goal

Use Kotlin as the development language, and perform real-time data exchange through LCM. Demonstrate the development capabilities of Kotlin and the use of LCM. Provide a basis for the development of the Kotlin-based LCM protocol.

Prerequisites

  1. Installed LCM, including the lcm.jar of the library file, and the lcm-gen tool;
  2. Install Gradle and JDK;
  3. have hands.

Steps

Create a Kotlin project

mkdir Kotlin-lcm-tutor
cd Kotlin-lcm-tutor
gradle init

In gradle, select application and Kotlin.

Create an LCM type

Create an LCM type definition file lcm_tutorial_t.lcm according to the LCM type definition, the content is as follows:

package exlcm;

struct example_t
{
    int64_t timestamp;
    double position[3];
    double orientation[4];
    int32_t num_ranges;
    int16_t ranges[num_ranges];
    string name;
    boolean enabled;
}

Run in the app/src/main/java directory:

lcm-gen -j lcm_tutorial_t.lcm

This generates a exlcm/example_t.java file in the current directory.

/* LCM type definition class file
 * This file was automatically generated by lcm-gen
 * DO NOT MODIFY BY HAND!!!!
 */

package exlcm;
 
import java.io.*;
import java.util.*;
import lcm.lcm.*;
 
public final class example_t implements lcm.lcm.LCMEEncodable
{<!-- -->
    public long timestamp;
    public double position[];
    public double orientation[];
    public int num_ranges;
    public short ranges[];
    public String name;
    public boolean enabled;
 
    public example_t()
    {<!-- -->
        position = new double[3];
        orientation = new double[4];
    }
 
    public static final long LCM_FINGERPRINT;
    public static final long LCM_FINGERPRINT_BASE = 0x1baa9e29b0fbaa8bL;
 
    static {<!-- -->
        LCM_FINGERPRINT = _hashRecursive(new ArrayList<Class<?>>());
    }
 
    public static long _hashRecursive(ArrayList<Class<?>> classes)
    {<!-- -->
        if (classes. contains(exlcm. example_t. class))
            return 0L;
 
        classes.add(exlcm.example_t.class);
        long hash = LCM_FINGERPRINT_BASE
            ;
        classes. remove(classes. size() - 1);
        return (hash<<1) + ((hash>>63) &1);
    }
 
    public void encode(DataOutput outs) throws IOException
    {<!-- -->
        outs.writeLong(LCM_FINGERPRINT);
        _encodeRecursive(outs);
    }
 
    public void _encodeRecursive(DataOutput outs) throws IOException
    {<!-- -->
        char[] __strbuf = null;
        outs.writeLong(this.timestamp);
 
        for (int a = 0; a < 3; a ++ ) {<!-- -->
            outs.writeDouble(this.position[a]);
        }
 
        for (int a = 0; a < 4; a ++ ) {<!-- -->
            outs.writeDouble(this.orientation[a]);
        }
 
        outs.writeInt(this.num_ranges);
 
        for (int a = 0; a <this.num_ranges; a ++ ) {<!-- -->
            outs.writeShort(this.ranges[a]);
        }
 
        __strbuf = new char[this.name.length()]; this.name.getChars(0, this.name.length(), __strbuf, 0); outs.writeInt(__strbuf.length + 1); for (int _i = 0; _i < __strbuf.length; _i ++ ) outs.write(__strbuf[_i]); outs.writeByte(0);
 
        outs.writeByte( this.enabled? 1 : 0);
 
    }
 
    public example_t(byte[] data) throws IOException
    {<!-- -->
        this(new LCMDataInputStream(data));
    }
 
    public example_t(DataInput ins) throws IOException
    {<!-- -->
        if (ins. readLong() != LCM_FINGERPRINT)
            throw new IOException("LCM Decode error: bad fingerprint");
 
        _decodeRecursive(ins);
    }
 
    public static exlcm.example_t _decodeRecursiveFactory(DataInput ins) throws IOException
    {<!-- -->
        exlcm.example_t o = new exlcm.example_t();
        o._decodeRecursive(ins);
        return o;
    }
 
    public void _decodeRecursive(DataInput ins) throws IOException
    {<!-- -->
        char[] __strbuf = null;
        this.timestamp = ins.readLong();
 
        this. position = new double[(int) 3];
        for (int a = 0; a < 3; a ++ ) {<!-- -->
            this.position[a] = ins.readDouble();
        }
 
        this. orientation = new double[(int) 4];
        for (int a = 0; a < 4; a ++ ) {<!-- -->
            this.orientation[a] = ins.readDouble();
        }
 
        this.num_ranges = ins.readInt();
 
        this. ranges = new short[(int) num_ranges];
        for (int a = 0; a <this.num_ranges; a ++ ) {<!-- -->
            this.ranges[a] = ins.readShort();
        }
 
        __strbuf = new char[ins.readInt()-1]; for (int _i = 0; _i < __strbuf.length; _i++) __strbuf[_i] = (char) (ins.readByte() &0xff) ; ins. readByte(); this. name = new String(__strbuf);
 
        this.enabled = ins.readByte()!=0;
 
    }
 
    public exlcm. example_t copy()
    {<!-- -->
        exlcm.example_t outobj = new exlcm.example_t();
        outobj.timestamp = this.timestamp;
 
        outobj. position = new double[(int) 3];
        System.arraycopy(this.position, 0, outobj.position, 0, 3);
        outobj. orientation = new double[(int) 4];
        System.arraycopy(this.orientation, 0, outobj.orientation, 0, 4);
        outobj.num_ranges = this.num_ranges;
 
        outobj. ranges = new short[(int) num_ranges];
        if (this. num_ranges > 0)
            System.arraycopy(this.ranges, 0, outobj.ranges, 0, (int) this.num_ranges);
        outobj.name = this.name;
 
        outobj.enabled = this.enabled;
 
        return outobj;
    }
 
}

Machine generated code, do not modify. There is also something a little more interesting here, that is, LCM defines a LCM_FINGERPRINT static variable for all structures. This variable is used for data verification. If the data is incorrect, it will throw exception. This has a practical effect when the information is passed. In the translation article of the type of LCM, how to calculate this fingerprint is also carefully written.

example_t method

Modify the build.gradle.kts file

The modification here is for one purpose, adding a reference to lcm.jar. Directly refer to the LCM installed in the system under Linux, and directly copy a lcm.jar to the project directory if it is too troublesome under Windows.

 // implementation(files("/usr/local/share/java/lcm.jar"))
    implementation(files("lib/lcm.jar"))

Implementation file

/*
 *LCM Kotlin Example
 */
package ktlcm

import exlcm.example_t
import lcm.lcm.LCM
import lcm.lcm.LCMDataInputStream
import java.io.IOException
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.util.TimeZone
import kotlin.random.Random
import kotlin.system.exitProcess


/*
    LCM Fingerprint
        For LCM Class: LCM_FINGERPRINT
        esle: -1L
 */
fun<T> java.lang.Class<T>.fingerprint(): Long{<!-- -->
    return fields.firstOrNull {<!-- --> it.name == "LCM_FINGERPRINT" }?.getLong(null) ?: -1L
}

/*
    LCMDataInputStreamFingerprint
        For LCM Class: first long in data
        esle: -1L
 */
fun LCMDataInputStream.fingerprint(): Long {<!-- -->
    val fp = if (available() >= 8) {<!-- -->readLong()} else {<!-- -->-1L}
    reset()
    return fp
}


val etf = example_t::class.java.fingerprint()


// Multicast is the core content of LCM, and also the core content of UDP. It is worth writing a special
const val multicast_host_url = "udpm://239.1.0.255:7667?ttl=1"

// Time zone
val zoneId: ZoneId = ZoneId.of("Asia/Chongqing").normalized()

// Turn the time into nanosecond representation, according to the UTC customary zero point as the reference
fun epochUTCNano(): Long {<!-- -->
    val now = Instant. now()
    return now.epochSecond * 1000000000 + now.nano
}

// convert the moment expressed in nanoseconds to a string representation
fun timeFromEpochNano(epochNano: Long): String {<!-- -->
    val epochSecond = epochNano / 1000000000
    val nano = epochNano % 1000000000
    return Instant.ofEpochSecond(epochSecond, nano).atZone(zoneId).toString()
}

fun main() {<!-- -->
    
    Thread(Runnable {<!-- -->

        val lcm = LCM(multicast_host_url)

        lcm.subscribe("EXAMPLE") {<!-- --> _, channel, data ->
            try {<!-- -->
                val msg = example_t(data)
                println("Received message on channel ${<!-- -->channel}")
                println("msg.timestamp = ${<!-- -->timeFromEpochNano(msg.timestamp)}")
                println("msg.position = ${<!-- -->msg.position.joinToString(", ", "[", "]")}")
                println("msg.orientation = ${<!-- -->msg.orientation.joinToString(", ", "[", "]")}")
                println("msg.num_ranges = ${<!-- -->msg.num_ranges}")
                println("msg.ranges = ${<!-- -->msg.ranges.joinToString(", ", "[", "]")}")
                println("msg.name = ${<!-- -->msg.name}")
                println("msg.enabled = ${<!-- -->msg.enabled}")
                println("")

                if (msg.name == "SHUTDOWN") {<!-- -->
                    exitProcess(0)
                }
            } catch (ex: IOException){<!-- -->
                println("Data not example_t")
            }
        }
        lcm.subscribeAll {<!-- -->l, c, d ->
            println("SubscribeAll:")
            println("Received message on channel ${<!-- -->c}")
            println("Data size : ${<!-- -->d.available()}")
            println("Data fingerprint : ${<!-- -->d.fingerprint()}")
            println(" example_t fingerprint : ${<!-- -->etf}")
            println("")
        }

        println("n_sub = ${<!-- -->lcm.numSubscriptions}")

        while (true) {<!-- -->
            Thread. sleep(100)
        }
    }).start()


    val lcm = LCM(multicast_host_url)

    val example = example_t()
    example.timestamp = epochUTCNano()
    example. position = doubleArrayOf(1.0, 2.0, 3.0)
    example. orientation = doubleArrayOf(1.0, 0.0, 0.0, 0.0)
    example.ranges = List(Random.nextInt(100, 200)) {<!-- --> Random.nextInt(0, 100).toShort() }.toShortArray()
    example.num_ranges = example.ranges.size
    example.name = "example string"
    example.enabled = true

    for (i in 1..10) {<!-- -->
        example.name = "example string ${<!-- -->i} @ ${<!-- -->System.getProperty("java.vendor")}"
        example.timestamp = epochUTCNano()
        example.ranges = List(Random.nextInt(100, 200)) {<!-- --> Random.nextInt(0, 100).toShort() }.toShortArray()
        example.num_ranges = example.ranges.size
        lcm. publish("EXAMPLE", example)
        Thread. sleep(1000)

    }

    example.name = "SHUTDOWN"
    lcm. publish("EXAMPLE", example)
}

Kotlin’s subscribe code is more concise than other programs, because Kotlin’s lambda expression syntax is more concise.
In actual applications, a queue is directly connected here, and the data is put into it.

 lcm. subscribe("EXAMPLE") {<!-- --> _, channel, data ->
            try {<!-- -->
                val msg = example_t(data)
                println("Received message on channel ${<!-- -->channel}")
                println("msg.timestamp = ${<!-- -->timeFromEpochNano(msg.timestamp)}")
                println("msg.position = ${<!-- -->msg.position.joinToString(", ", "[", "]")}")
                println("msg.orientation = ${<!-- -->msg.orientation.joinToString(", ", "[", "]")}")
                println("msg.num_ranges = ${<!-- -->msg.num_ranges}")
                println("msg.ranges = ${<!-- -->msg.ranges.joinToString(", ", "[", "]")}")
                println("msg.name = ${<!-- -->msg.name}")
                println("msg.enabled = ${<!-- -->msg.enabled}")
                println("")

                if (msg.name == "SHUTDOWN") {<!-- -->
                    exitProcess(0)
                }
            } catch (ex: IOException){<!-- -->
                println("Data not example_t")
            }
        }

The subscribeAll function of LCM can subscribe to all messages. The parameter of this function is a lambda expression, and the parameters of this expression are LCM, channel, and data.

 lcm. subscribeAll {<!-- -->l, c, d ->
            println("SubscribeAll:")
            println("Received message on channel ${<!-- -->c}")
            println("Data size : ${<!-- -->d.available()}")
            println("Data fingerprint : ${<!-- -->d.fingerprint()}")
            println(" example_t fingerprint : ${<!-- -->etf}")
            println("")
        }

So in fact, the monitoring of LCM is all done in Java, and it is not without reason.

Output

SubscribeAll:
   Received message on channel EXAMPLE
   Data size: 323
   Data fingerprint : 3987159373929993494
   example_t fingerprint : 3987159373929993494

Received message on channel EXAMPLE
   msg.timestamp = 2023-04-22T09:27:58.576805800+08:00[Asia/Chongqing]
   msg.position = [1.0, 2.0, 3.0]
   msg. orientation = [1.0, 0.0, 0.0, 0.0]
   msg.num_ranges = 112
   msg.ranges = [97, 57, 2, 47, 4, 4, 87, 63, 65, 34, 49, 51, 75, 49, 88, 19, 55, 11, 60, 97, 16, 5, 10 , 98, 86, 6, 8, 38, 55, 40, 54, 18, 52, 96, 19, 72, 71, 23, 47, 62, 34, 85, 69, 87, 14, 78, 81, 47 , 61, 69, 80, 88, 69, 89, 51, 19, 86, 19, 92, 85, 46, 55, 83, 66, 11, 24, 45, 10, 58, 94, 55, 79, 37 , 69, 82, 46, 71, 35, 68, 95, 9, 22, 71, 56, 70, 74, 36, 46, 94, 42, 27, 52, 7, 24, 60, 19, 37, 16 , 36, 59, 90, 46, 66, 75, 8, 46, 88, 88, 68, 29, 58, 1]
   msg.name = example string 8 @ Eclipse Adoptium
   msg.enabled = true

SubscribeAll:
   Received message on channel EXAMPLE
   Data size: 341
   Data fingerprint : 3987159373929993494
   example_t fingerprint : 3987159373929993494

Received message on channel EXAMPLE
   msg.timestamp = 2023-04-22T09:27:59.587089500+08:00[Asia/Chongqing]
   msg.position = [1.0, 2.0, 3.0]
   msg. orientation = [1.0, 0.0, 0.0, 0.0]
   msg.num_ranges = 115
   msg.ranges = [54, 41, 26, 72, 61, 97, 47, 99, 20, 29, 13, 37, 41, 31, 50, 59, 61, 57, 81, 84, 44, 96, 15 , 97, 60, 19, 75, 85, 59, 72, 8, 43, 36, 29, 49, 75, 19, 78, 23, 60, 21, 63, 90, 5, 2, 71, 40, 41 , 21, 58, 17, 25, 78, 24, 78, 84, 72, 55, 29, 3, 16, 62, 20, 54, 76, 31, 86, 58, 85, 11, 29, 37, 96 , 75, 28, 4, 73, 65, 97, 67, 49, 69, 55, 55, 61, 82, 44, 65, 10, 41, 54, 8, 94, 51, 92, 25, 75, 60 , 85, 75, 14, 48, 74, 11, 91, 47, 57, 41, 88, 84, 12, 79, 62, 65, 98]
   msg.name = example string 9 @ Eclipse Adoptium
   msg.enabled = true

There are a few tricks in this output, because I am familiar with Java… new to Zig…

Conclusion

  1. Kotlin uses the Java language library, which is very smooth.
  2. The entire Java implementation of LCM is intuitive.
  3. A timestamp should be written specifically.