Kotlin使用LCM(Lightweight Communications and Marshalling)协议通信

Kotlin使用LCM(Lightweight Communications and Marshalling)协议通信

目标

使用Kotlin作为开发语言,通过LCM进行实时的数据交换。展示Kotlin的开发能力,以及LCM的使用。为实现基于Kotlin的LCM协议的开发提供基础。

前提

  1. 安装了LCM,包括库文件的lcm.jar,以及lcm-gen工具;
  2. 安装Gradle和JDK;
  3. 有手。

步骤

创建一个Kotlin项目

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

gradle里面选择application、Kotlin就可以。

创建一个LCM类型

根据LCM类型定义创建一个LCM类型定义文件lcm_tutorial_t.lcm,内容如下:

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;
}

app/src/main/java目录下运行:

lcm-gen -j lcm_tutorial_t.lcm

这就在当前目录下生成了一个exlcm/example_t.java文件。

/* 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.LCMEncodable
{
    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;
    }
 
}

机器生成的代码,不要修改。这里面也有一点点稍微好玩一点的东西,就是LCM给所有的结构定义了一个LCM_FINGERPRINT的静态变量,这个变量是用来做数据校验的,如果数据不对,就会抛出异常。这在信息传递的时候有实际的作用。在LCM的类型那篇翻译文章里面还仔细写了如何计算这个指纹。

example_t的方法

修改build.gradle.kts文件

这里的修改就一个目的,增加到lcm.jar的的引用。在Linux下面直接引用系统安装的LCM,在windows下面嫌麻烦就直接拷贝一个lcm.jar到项目目录下面。

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

实现文件

/*
 * 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
}

/*
    LCMDataInputStream Fingerprint
        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()


// 组播是LCM的核心内容,也是UDP的核心内容,值得专门写一个
const val multicast_host_url = "udpm://239.1.0.255:7667?ttl=1"

// 时区
val zoneId: ZoneId = ZoneId.of("Asia/Chongqing").normalized()

// 把时刻变成纳秒表示,按照UTC惯用零点为基准
fun epochUTCNano(): Long {
    val now = Instant.now()
    return now.epochSecond * 1000000000 + now.nano
}

// 把纳秒表示的时刻转换成字符串表示
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的subscribe代码比其他的程序更加简洁,因为Kotlin的lambda表达式的语法更加简洁。
在实际的应用中,这里直接接一个队列,拿到数据就放进去。

        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函数,可以订阅所有的消息,这个函数的参数是一个lambda表达式,这个表达式的参数是LCM、channel、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("")
        }

所以实际上LCM的监控什么的都是用Java做的,也不是没有原因。

输出

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

这个输出有几个小花样,因为我Java熟手……Zig新手……

结论

  1. Kotlin使用Java语言的库,非常顺滑。
  2. LCM的整个Java实现很直观。
  3. 时间戳要专门写一个。
posted @ 2023-04-19 19:47  大福是小强  阅读(27)  评论(0编辑  收藏  举报  来源