From 1cf6e37dc356144f3da2a2dcde75d1ced8bc5ad6 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 19 Mar 2015 16:08:46 +0100 Subject: initial commit --- .gitignore | 29 ++++ README.md | 3 + .../com/github/jodersky/mavlink/Context.scala | 6 + .../scala/com/github/jodersky/mavlink/Crc.scala | 35 +++++ .../com/github/jodersky/mavlink/Generator.scala | 45 ++++++ .../com/github/jodersky/mavlink/ParseError.scala | 3 + .../scala/com/github/jodersky/mavlink/Parser.scala | 110 +++++++++++++++ .../com/github/jodersky/mavlink/StringUtils.scala | 16 +++ .../github/jodersky/mavlink/trees/package.scala | 44 ++++++ .../src/main/twirl/org/mavlink/Assembler.scala.txt | 19 +++ .../src/main/twirl/org/mavlink/Crc.scala.txt | 28 ++++ .../main/twirl/org/mavlink/MavlinkBuffer.scala.txt | 24 ++++ .../src/main/twirl/org/mavlink/Packet.scala.txt | 59 ++++++++ .../src/main/twirl/org/mavlink/Parser.scala.txt | 157 +++++++++++++++++++++ .../src/main/twirl/org/mavlink/_header.scala.txt | 5 + .../twirl/org/mavlink/messages/messages.scala.txt | 120 ++++++++++++++++ mavlink-library/src/test/resources/concise.xml | 115 +++++++++++++++ .../com/github/jodersky/mavlink/MainTest.scala | 16 +++ .../github/jodersky/mavlink/sbt/MavlinkKeys.scala | 14 ++ .../github/jodersky/mavlink/sbt/SbtMavlink.scala | 51 +++++++ project/Build.scala | 42 ++++++ project/plugins.sbt | 1 + 22 files changed, 942 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 mavlink-library/src/main/scala/com/github/jodersky/mavlink/Context.scala create mode 100644 mavlink-library/src/main/scala/com/github/jodersky/mavlink/Crc.scala create mode 100644 mavlink-library/src/main/scala/com/github/jodersky/mavlink/Generator.scala create mode 100644 mavlink-library/src/main/scala/com/github/jodersky/mavlink/ParseError.scala create mode 100644 mavlink-library/src/main/scala/com/github/jodersky/mavlink/Parser.scala create mode 100644 mavlink-library/src/main/scala/com/github/jodersky/mavlink/StringUtils.scala create mode 100644 mavlink-library/src/main/scala/com/github/jodersky/mavlink/trees/package.scala create mode 100644 mavlink-library/src/main/twirl/org/mavlink/Assembler.scala.txt create mode 100644 mavlink-library/src/main/twirl/org/mavlink/Crc.scala.txt create mode 100644 mavlink-library/src/main/twirl/org/mavlink/MavlinkBuffer.scala.txt create mode 100644 mavlink-library/src/main/twirl/org/mavlink/Packet.scala.txt create mode 100644 mavlink-library/src/main/twirl/org/mavlink/Parser.scala.txt create mode 100644 mavlink-library/src/main/twirl/org/mavlink/_header.scala.txt create mode 100644 mavlink-library/src/main/twirl/org/mavlink/messages/messages.scala.txt create mode 100644 mavlink-library/src/test/resources/concise.xml create mode 100644 mavlink-library/src/test/scala/com/github/jodersky/mavlink/MainTest.scala create mode 100644 mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/MavlinkKeys.scala create mode 100644 mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/SbtMavlink.scala create mode 100644 project/Build.scala create mode 100644 project/plugins.sbt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e38fe35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# sbt +.cache +.history/ +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ + +# scala-ide specific +/.settings +/.scala_dependencies +/.project +/.classpath +/.cache +/.history + +# ensime +.ensime + +# general files +/*.jar +*.swp +*.class +*.log +*~ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb894af --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# SBT-MAVLink Plugin + +This plugin aims to provide generation of Scala sources from MAVLink message definition files. \ No newline at end of file diff --git a/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Context.scala b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Context.scala new file mode 100644 index 0000000..d025285 --- /dev/null +++ b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Context.scala @@ -0,0 +1,6 @@ +package com.github.jodersky.mavlink + +/** Represents the context under which MAVLink scala code was generated. */ +case class Context( + version: String +) \ No newline at end of file diff --git a/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Crc.scala b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Crc.scala new file mode 100644 index 0000000..6150e48 --- /dev/null +++ b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Crc.scala @@ -0,0 +1,35 @@ +package com.github.jodersky.mavlink + +/** + * X.25 CRC calculation for MAVlink messages. The checksum must be initialized, + * updated with each field of a message, and then finished with the message + * id. + */ +case class Crc(val crc: Int = 0xffff) extends AnyVal { + + /** + * Accumulates data into a new checksum. + */ + def accumulate(datum: Byte): Crc = { + val d = datum & 0xff + var tmp = d ^ (crc & 0xff) + tmp ^= (tmp << 4) & 0xff; + Crc( + ((crc >> 8) & 0xff) ^ (tmp << 8) ^ (tmp << 3) ^ ((tmp >> 4) & 0xff)) + } + + def accumulate(data: Seq[Byte]): Crc = { + var next = this + for (d <- data) { + next = next.accumulate(d) + } + next + } + + /** Least significant byte of checksum. */ + def lsb: Byte = crc.toByte + + /** Most significant byte of checksum. */ + def msb: Byte = (crc >> 8).toByte + +} \ No newline at end of file diff --git a/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Generator.scala b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Generator.scala new file mode 100644 index 0000000..06a4909 --- /dev/null +++ b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Generator.scala @@ -0,0 +1,45 @@ +package com.github.jodersky.mavlink + +import scala.xml.XML +import java.io.FileWriter +import java.io.BufferedWriter +import scalax.file.Path +import java.io.File + +import trees._ + +/** + * Generates Scala code implementing the MAVLink protocol. + * @param dialect a specific MAVLink dialect for which to generate code + */ +class Generator(dialect: Dialect) { + + lazy val maxPayloadLength = { + val widths = dialect.messages map { msg => + msg.fields.map(_.tpe.sizeof).sum + } + widths.max + } + + lazy val extraCrcs = Array.tabulate[Byte](255){i => + val message = dialect.messages.find(_.id == i) + message.map(_.checksum).getOrElse(0) + } + + /** + * Generates Scala code implementing MAVLink. + * @return a list containing proposed Scala file names pointing to their contents + */ + def generate(): List[(String, String)] = { + val context = Context(dialect.version) + + List( + "org/mavlink/Assembler.scala" -> org.mavlink.txt.Assembler(context).body, + "org/mavlink/Crc.scala" -> org.mavlink.txt.Crc(context).body, + "org/mavlink/MavlinkBuffer.scala" -> org.mavlink.txt.MavlinkBuffer(context).body, + "org/mavlink/Packet.scala" -> org.mavlink.txt.Packet(context, maxPayloadLength, extraCrcs).body, + "org/mavlink/Parser.scala" -> org.mavlink.txt.Parser(context).body, + "org/mavlink/messages/messages.scala" -> org.mavlink.messages.txt.messages(context, dialect.messages).body + ) + } +} \ No newline at end of file diff --git a/mavlink-library/src/main/scala/com/github/jodersky/mavlink/ParseError.scala b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/ParseError.scala new file mode 100644 index 0000000..e224f39 --- /dev/null +++ b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/ParseError.scala @@ -0,0 +1,3 @@ +package com.github.jodersky.mavlink + +class ParseError(message: String) extends RuntimeException(message) \ No newline at end of file diff --git a/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Parser.scala b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Parser.scala new file mode 100644 index 0000000..e2dda42 --- /dev/null +++ b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/Parser.scala @@ -0,0 +1,110 @@ +package com.github.jodersky.mavlink + +import scala.language.postfixOps + +import scala.xml.Node +import scala.util.Try + +import trees._ + +/** + * Provides means to parse a MAVLink dialect definition into a + * scala object representation. + */ +object Parser { + + def fatal(error: String, node: Node) = throw new ParseError("Parse error at " + node + ": " + error) + def warn(warning: String, node: Node) = Console.err.println("Warning " + node + ": " + warning) + + def parseDialect(node: Node): Dialect = parse(node) match { + case p: Dialect => p + case _ => fatal("expected mavlink protocol definition", node) + } + + def parse(node: Node): Tree = node match { + case {_*} => + val description = node.text + val name = (node \ "@name").map(_.text).headOption getOrElse fatal("no name defined for field", node) + val enum = (node \ "@enum").map(_.text).headOption + val (tpe, native) = (node \ "@type") map (_.text) headOption match { + case Some(t) => (parseType(t, node), t) + case None => fatal("no field type specified", node) + } + Field(tpe, native, name, enum, description) + + case {_*} => + val value = (node \ "@value").map(_.text).headOption map { str => + Try { Integer.parseInt(str) } getOrElse fatal("value must be an integer", node) + } getOrElse fatal("no value defined", node) + val name = (node \ "@name").map(_.text).headOption getOrElse fatal("no name defined for enum entry", node) + val description = (node \ "description").text + EnumEntry(value, name, description) + + case {_*} => + val name = (node \ "@name").map(_.text).headOption getOrElse fatal("no name defined for enum", node) + val entries = (node \ "entry").toSet map { n: Node => + parse(n) match { + case e: EnumEntry => e + case _ => fatal("illegal definition in enum, only entries are allowed", n) + } + } + Enum(name, entries) + + case {_*} => + val id = (node \ "@id").map(_.text).headOption map { str => + val id = Try { Integer.parseInt(str) } getOrElse fatal("id must be an integer", node) + if (id < 0 || id > 255) warn("message id is not in the range [0-255]", node) + id.toByte + } getOrElse fatal("no id defined", node) + val name = (node \ "@name").map(_.text).headOption getOrElse fatal("no name defined for message", node) + val description = (node \ "description").text + + val fields = (node \ "field") map { n: Node => + parse(n) match { + case e: Field => e + case _ => fatal("illegal definition in message, only fields are allowed", n) + } + } + Message(id, name, description, fields) + + case {_*} => + val version = (node \ "version").text + + val enums = (node \ "enums" \ "_").toSet map { n: Node => + parse(n) match { + case e: Enum => e + case _ => fatal("illegal definition in enums, only enum declarations are allowed", n) + } + } + + val messages = (node \ "messages" \ "_").toSet map { n: Node => + parse(n) match { + case e: Message => e + case e => fatal("illegal definition in messages, only message declarations are allowed", n) + } + } + Dialect(version, enums, messages) + + case x => fatal("unknown", x) + + } + + val ArrayPattern = """(.*)\[(\d+)\]""".r + def parseType(typeStr: String, node: Node): Type = typeStr match { + case "int8_t" => IntType(1, true) + case "int16_t" => IntType(2, true) + case "int32_t" => IntType(4, true) + case "int64_t" => IntType(8, true) + case "uint8_t" => IntType(1, false) + case "uint16_t" => IntType(2, false) + case "uint32_t" => IntType(4, false) + case "uint64_t" => IntType(8, false) + case "float" => FloatType(4) + case "double" => FloatType(8) + case "char" => IntType(1, true) + case ArrayPattern("char", l) => StringType(l.toInt) + case ArrayPattern(u, l) => ArrayType(parseType(u, node), l.toInt) + case unknown => fatal("unknown field type " + unknown, node) + } + +} \ No newline at end of file diff --git a/mavlink-library/src/main/scala/com/github/jodersky/mavlink/StringUtils.scala b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/StringUtils.scala new file mode 100644 index 0000000..38b87e7 --- /dev/null +++ b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/StringUtils.scala @@ -0,0 +1,16 @@ +package com.github.jodersky.mavlink + +object StringUtils { + + def camelify(str: String) = { + val lower = str.toLowerCase + "_([a-z\\d])".r.replaceAllIn(lower, {m => m.group(1).toUpperCase()}) + } + + def Camelify(str: String) = { + val camel = camelify(str) + val (head, tail) = camel.splitAt(1) + head.toUpperCase + tail + } + +} \ No newline at end of file diff --git a/mavlink-library/src/main/scala/com/github/jodersky/mavlink/trees/package.scala b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/trees/package.scala new file mode 100644 index 0000000..49b697b --- /dev/null +++ b/mavlink-library/src/main/scala/com/github/jodersky/mavlink/trees/package.scala @@ -0,0 +1,44 @@ +package com.github.jodersky.mavlink + +package trees { + + sealed trait Tree + + case class Dialect(version: String, enums: Set[Enum], messages: Set[Message]) extends Tree + case class Enum(name: String, entries: Set[EnumEntry]) extends Tree + case class EnumEntry(value: Int, name: String, description: String) extends Tree + case class Field(tpe: Type, nativeType: String, name: String, enum: Option[String], description: String) extends Tree + case class Message(id: Byte, name: String, description: String, fields: Seq[Field]) extends Tree { + def orderedFields = fields.toSeq.sortBy(_.tpe.width)(Ordering[Int].reverse) + + lazy val checksum = { + var c = new Crc() + c = c.accumulate((name + " ").getBytes) + for (field <- orderedFields) { + c = c.accumulate((field.nativeType + " ").getBytes) + c = c.accumulate((field.name + " ").getBytes) + } + (c.lsb ^ c.msb).toByte + } + } + + trait Type extends Tree { + def width: Int // width in bytes of the type + def sizeof: Int = width // size of bytes of the type + } + + case class IntType(width: Int, signed: Boolean) extends Type + case class FloatType(width: Int) extends Type + case class ArrayType(underlying: Type, length: Int) extends Type { + def width = underlying.width + override def sizeof = width * length + } + case object CharType extends Type { + def width = 1 + } + case class StringType(maxLength: Int) extends Type { + val width = 1 + override def sizeof = width * maxLength + } + +} \ No newline at end of file diff --git a/mavlink-library/src/main/twirl/org/mavlink/Assembler.scala.txt b/mavlink-library/src/main/twirl/org/mavlink/Assembler.scala.txt new file mode 100644 index 0000000..e3571e6 --- /dev/null +++ b/mavlink-library/src/main/twirl/org/mavlink/Assembler.scala.txt @@ -0,0 +1,19 @@ +@(__context: Context)@_header(__context) +package org.mavlink + +import java.nio.ByteBuffer + +/** + * Utility class for assembling packets with increasing sequence number + * originating from given system and component IDs. + */ +class Assembler(systemId: Byte, componentId: Byte) { + private var seq = 0; + + /** Assemble a given message ID and payload into a packet. */ + def assemble(messageId: Byte, payload: ByteBuffer): Packet = { + val p = Packet(seq.toByte, systemId, componentId, messageId, payload) + seq += 1 + p + } +} \ No newline at end of file diff --git a/mavlink-library/src/main/twirl/org/mavlink/Crc.scala.txt b/mavlink-library/src/main/twirl/org/mavlink/Crc.scala.txt new file mode 100644 index 0000000..f6e3cce --- /dev/null +++ b/mavlink-library/src/main/twirl/org/mavlink/Crc.scala.txt @@ -0,0 +1,28 @@ +@(__context: Context)@_header(__context) +package org.mavlink + +/** + * X.25 CRC calculation for MAVlink messages. The checksum must be initialized, + * updated with each field of a message, and then finished with the message + * id. + */ +case class Crc(val crc: Int = 0xffff) extends AnyVal { + + /** + * Accumulates data into a new checksum. + */ + def accumulate(datum: Byte): Crc = { + val d = datum & 0xff + var tmp = d ^ (crc & 0xff) + tmp ^= (tmp << 4) & 0xff; + Crc( + ((crc >> 8) & 0xff) ^ (tmp << 8) ^ (tmp << 3) ^ ((tmp >> 4) & 0xff)) + } + + /** Least significant byte of checksum. */ + def lsb: Byte = crc.toByte + + /** Most significant byte of checksum. */ + def msb: Byte = (crc >> 8).toByte + +} \ No newline at end of file diff --git a/mavlink-library/src/main/twirl/org/mavlink/MavlinkBuffer.scala.txt b/mavlink-library/src/main/twirl/org/mavlink/MavlinkBuffer.scala.txt new file mode 100644 index 0000000..9a01dbe --- /dev/null +++ b/mavlink-library/src/main/twirl/org/mavlink/MavlinkBuffer.scala.txt @@ -0,0 +1,24 @@ +@(__context: Context)@_header(__context) +package org.mavlink + +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** Utility functions for using ByteBuffers. */ +class MavlinkBuffer { + + /** + * Allocates a ByteBuffer for using in MAVLink message processing. + * @@param direct specifies if a direct buffer should be used + */ + def allocate(direct: Boolean = true) = { + val buffer = if (direct) { + ByteBuffer.allocateDirect(Packet.MaxPayloadLength) + } else { + val underlying = new Array[Byte](Packet.MaxPayloadLength) + ByteBuffer.wrap(underlying) + } + buffer.order(ByteOrder.LITTLE_ENDIAN) // MAVLink uses little endian + } + +} \ No newline at end of file diff --git a/mavlink-library/src/main/twirl/org/mavlink/Packet.scala.txt b/mavlink-library/src/main/twirl/org/mavlink/Packet.scala.txt new file mode 100644 index 0000000..a0d11a5 --- /dev/null +++ b/mavlink-library/src/main/twirl/org/mavlink/Packet.scala.txt @@ -0,0 +1,59 @@ +@(__context: Context, __maxPayloadLength: Int, __extraCrcs: Seq[Byte])@_header(__context) +package org.mavlink + +import java.nio.ByteBuffer + +/** + * Represents a MAVLink packet. + * Note that due to performance reasons, the packet's payload contents are mutable. + * @@param seq sequence number + * @@param systemId system ID of sender + * @@param componentId component ID of sender + * @@param messageId MAVLink message identification (this depends on the dialect used) + * @@param payload message contents + */ +case class Packet( + seq: Byte, + systemId: Byte, + componentId: Byte, + messageId: Byte, + payload: ByteBuffer +) { + + /* + def crc = { + var c = new Crc() + c = c.accumulate(payload.length.toByte) + c = c.accumulate(seq) + c = c.accumulate(systemId) + c = c.accumulate(componentId) + c = c.accumulate(messageId) + while (payload.) + for (p <- payload) { + c = c.accumulate(p) + } + c = c.accumulate(Packet.extraCrc(messageId)) + c + }*/ +} + +object Packet { + + /** Start byte signaling the beginning of a packet. */ + final val Stx: Byte = (0xfe).toByte + + /** Maximum length of a payload contained in a packet. */ + final val MaxPayloadLength: Int = @__maxPayloadLength + + /** Additional CRCs indexed by message ID (see MAVLink specification). */ + final val ExtraCrcs: Seq[Byte] = Array[Byte]( + @__extraCrcs.map(_ formatted "%3d").grouped(10).map(_.mkString(",")).mkString(",\n ") + ) + + /** Utility function to index ExtraCrcs with a byte. */ + def extraCrc(id: Byte) = ExtraCrcs(id & 0xff) + + /** An invalid packet with no payload. */ + def emoty = Packet(0, 0, 0, -128, ByteBuffer.wrap(Array(0: Byte))) + +} \ No newline at end of file diff --git a/mavlink-library/src/main/twirl/org/mavlink/Parser.scala.txt b/mavlink-library/src/main/twirl/org/mavlink/Parser.scala.txt new file mode 100644 index 0000000..ca04ae1 --- /dev/null +++ b/mavlink-library/src/main/twirl/org/mavlink/Parser.scala.txt @@ -0,0 +1,157 @@ +@(__context: Context)@_header(__context) +package org.mavlink + +import java.nio.ByteBuffer + +object Parser { + + /** Internal parser states. */ + object States { + sealed trait State + case object Idle extends State + case object GotStx extends State + case object GotLength extends State + case object GotSeq extends State + case object GotSysId extends State + case object GotCompId extends State + case object GotMsgId extends State + case object GotCrc1 extends State + case object GotPayload extends State + } + + /** Errors that may occur while receiving data. */ + object Errors { + sealed trait Error + case object CrcError extends Error + case object OverflowError extends Error + } +} + +/** + * A parser to divide byte streams into mavlink packets. + * A parser is created with receiver and error functions and bytes are then fed into it. Once + * a valid packet has been received (or an error encountered), the receiver (or error) functions + * are called. + * Note that due to memory and performance issues, a received packet and payload is + * only guaranteed to be valid within the receiver function. After exiting the function, the + * underlying packet's data may be overwritten by a new packet. + * + * @@param buffer a buffer into which the received payload is stored + * @@param receiver called when a valid packet has been received + * @@param error called when invalid data was received + */ +class Parser(payload: ByteBuffer, receiver: Packet => Unit, error: Parser.Errors.Error => Unit = _ => ()) { + import Parser._ + + private var state: States.State = States.Idle + + private object inbound { + var length: Int = 0 + var seq: Byte = 0 + var systemId: Byte = 0 + var componentId: Byte = 0 + var messageId: Byte = 0 + var crc: Crc = new Crc() + } + + /** + * Parses a byte as part of an incoming MAVLink message. May result + * in calling receiver or error function. + */ + def push(c: Byte): Unit = { + import States._ + + state match { + case Idle => + if (c == Packet.Stx) { + state = GotStx + } + + case GotStx => + inbound.crc = new Crc() + inbound.length = (c & 0xff) + inbound.crc = inbound.crc.accumulate(c) + state = GotLength + + case GotLength => + inbound.seq = c; + inbound.crc = inbound.crc.accumulate(c) + state = GotSeq + + case GotSeq => + inbound.systemId = c + inbound.crc = inbound.crc.accumulate(c) + state = GotSysId + + case GotSysId => + inbound.componentId = c + inbound.crc = inbound.crc.accumulate(c) + state = GotCompId + + case GotCompId => + inbound.messageId = c + inbound.crc = inbound.crc.accumulate(c) + if (inbound.length == 0) { + state = GotPayload + } else { + state = GotMsgId + payload.clear() + } + + case GotMsgId => + if (!payload.hasRemaining) { + state = Idle + error(Errors.OverflowError) + } else { + payload.put(c) + inbound.crc = inbound.crc.accumulate(c) + if (payload.position >= inbound.length) { + state = GotPayload + } + } + + case GotPayload => + inbound.crc = inbound.crc.accumulate(Packet.extraCrc(inbound.messageId)) + if (c != inbound.crc.lsb) { + state = Idle + if (c == Packet.Stx) { + state = GotStx + } + error(Errors.CrcError) + } else { + state = GotCrc1 + } + + case GotCrc1 => + if (c != inbound.crc.msb) { + state = Idle + if (c == Packet.Stx) { + state = GotStx + } + error(Errors.CrcError) + } else { + val packet = Packet( + inbound.seq, + inbound.systemId, + inbound.componentId, + inbound.messageId, + payload) + state = Idle + receiver(packet) + } + } + } + + /** + * Parses a sequence of bytes. + */ + def push(bytes: Traversable[Byte]): Unit = for (b <- bytes) push(b) + + /** + * Parses all bytes of contained in a byte buffer. + */ + def push(bytes: ByteBuffer): Unit = while(bytes.hasRemaining) { + push(bytes.get()) + } + +} \ No newline at end of file diff --git a/mavlink-library/src/main/twirl/org/mavlink/_header.scala.txt b/mavlink-library/src/main/twirl/org/mavlink/_header.scala.txt new file mode 100644 index 0000000..3fd5da9 --- /dev/null +++ b/mavlink-library/src/main/twirl/org/mavlink/_header.scala.txt @@ -0,0 +1,5 @@ +@(__context: Context)/* + * MAVLink Abstraction Layer for Scala. + * + * This file was automatically generated. + */ \ No newline at end of file diff --git a/mavlink-library/src/main/twirl/org/mavlink/messages/messages.scala.txt b/mavlink-library/src/main/twirl/org/mavlink/messages/messages.scala.txt new file mode 100644 index 0000000..6f444dc --- /dev/null +++ b/mavlink-library/src/main/twirl/org/mavlink/messages/messages.scala.txt @@ -0,0 +1,120 @@ +@(__context: Context, __messages: Set[Message])@org.mavlink.txt._header(__context) +package org.mavlink.messages + +import java.nio.ByteBuffer +import java.nio.charset.Charset + +@__scalaMessageType(message: Message) = {@StringUtils.Camelify(message.name)} + +@__scalaFieldName(field: Field) = {@StringUtils.camelify(field.name)} + +@__scalaFieldType(tpe: Type) = {@tpe match { + case IntType(1, _) => {Byte} + case IntType(2, _) => {Short} + case IntType(4, _) => {Int} + case IntType(8, _) => {Long} + case FloatType(4) => {Float} + case FloatType(8) => {Double} + case StringType(_) => {String} + case ArrayType(underlying, _) => {Seq[@__scalaFieldType(underlying)]} +}} +@__scalaFieldFormal(field: Field) = {@__scalaFieldName(field): @__scalaFieldType(field.tpe)} + +@__bufferReadMethod(buffer: String, tpe: Type) = {@tpe match { + case IntType(1, _) => {@{buffer}.get()} + case IntType(2, _) => {@{buffer}.getShort()} + case IntType(4, _) => {@{buffer}.getInt()} + case IntType(8, _) => {@{buffer}.getLong()} + case FloatType(4) => {@{buffer}.getFloat()} + case FloatType(8) => {@{buffer}.getDouble()} + case StringType(maxLength) =>{{ + val bytes = Array[Byte](@maxLength) + @{buffer}.get(bytes, 0, @maxLength) + val length = bytes.indexOf(0) match { + case -1 => @maxLength + case i => i + } + new String(bytes, 0, length, Charset.forName("UTF-8")) + }} + case ArrayType(underlying, length) => {for (i <- 0 until @length) yield {@__bufferReadMethod(buffer, underlying)}} +}} +@__bufferWriteMethod(buffer: String, data: String, tpe: Type) = {@tpe match { + case IntType(1, _) => {@{buffer}.put(@data)} + case IntType(2, _) => {@{buffer}.putShort(@data)} + case IntType(4, _) => {@{buffer}.putInt(@data)} + case IntType(8, _) => {@{buffer}.putLong(@data)} + case FloatType(4) => {@{buffer}.putFloat(@data)} + case FloatType(8) => {@{buffer}.putDouble(@data)} + case StringType(maxLength) => { + { + val bytes = @{data}.getBytes(Charset.forName("UTF-8")) + val endPosition = @{buffer}.position + @maxLength + @{buffer}.put(bytes, 0, math.max(bytes.length, @maxLength)) + while (@{buffer}.position < endPosition) { + @{buffer}.put(0: Byte) + } + }} + case ArrayType(underlying, length) => {for (i <- 0 until @length) {@__bufferWriteMethod(buffer, data + "(i)", underlying)}} +}} + +@__commentParagraphs(paragraphs: Seq[String]) = {@paragraphs.mkString("/**\n * ", "\n * ", "\n */")} +@__comment(message: Message) = {@__commentParagraphs(message.description.grouped(100).toList ++ message.fields.map(field => "@param " + field.name + " " + field.description))} + +/** Marker trait for MAVLink messages. All supported messages extend this trait. */ +sealed trait Message + +@for(__msg <- __messages) { +@__comment(__msg) +case class @{__scalaMessageType(__msg)}(@__msg.fields.map(__scalaFieldFormal).mkString(", ")) extends Message +} + +/** + * Wraps an unknown message. + * @@param id the ID of the message + * @@param payload the message's contents + */ +case class Unknown(id: Byte, payload: ByteBuffer) extends Message + +/** + * Provides utility methods for converting data to and from MAVLink messages. + */ +object Message { + + /** + * Interprets an ID and payload as a message. The contents must be ordered + * according to the MAVLink specification. + * Note: this method reads from the provided ByteBuffer and thereby modifies its + * internal state. + * @@param id ID of the message + * @@param payload contents of the message + * @@return payload interpreted as a message or 'Unknown' in case of an unknown ID + */ + def unpack(id: Byte, payload: ByteBuffer): Message = id match { + @for(__msg <- __messages) { + case @__msg.id => + @for(__field <- __msg.orderedFields) {val @__scalaFieldFormal(__field) = @__bufferReadMethod("payload", __field.tpe) + } + @{__scalaMessageType(__msg)}(@__msg.fields.map(__scalaFieldName(_)).mkString(", ")) + } + case u => Unknown(u, payload) + } + + /** + * Writes the contents of a message to a ByteBuffer. The message is encoded according + * to the MAVLink specification. + * @@param message the message to write + * @@param payload buffer that will hold the encoded message + * @@return id of the encoded message + */ + def pack(message: Message, payload: ByteBuffer): Byte = message match { + @for(__msg <- __messages) { + case m: @__scalaMessageType(__msg) => + @for(__field <- __msg.orderedFields) {@__bufferWriteMethod("payload", "m." + __scalaFieldName(__field), __field.tpe) + } + @__msg.id + } + case u: Unknown => + payload.put(u.payload) + u.id + } +} \ No newline at end of file diff --git a/mavlink-library/src/test/resources/concise.xml b/mavlink-library/src/test/resources/concise.xml new file mode 100644 index 0000000..fcbdc35 --- /dev/null +++ b/mavlink-library/src/test/resources/concise.xml @@ -0,0 +1,115 @@ + + + 1 + + + + Uninitialized system, state is unknown. + + + System is booting up. + + + System is calibrating and not flight-ready. + + + System is grounded and on standby. It can be launched any time. + + + System is active and might be already airborne. Motors are engaged. + + + System is in a non-normal flight mode. It can however still navigate. + + + System is in a non-normal flight mode. It lost control over parts or over the whole airframe. It is in mayday and going down. + + + System just initialized its power-down sequence, will shut down now. + + + + + + + The heartbeat message shows that a system is present and responding. + Global state of system. + + + Information about the main power source. + Voltage of the source (mV) + + + The IMU readings in a NED body frame + X acceleration (mm/s^2) + Y acceleration (mm/s^2) + Z acceleration (mm/s^2) + Angular speed around X axis (mrad / sec) + Angular speed around Y axis (mrad / sec) + Angular speed around Z axis (mrad / sec) + X Magnetic field (uT) + Y Magnetic field (uT) + Z Magnetic field (uT) + Altitude to mean sea level (mm) + Ambient temperature (mK) + + + Information on distance sensors + Relative altitude to ground (mm) + + + Ping a target system, usually used to determine latency. + System ID + Component ID + + + Acknowledgement packet + System ID + Component ID + + + Status of motors + m0 + m1 + m2 + m3 + + + The attitude in the aeronautical frame (right-handed, Z-down, X-front, Y-right). + Roll angle + Pitch angle + Yaw angle + + + The RAW values of the RC channels sent to the MAV to override info received from the RC radio. A value of UINT16_MAX means no change to that channel. A value of 0 means control of that channel should be released back to the RC radio. The standard PPM modulation is as follows: 1000 microseconds: 0%, 2000 microseconds: 100%. Individual receivers/transmitters might violate this specification. + System ID + Component ID + RC channel 1 value, in microseconds. A value of UINT16_MAX means to ignore this field. + RC channel 2 value, in microseconds. A value of UINT16_MAX means to ignore this field. + RC channel 3 value, in microseconds. A value of UINT16_MAX means to ignore this field. + RC channel 4 value, in microseconds. A value of UINT16_MAX means to ignore this field. + RC channel 5 value, in microseconds. A value of UINT16_MAX means to ignore this field. + RC channel 6 value, in microseconds. A value of UINT16_MAX means to ignore this field. + RC channel 7 value, in microseconds. A value of UINT16_MAX means to ignore this field. + RC channel 8 value, in microseconds. A value of UINT16_MAX means to ignore this field. + + + Status generated by radio + local signal strength + remote signal strength + how full the tx buffer is as a percentage + background noise level + remote background noise level + receive errors + count of error corrected packets + + + Test + a byte array + a float array + a char + a double + a string + + + diff --git a/mavlink-library/src/test/scala/com/github/jodersky/mavlink/MainTest.scala b/mavlink-library/src/test/scala/com/github/jodersky/mavlink/MainTest.scala new file mode 100644 index 0000000..faf252a --- /dev/null +++ b/mavlink-library/src/test/scala/com/github/jodersky/mavlink/MainTest.scala @@ -0,0 +1,16 @@ +package com.github.jodersky.mavlink + +import scala.io.Source +import scala.xml.XML +import trees._ + +object MainTest { + + def main(args: Array[String]): Unit = { + val definition = XML.load(getClass.getResource("/concise.xml")) + val dialect = Parser.parseDialect(definition) + val generator = new Generator(dialect) + println(generator.generate()) + } + +} \ No newline at end of file diff --git a/mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/MavlinkKeys.scala b/mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/MavlinkKeys.scala new file mode 100644 index 0000000..e09a623 --- /dev/null +++ b/mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/MavlinkKeys.scala @@ -0,0 +1,14 @@ +package com.github.jodersky.mavlink.sbt + +import sbt._ +import sbt.Keys._ +import java.io.File + +object MavlinkKeys { + + lazy val mavlinkDialect = settingKey[File]("Dialect definition from which to generate files.") + lazy val mavlinkTarget = settingKey[File]("Target directory to contain all generated mavlink files.") + + lazy val mavlinkGenerate = taskKey[Seq[File]]("Generate mavlink files.") + +} \ No newline at end of file diff --git a/mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/SbtMavlink.scala b/mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/SbtMavlink.scala new file mode 100644 index 0000000..41a6bcf --- /dev/null +++ b/mavlink-plugin/src/main/scala/com/github/jodersky/mavlink/sbt/SbtMavlink.scala @@ -0,0 +1,51 @@ +package com.github.jodersky.mavlink.sbt + +import com.github.jodersky.mavlink.Parser +import com.github.jodersky.mavlink.Generator +import scala.xml.XML + +import MavlinkKeys._ +import sbt._ +import sbt.Keys._ +import sbt.plugins._ + +object SbtMavlink extends AutoPlugin { + + override def trigger = allRequirements + + override def requires = JvmPlugin //this is required as sourceGenerators are otherwise reset + + override lazy val projectSettings: Seq[Setting[_]] = Seq( + mavlinkDialect in Compile := (baseDirectory in Compile).value / "conf" / "mavlink.xml", + mavlinkTarget in Compile := (sourceManaged in Compile).value, + mavlinkGenerate in Compile := generationTask.value, + sourceGenerators in Compile += (mavlinkGenerate in Compile).taskValue + ) + + lazy val generationTask = Def.task[Seq[File]] { + val dialectDefinitionFile = (mavlinkDialect in Compile).value + + if (!dialectDefinitionFile.exists) sys.error( + "Dialect definition " + dialectDefinitionFile.getAbsolutePath + " does not exist." + ) + + val dialectDefinition = XML.loadFile(dialectDefinitionFile) + val dialect = Parser.parseDialect(dialectDefinition) + val pathToSource = (new Generator(dialect)).generate() + + val outDirectory = (mavlinkTarget in Compile).value + + streams.value.log.info("Generating mavlink files...") + + val files = for ((path, source) <- pathToSource) yield { + streams.value.log.debug("Generating " + path) + val file = outDirectory / path + IO.write(file, source) + file.getAbsoluteFile + } + + streams.value.log.info("Done generating mavlink files") + files + } + +} diff --git a/project/Build.scala b/project/Build.scala new file mode 100644 index 0000000..ddf108e --- /dev/null +++ b/project/Build.scala @@ -0,0 +1,42 @@ +import sbt._ +import sbt.Keys._ +import play.twirl.sbt.SbtTwirl +import play.twirl.sbt.Import._ + +object ApplicationBuild extends Build { + + val common = Seq( + scalaVersion := "2.10.4", + scalacOptions ++= Seq("-feature", "-deprecation"), + organization := "com.github.jodersky", + version := "0.1-SNAPSHOT" + ) + + lazy val root = Project("root", file(".")).aggregate( + library, + plugin + ) + + lazy val library = ( + Project("mavlink-library", file("mavlink-library")) + enablePlugins(SbtTwirl) + settings(common: _*) + settings( + libraryDependencies += "com.github.scala-incubator.io" %% "scala-io-file" % "0.4.2", + TwirlKeys.templateImports += "com.github.jodersky.mavlink._", + TwirlKeys.templateImports += "com.github.jodersky.mavlink.trees._" + ) + ) + + lazy val plugin = ( + Project("mavlink-plugin", file("mavlink-plugin")) + settings(common: _*) + settings( + sbtPlugin := true, + name := "sbt-mavlink" + ) + dependsOn(library) + ) + +} + diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..7b458b6 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4") \ No newline at end of file -- cgit v1.2.3