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
- Installed LCM, including the lcm.jar of the library file, and the lcm-gen tool;
- Install Gradle and JDK;
- 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.
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
- Kotlin uses the Java language library, which is very smooth.
- The entire Java implementation of LCM is intuitive.
- A timestamp should be written specifically.