Kotlin使用LCM(Lightweight Communications and Marshalling)协议通信
Kotlin使用LCM(Lightweight Communications and Marshalling)协议通信
目标
使用Kotlin作为开发语言,通过LCM进行实时的数据交换。展示Kotlin的开发能力,以及LCM的使用。为实现基于Kotlin的LCM协议的开发提供基础。
前提
- 安装了LCM,包括库文件的lcm.jar,以及lcm-gen工具;
- 安装Gradle和JDK;
- 有手。
步骤
创建一个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的类型那篇翻译文章里面还仔细写了如何计算这个指纹。
修改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新手……
结论
- Kotlin使用Java语言的库,非常顺滑。
- LCM的整个Java实现很直观。
- 时间戳要专门写一个。