aboutsummaryrefslogtreecommitdiff
path: root/vfd-dashboard/src/main
diff options
context:
space:
mode:
authorJakob Odersky <jodersky@gmail.com>2015-01-20 17:37:10 +0100
committerJakob Odersky <jodersky@gmail.com>2015-01-20 17:37:10 +0100
commit13eae49f4c48c03b2a7a9b40a08ca68063044c6d (patch)
tree6ad530458c1600dabdf2f6cdcf3482282268ebca /vfd-dashboard/src/main
parentdb266a316a2d5a22cd11503094e10b327a8e1cd6 (diff)
downloadmavigator-13eae49f4c48c03b2a7a9b40a08ca68063044c6d.tar.gz
mavigator-13eae49f4c48c03b2a7a9b40a08ca68063044c6d.tar.bz2
mavigator-13eae49f4c48c03b2a7a9b40a08ca68063044c6d.zip
rename subprojects
Diffstat (limited to 'vfd-dashboard/src/main')
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/Environment.scala14
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/Launcher.scala34
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/Main.scala13
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/MavlinkSocket.scala65
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/ui/Layout.scala57
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/SvgInstrument.scala55
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/ui/components/instruments.scala124
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Communication.scala106
-rw-r--r--vfd-dashboard/src/main/scala/vfd/dashboard/ui/panels/Primary.scala46
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&amp;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