diff options
author | Jakob Odersky <jodersky@gmail.com> | 2015-01-20 17:37:10 +0100 |
---|---|---|
committer | Jakob Odersky <jodersky@gmail.com> | 2015-01-20 17:37:10 +0100 |
commit | 13eae49f4c48c03b2a7a9b40a08ca68063044c6d (patch) | |
tree | 6ad530458c1600dabdf2f6cdcf3482282268ebca /vfd-dashboard/src/main/scala/vfd/dashboard | |
parent | db266a316a2d5a22cd11503094e10b327a8e1cd6 (diff) | |
download | mavigator-13eae49f4c48c03b2a7a9b40a08ca68063044c6d.tar.gz mavigator-13eae49f4c48c03b2a7a9b40a08ca68063044c6d.tar.bz2 mavigator-13eae49f4c48c03b2a7a9b40a08ca68063044c6d.zip |
rename subprojects
Diffstat (limited to 'vfd-dashboard/src/main/scala/vfd/dashboard')
9 files changed, 514 insertions, 0 deletions
diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/Environment.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/Environment.scala new file mode 100644 index 0000000..d4dd306 --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/Environment.scala @@ -0,0 +1,14 @@ +package vfd.dashboard + +import org.scalajs.dom.HTMLElement + +/** Represents an application's environment */ +trait Environment { + + /** The application's root element. */ + def root: HTMLElement + + /** Retrieve an asset's URL based on its file location. */ + def asset(file: String): String + +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/Launcher.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/Launcher.scala new file mode 100644 index 0000000..6a9ba82 --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/Launcher.scala @@ -0,0 +1,34 @@ +package vfd.dashboard + +import scala.scalajs.js.annotation.JSExport + +import org.scalajs.dom + +@JSExport("Launcher") +class Launcher(rootId: String, assetsBase: String) { + + lazy val env = new Environment { + val root = dom.document.getElementById(rootId) + def asset(file: String) = assetsBase + "/" + file + } + + @JSExport + def main() = { + import env._ + + val args: Seq[(String, String)] = for ( + i <- 0 until root.attributes.length; + attr = root.attributes.item(i); + if attr.name.startsWith("data-") + ) yield { + attr.name.drop(5) -> attr.value + } + + while (env.root.hasChildNodes) { + env.root.removeChild(env.root.firstChild) + } + + Main.main(args.toMap)(env) + } + +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/Main.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/Main.scala new file mode 100644 index 0000000..36ca2db --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/Main.scala @@ -0,0 +1,13 @@ +package vfd.dashboard + +import vfd.dashboard.ui.Layout + +object Main { + + def main(args: Map[String, String])(implicit env: Environment) = { + val socket = new MavlinkSocket(args("socketurl"), args("remotesystemid").toInt) + val layout = new Layout(socket) + + env.root.appendChild(layout.element) + } +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/MavlinkSocket.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/MavlinkSocket.scala new file mode 100644 index 0000000..162ca64 --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/MavlinkSocket.scala @@ -0,0 +1,65 @@ +package vfd.dashboard + +import scala.scalajs.js +import scala.scalajs.js.Any.fromFunction1 + +import org.mavlink.Packet +import org.mavlink.Parser +import org.mavlink.messages.Message +import org.scalajs.dom + +import rx.core.Rx +import rx.core.Var +import rx.ops.RxOps + +class MavlinkSocket(url: String, remoteSystemId: Int) { + + lazy val packet: Var[Packet] = Var(Packet.Empty) + lazy val message: Rx[Message] = packet.map{p => + Message.unpack(p.messageId, p.payload) + } + + object stats { + val crcErrors = Var(0) + val overflows = Var(0) + val wrongIds = Var(0) + val packets = Var(0) + val open = Var(false) + } + + private val parser = new Parser( + pckt => { + pckt match { + case Packet(seq, `remoteSystemId`, compId, msgId, payload) => + packet() = pckt + stats.packets() += 1 + case _ => + stats.wrongIds() += 1 + } + }, + err => { + err match { + case Parser.ParseErrors.CrcError => stats.crcErrors() += 1 + case Parser.ParseErrors.OverflowError => stats.overflows() += 1 + } + }) + + private val connection = new dom.WebSocket(url) + + connection.binaryType = "arraybuffer"; + connection.onopen = (e: dom.Event) => { + stats.open() = true + } + connection.onmessage = (e: dom.MessageEvent) => { + val buffer = e.data.asInstanceOf[js.typedarray.ArrayBuffer] + val view = new js.typedarray.DataView(buffer) + + for (i <- 0 until view.byteLength) { + parser.push(view.getInt8(i)) + } + } + connection.onclose = (e: dom.CloseEvent) => { + stats.open() = false + } + +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/ui/Layout.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/Layout.scala new file mode 100644 index 0000000..29ca4ca --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/Layout.scala @@ -0,0 +1,57 @@ +package vfd.dashboard.ui + +import org.scalajs.dom.HTMLElement + +import scalatags.JsDom.all.ExtendedString +import scalatags.JsDom.all.bindNode +import scalatags.JsDom.all.`class` +import scalatags.JsDom.all.div +import scalatags.JsDom.all.height +import scalatags.JsDom.all.iframe +import scalatags.JsDom.all.p +import scalatags.JsDom.all.src +import scalatags.JsDom.all.stringAttr +import scalatags.JsDom.all.stringFrag +import scalatags.JsDom.all.stringStyle +import scalatags.JsDom.all.style +import scalatags.JsDom.all.width +import vfd.dashboard.Environment +import vfd.dashboard.MavlinkSocket +import vfd.dashboard.ui.panels.Communication +import vfd.dashboard.ui.panels.Primary + +class Layout(socket: MavlinkSocket) { + + val map = iframe( + width := "100%", + height := "350px", + "frameborder".attr := "0", + "scrolling".attr := "no", + "marginheight".attr := "0", + "marginwidth".attr := "0", + src := "http://www.openstreetmap.org/export/embed.html?bbox=6.5611016750335684%2C46.51718501017836%2C6.570038795471191%2C46.520577350893525&layer=mapnik") + + val feed = div(style := "width: 100%; height: 460px; color: #ffffff; background-color: #c2c2c2; text-align: center;")( + p(style := "padding-top: 220px")("video feed")) + + def element(implicit env: Environment): HTMLElement = div(`class` := "container-fluid")( + div(`class` := "row")( + div(`class` := "col-xs-12")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")()))), + div(`class` := "row")( + div(`class` := "col-xs-4")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")( + map))), + div(`class` := "col-xs-5")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")( + feed)), + div(`class` := "panel panel-default")( + div(`class` := "panel-body")(Primary(socket)))), + div(`class` := "col-xs-3")( + div(`class` := "panel panel-default")( + div(`class` := "panel-body")(Communication(socket)))))).render + +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/SvgInstrument.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/SvgInstrument.scala new file mode 100644 index 0000000..8ddba1a --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/SvgInstrument.scala @@ -0,0 +1,55 @@ +package vfd.dashboard.ui.components + +import scala.scalajs.js.Any.fromFunction1 + +import org.scalajs.dom + +import scalatags.JsDom.all.ExtendedString +import scalatags.JsDom.all.`object` +import scalatags.JsDom.all.stringAttr +import scalatags.JsDom.all.stringFrag +import scalatags.JsDom.all.stringStyle +import scalatags.JsDom.all.`type` +import scalatags.JsDom.all.width +import vfd.dashboard.Environment + +trait SvgInstrument[A] { + + /** SVG object element that contains the rendered instrument */ + def element: dom.HTMLObjectElement + + /** Actual svg document */ + protected def content: dom.Document = element.contentDocument + + /** Moveable parts of the instrument */ + protected def moveable: Seq[dom.HTMLElement] + + /** Updates the instrument to show a new value */ + def update(value: A): Unit + + protected def load(event: dom.Event): Unit = { + for (part <- moveable) { + part.style.transition = "transform 250ms ease-out" + } + } + + element.addEventListener("load", (e: dom.Event) => load(e)) +} + +object SvgInstrument { + + def svg(name: String)(implicit app: Environment): dom.HTMLObjectElement = { + val path = app.asset("images/instruments/" + name + ".svg") + `object`(`type` := "image/svg+xml", "data".attr := path, width := "100%")( + "Error loading instrument " + name).render + } + + def translate(elem: dom.HTMLElement, x: Int, y: Int): Unit = { + elem.style.transform = "translate(" + x + "px, " + y + "px)"; + } + + def rotate(elem: dom.HTMLElement, deg: Int): Unit = { + elem.style.transform = "rotateZ(" + deg + "deg)"; + } + +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/instruments.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/instruments.scala new file mode 100644 index 0000000..1cde83c --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/instruments.scala @@ -0,0 +1,124 @@ +package vfd.dashboard.ui.components + +import org.scalajs.dom + +import scalatags.JsDom.all.ExtendedString +import scalatags.JsDom.all.`object` +import scalatags.JsDom.all.stringAttr +import scalatags.JsDom.all.stringFrag +import scalatags.JsDom.all.stringStyle +import scalatags.JsDom.all.`type` +import scalatags.JsDom.all.width +import vfd.dashboard.Environment + +class Led(implicit env: Environment) extends SvgInstrument[String] { + lazy val element = `object`(`type` := "image/svg+xml", "data".attr := env.asset("images/leds/led.svg"), width := "100%")( + "Error loading led.").render + + def update(color: String) = { + content.getElementById("light").setAttribute("fill", color) + } + + protected def moveable = Seq() + +} + +class Horizon(implicit env: Environment) extends SvgInstrument[(Double, Double)] { + lazy val element = SvgInstrument.svg("horizon") + + def pitch = content.getElementById("pitch") + def roll = content.getElementById("roll") + protected def moveable = Seq(pitch, roll) + + def update(pitchRoll: (Double, Double)) = { + SvgInstrument.translate(pitch, 0, pitchRoll._1.toInt) + SvgInstrument.rotate(roll, pitchRoll._2.toInt) + } +} + +class Altimeter(implicit env: Environment) extends SvgInstrument[Double] { + lazy val element = SvgInstrument.svg("altimeter") + + def hand = content.getElementById("hand") + protected def moveable = Seq(hand) + + // 36deg === 1m + def update(altitude: Double) = { + SvgInstrument.rotate(hand, (altitude * 36).toInt) + } +} + +class Compass(implicit env: Environment) extends SvgInstrument[Double] { + lazy val element = SvgInstrument.svg("compass") + + def plate = content.getElementById("heading") + protected def moveable = Seq(plate) + + def update(heading: Double) = { + SvgInstrument.rotate(plate, heading.toInt) + } +} + +class Generic( + min: Double, + med: Double, + max: Double, + unit: String)(implicit env: Environment) extends SvgInstrument[Double] { + + lazy val element = SvgInstrument.svg("generic") + + def handElement = content.getElementById("hand") + def unitElement = content.getElementById("unit") + def valueElement = content.getElementById("value") + def minElement = content.getElementById("min") + def medElement = content.getElementById("med") + def maxElement = content.getElementById("max") + protected def moveable = Seq(handElement) + + override protected def load(e: dom.Event) = { + unitElement.textContent = unit + minElement.textContent = min.toString + medElement.textContent = med.toString + maxElement.textContent = max.toString + update(min) + super.load(e) + } + + def update(value: Double) = { + SvgInstrument.rotate(handElement, (value * 270 / (max - min)).toInt) + valueElement.textContent = value.toString + } +} + +class Bar(implicit env: Environment) extends SvgInstrument[Double] { + + lazy val element = SvgInstrument.svg("bar") + + def level = content.getElementById("level") + protected def moveable = Seq(level) + + def update(value: Double) = { + SvgInstrument.translate(level, 0, (97 * (1 - value / 100)).toInt) + } + +} + +class Balance(implicit env: Environment) extends SvgInstrument[(Double, Double, Double, Double)] { + lazy val element = SvgInstrument.svg("balance") + + def position = content.getElementById("position") + protected def moveable = Seq(position) + + def update(value: (Double, Double, Double, Double)) = { + val m0 = value._1 + val m1 = value._2 + val m2 = value._3 + val m3 = value._4 + val s = m0 + m1 + m2 + m3 + val i = (m0 - m2) / s + val j = (m1 - m3) / s + val x = 0.5 * (i - j) + val y = 0.5 * (-i - j) + SvgInstrument.translate(position, (x * 50).toInt, (y * 50).toInt) + } +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Communication.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Communication.scala new file mode 100644 index 0000000..dd43ab4 --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Communication.scala @@ -0,0 +1,106 @@ +package vfd.dashboard.ui.panels + +import org.mavlink.messages.Heartbeat +import org.mavlink.messages.Motor +import org.mavlink.messages.Power +import org.scalajs.dom.HTMLElement +import rx.core.Obs +import scalatags.JsDom.all.bindNode +import scalatags.JsDom.all.`class` +import scalatags.JsDom.all.div +import scalatags.JsDom.all.stringAttr +import scalatags.JsDom.all.table +import scalatags.JsDom.all.tbody +import scalatags.JsDom.all.td +import scalatags.JsDom.all._ +import vfd.dashboard.Environment +import vfd.dashboard.MavlinkSocket +import vfd.dashboard.ui.components.Generic +import vfd.dashboard.ui.components.Balance +import vfd.dashboard.ui.components.Bar +import vfd.dashboard.ui.components.Led + +object Communication { + + def apply(socket: MavlinkSocket)(implicit app: Environment): HTMLElement = { + + val hb = i(`class` := "fa fa-heart heartbeat").render + + def foo() = { + hb.textContent = "" + } + + val motor0 = new Generic(0, 50, 100, "%") + val motor1 = new Generic(0, 50, 100, "%") + val motor2 = new Generic(0, 50, 100, "%") + val motor3 = new Generic(0, 50, 100, "%") + val powerDistribution = new Balance() + val batteryLevel = new Bar() + + Obs(socket.message, skipInitial = true) { + socket.message() match { + case Motor(m0, m1, m2, m3) => + motor0.update(m0) + motor1.update(m1) + motor2.update(m2) + motor3.update(m3) + powerDistribution.update(m0, m1, m2, m3) + + case Power(mV) => + batteryLevel.update(100 * (mV - 9600) / 12600) + case Heartbeat(_) => { + hb.style.visibility = "hidden" + hb.style.visibility = "visible" + //hb.classList.remove("heartbeat") + //hb.offsetHeight + //hb.classList.add("heartbeat") + } + case _ => + } + } + + div( + table(`class` := "table")( + thead("Communication"), + tbody( + tr( + td("Conn"), + div(width := "20px")(td((new Led()).element)), + td("Server"), + td("5 ms")), + tr( + td("Uplink"), + td("-20 dBm"), + td("Heartbeat"), + td(hb)))), + table(`class` := "table-instrument", style := "height: 100px")( + tbody( + tr( + td(), + td(), + td(), + td(), + td(), + td(), + td(), + td(), + td(), + td(batteryLevel.element)))), + table (`class` := "table-instrument")( + thead("Motors"), + tbody( + tr( + td(motor0.element), + td(), + td(motor1.element)), + tr( + td(), + td(powerDistribution.element), + td()), + tr( + td(motor2.element), + td(), + td(motor3.element))))).render + } + +}
\ No newline at end of file diff --git a/vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Primary.scala b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Primary.scala new file mode 100644 index 0000000..14b26f6 --- /dev/null +++ b/vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Primary.scala @@ -0,0 +1,46 @@ +package vfd.dashboard.ui.panels + +import org.mavlink.messages.Attitude +import org.scalajs.dom.HTMLElement + +import rx.core.Obs +import scalatags.JsDom.all.bindNode +import scalatags.JsDom.all.`class` +import scalatags.JsDom.all.stringAttr +import scalatags.JsDom.all.table +import scalatags.JsDom.all.tbody +import scalatags.JsDom.all.td +import scalatags.JsDom.all.tr +import vfd.dashboard.Environment +import vfd.dashboard.MavlinkSocket +import vfd.dashboard.ui.components.Altimeter +import vfd.dashboard.ui.components.Compass +import vfd.dashboard.ui.components.Horizon + +object Primary { + + def apply(socket: MavlinkSocket)(implicit env: Environment): HTMLElement = { + + val compass = new Compass + val horizon = new Horizon + val altimeter = new Altimeter + + Obs(socket.message, skipInitial = true) { + socket.message() match { + case Attitude(roll, pitch, yaw) => + horizon.update(pitch, roll) + compass.update(yaw) + case _ => () + } + } + + table(`class` := "table-instrument")( + tbody( + tr( + td(compass.element), + td(horizon.element), + td(altimeter.element)))).render + + } + +}
\ No newline at end of file |