diff options
Diffstat (limited to 'vfd-dashboard/src/main/scala/vfd/dashboard/ui')
5 files changed, 388 insertions, 0 deletions
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 |