diff options
Diffstat (limited to 'mavigator-cockpit/src')
26 files changed, 421 insertions, 794 deletions
diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/Cockpit.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Cockpit.scala new file mode 100644 index 0000000..f8aba76 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Cockpit.scala @@ -0,0 +1,26 @@ +package mavigator +package cockpit + +import scala.scalajs.js.annotation.JSExport + +import mavigator.util.{Environment, Application} +import scalatags.JsDom.all._ +import util._ + +class Cockpit + extends Page + with Layout + with MavlinkWebSockets + with Instruments + +@JSExport("mavigator_cockpit_Main") +object Cockpit extends Application { + + override def main(args: Map[String, String])(implicit env: Environment): Unit = { + val app = new Cockpit + app.attach(env) + app.connect(args("socketUrl"), args("remoteSystemId").toInt) + } + +} + diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/Instruments.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Instruments.scala new file mode 100644 index 0000000..7c4bf92 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Instruments.scala @@ -0,0 +1,173 @@ +package mavigator +package cockpit + +import org.scalajs.dom +import org.scalajs.dom.html + +import scalatags.JsDom.all._ + +import util.Page + +trait Instruments { page: Page => + + trait Instrument[A] { + def element: html.Element + def update(newValue: A): Unit + } + + /** Common behaviour for svg-based instruments. */ + abstract class SvgInstrument[A]( + path: String + ) extends Instrument[A] { + + override def element: html.Element = objectElement + + /** SVG object element that contains the rendered instrument */ + lazy val objectElement: html.Object = SvgInstrument.svgObject(path) + + /** Retrieves an element of the underlying SVG document by ID. */ + protected def part(id: String): html.Element = + objectElement.contentDocument.getElementById(id).asInstanceOf[html.Element] + + /** Movable parts of the instrument */ + protected def moveable: Seq[html.Element] + + /** Called when element has been loaded. */ + private def load(event: dom.Event): Unit = { + for (part <- moveable) { + part.style.transition = "transform 20ms ease-out" + } + } + + element.addEventListener("load", (e: dom.Event) => load(e)) + } + + /** Contains helpers for SVG instruments. */ + object SvgInstrument { + + /** Retrieves an SVG object element by its instrument's name. */ + def svgObject(path: String): html.Object = { + val fullPath = page.asset("images/" + path) + `object`(`type` := "image/svg+xml", "data".attr := fullPath, width := 100.pct)( + "Error loading instrument " + fullPath).render + } + + /** Applies translation styling to an element. */ + def translate(elem: html.Element, x: Int, y: Int): Unit = { + elem.style.transform = "translate(" + x + "px, " + y + "px)"; + } + + /** Applies rotation styling to an element. */ + def rotate(elem: html.Element, rad: Double): Unit = { + elem.style.transform = "rotateZ(" + rad + "rad)"; + } + + } + + lazy val attitudeOverlay = new SvgInstrument[(Float, Float)]("hud/attitude.svg") { + override def element = div(`class`:="hud-overlay")(objectElement).render + private lazy val pitchPart = part("pitch") + private lazy val rollPart = part("roll") + override lazy val moveable = Seq(pitchPart, rollPart) + + override def update(pitchRoll: (Float, Float)) = { + import SvgInstrument._ + val (pitch, roll) = pitchRoll + translate(pitchPart, 0, (pitch * 180 / math.Pi * 10).toInt) // 1deg === 10px + rotate(rollPart, roll) + } + } + + lazy val horizonOverlay = new SvgInstrument[(Float, Float)]("hud/horizon.svg") { + import SvgInstrument._ + + override def element = div(`class`:="hud-overlay")(objectElement).render + lazy val horizon = part("horizon") + lazy override val moveable = Seq(horizon) + + override def update(pitchRoll: (Float, Float)) = { + val (pitch, roll) = pitchRoll + + val t = (pitch * 180 / math.Pi * 10).toInt // 1deg === 10px + + horizon.style.transform = s"rotateZ(${roll}rad)translate(0px, ${t}px) " + } + } + + + val overlayStyle = """ + |.hud-overlay { + | position: absolute; + | left: 0; + | right: 0; + | top: 0; + | bottom: 0; + | + | display: flex; + | flex-direction: row; + | justify-content: center; + | align-items: center; + |} + |.hud-overlay > * { + | flex: 1 1 0; + | width: 100%; + | height: 100%; + | max-width: 100%; + | max-height: 100%; + |}""".stripMargin + + + def mode(name: String, kind: String, on: Boolean = false) = { + div(`class` := s"mode $kind ${if (!on) "off"}")(name) + } + + //TODO make these into real instruments and lazy vals + def modes = div(style := "float: right;")( + mode("LINK", "danger", true), + mode("BAT", "warning", true), + mode("GPS", "warning", true), + mode("STABILIZED", "info", true) + ) + + val modeStyle = """ +.mode { + display: inline-block; + box-sizing: border-box; + text-decoration: normal; + margin-right: 5px; + padding: 5px; +} + +.mode.danger { + color: #d9534f; + text-shadow: 0 0 5px #d9534f; + animation: danger-blink 0.5s linear infinite; + -webkit-animation: danger-blink 0.5s linear infinite; +} + +.mode.warning { + color: #f0ad4e; + text-shadow: 0 0 5px #f0ad4e; +} + +.mode.info { + color: #5bc0de; + text-shadow: 0 0 5px #5bc0de; +} + +.mode.success { + color: #5cb85c; + text-shadow: 0 0 5px #5cb85c; +} + +.mode.off { + color: #e6e6e6; + text-shadow: none; + animation: none; + -webkit-animation: none; +} +""" + + def instrumentStyles: Seq[String] = Seq(overlayStyle, modeStyle) + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/Layout.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Layout.scala new file mode 100644 index 0000000..887c4d6 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/Layout.scala @@ -0,0 +1,54 @@ +package mavigator +package cockpit + +import org.scalajs.dom.html +import scalatags.JsDom.all._ + +import util._ + +/** Provides main cockpit layout. */ +trait Layout { self: Page with Instruments => + + /** Elements to display in the mode control pannel (top panel). */ + def mcp = div(id := "mcp")( + img(src := asset("images/logo-invert.svg"), style:="height: 20px; margin: 5px;"), + span(`class`:="mode warning")("Demo System"), + modes + ) + + /** Element to deisplay on heads-up display (main area). */ + def hud = div(id :="hud")( + attitudeOverlay.element + ) + + val layoutStyle = """ + |#cockpit { + | width: 100%; + | height: 100%; + | display: flex; + | flex-direction: column; + | justify-content: flex-start; + | align-items: stretch; + | + | background-color: pink; + |} + | + |#mcp { + | flex: 0 1 0; + | background-color: #222222; + |} + | + |#hud { + | flex: 1 1 auto; + | position: relative; + | background-color: lightblue; + |}""".stripMargin + + override def styles = Seq(layoutStyle) ++ instrumentStyles + + override def elements: Seq[html.Element] = Seq(div(id := "cockpit")( + mcp, + hud + ).render) + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/cockpit/MavlinkWebSockets.scala b/mavigator-cockpit/src/main/scala/mavigator/cockpit/MavlinkWebSockets.scala new file mode 100644 index 0000000..3797005 --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/cockpit/MavlinkWebSockets.scala @@ -0,0 +1,60 @@ +package mavigator +package cockpit + +import org.mavlink._ +import org.mavlink.Parser.Errors._ +import org.mavlink.messages._ + +import org.scalajs.dom +import scalajs.js + +trait MavlinkWebSockets { self: Instruments => + + private class MavlinkWebSocket(url: String, remoteSystemId: Int) { + + private var _open = false + def open: Boolean = _open + + private val parser = new Parser( + { + case pckt@Packet(seq, `remoteSystemId`, compId, msgId, payload) => + val msg = Message.unpack(pckt.messageId, pckt.payload) + onMessage(msg) + //_packets() = _packets.now + 1 + case _ => + //_wrongIds() = _wrongIds.now + 1 + }, + { + case CrcError => //_crcErrors() = _crcErrors.now + 1 + case OverflowError => //_overflows() = _overflows.now + 1 + } + ) + + private val connection = new dom.WebSocket(url) + + connection.binaryType = "arraybuffer" + + connection.onopen = (e: dom.Event) => { + _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) => { + _open = false + } + + } + + private def onMessage(msg: Message): Unit = msg match { + case a: Attitude => attitudeOverlay.update((a.pitch.toFloat, a.roll.toFloat)) + case _ => () + } + + def connect(url: String, remoteSystemId: Int): Unit = new MavlinkWebSocket(url, remoteSystemId) +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala deleted file mode 100644 index f3a111a..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/Main.scala +++ /dev/null @@ -1,23 +0,0 @@ -package mavigator.dashboard - -import scala.scalajs.js -import scala.scalajs.js.annotation.JSExport - -import org.scalajs.dom.html - -import mavigator.dashboard.ui.Layout -import mavigator.util.Environment -import mavigator.util.Application - -import scalatags.JsDom.all._ - -@JSExport("mavigator_dashboard_Main") -object Main extends Application { - - override def main(args: Map[String, String])(implicit env: Environment): Unit = { - val socket = new MavlinkSocket(args("socketUrl"), args("remoteSystemId").toInt) - val layout = new Layout(socket) - - env.root.appendChild(layout.element) - } -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala deleted file mode 100644 index 7f4ffdc..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/MavlinkSocket.scala +++ /dev/null @@ -1,70 +0,0 @@ -package mavigator.dashboard - -import scala.scalajs.js -import scala.scalajs.js.Any.fromFunction1 -import org.mavlink.Packet -import org.mavlink.Parser -import org.mavlink.Parser.Errors._ -import org.mavlink.messages.Message -import org.scalajs.dom -import scala.concurrent.duration._ -import rx._ -import rx.ops._ -import scala.concurrent.ExecutionContext.Implicits.global - -class MavlinkSocket(url: String, val remoteSystemId: Int) { - implicit val scheduler = new DomScheduler - - lazy val packet: Var[Packet] = Var(Packet.empty) - lazy val message: Rx[Message] = packet.map{p => - Message.unpack(p.messageId, p.payload) - } - - object stats { - private val DebounceTime = 1.seconds - - private[MavlinkSocket] val _crcErrors = Var(0) - private[MavlinkSocket] val _overflows = Var(0) - private[MavlinkSocket] val _wrongIds = Var(0) - private[MavlinkSocket] val _packets = Var(0) - - val crcErrors = _crcErrors.debounce(DebounceTime) - val overflows = _overflows.debounce(DebounceTime) - val wrongIds = _wrongIds.debounce(DebounceTime) - val packets = _packets.debounce(DebounceTime) - val open = Var(false) - } - - private val parser = new Parser( - { - case pckt@Packet(seq, `remoteSystemId`, compId, msgId, payload) => - packet() = pckt - stats._packets() += 1 - case _ => - stats._wrongIds() += 1 - }, - { - case CrcError => stats._crcErrors() += 1 - case 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 - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala deleted file mode 100644 index 2637ddf..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/RxUtil.scala +++ /dev/null @@ -1,70 +0,0 @@ -package mavigator.dashboard - -import scala.language.implicitConversions -import scala.util.Failure -import scala.util.Success - -import org.scalajs.dom.html - -import rx.Obs -import rx.Rx -import rx.Rx -import rx.Var -import rx.Var -import scalatags.JsDom.all.Frag -import scalatags.JsDom.all.HtmlTag -import scalatags.JsDom.all.backgroundColor -import scalatags.JsDom.all.bindNode -import scalatags.JsDom.all.span -import scalatags.JsDom.all.stringFrag -import scalatags.JsDom.all.stringStyle - -package object rxutil { - - /** Rx, implicitly enhanced with additional methods. */ - implicit class RichRx(val rx: Rx[_]) extends AnyVal { - - /** - * Builds a new Rx by applying a partial function to all values of - * this Rx on which the function is defined. - * @param initial initial value of the returned Rx - * @param pf the partial function which filters and maps this Rx - * @return a new Rx resulting from applying the given partial - * function pf to each value on which it is defined and collecting - * the result - */ - def collect[B](initial: B)(pf: PartialFunction[Any, B]): Rx[B] = { - val result: Var[B] = Var(initial) - Obs(rx, skipInitial = true) { - if (pf.isDefinedAt(rx())) { - result() = pf(rx()) - } - } - result - } - - } - - /** - * Copied from https://github.com/lihaoyi/workbench-example-app/blob/todomvc/src/main/scala/example/Framework.scala - * - * Sticks some Rx into a Scalatags fragment, which means hooking up an Obs - * to propagate changes into the DOM via the element's ID. Monkey-patches - * the Obs onto the element itself so we have a reference to kill it when - * the element leaves the DOM (e.g. it gets deleted). - */ - implicit def rxMod[T <: html.Element](r: Rx[HtmlTag]): Frag = { - def rSafe = r.toTry match { - case Success(v) => v.render - case Failure(e) => span(e.toString, backgroundColor := "red").render - } - var last = rSafe - Obs(r, skipInitial = true) { - val newLast = rSafe - last.parentElement.replaceChild(newLast, last) - last = newLast - } - bindNode(last) - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala deleted file mode 100644 index 18c5c27..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Hud.scala +++ /dev/null @@ -1,47 +0,0 @@ -package mavigator.dashboard.ui - -import mavigator.util.Environment -import mavigator.dashboard.ui.instruments._ - -import scalatags.JsDom.all._ -import rx._ - -class Hud(attitude: Rx[(Double, Double)])(implicit env: Environment) { - - private def overlay(name: String, z: Int, thinnerThanWide: Boolean = false) = { - val direction = if (thinnerThanWide) "row" else "column" - div( - style:= - "position: absolute; left: 0; right: 0; top: 0; bottom: 0;" + - s"display: flex; align-content: center; align-items: stretch; flex-direction: $direction;"+ - s"z-index: $z;" - )( - `object`(`type`:="image/svg+xml", "data".attr:=env.asset("images/hud/" + name + ".svg"), style := "flex: 1 1 100%;") - ) - } - - object Horizon extends SvgInstrument[(Double, Double)] { - import SvgInstrument._ - - val value = attitude - - lazy val element = `object`(`type`:="image/svg+xml", "data".attr:=env.asset("images/hud/horizon.svg"), style:="flex: 1 1 100%;").render - lazy val horizon = part("horizon") - lazy val moveable = Seq(horizon) - - protected def update(pitchRoll: (Double, Double)) = { - rotate(horizon, pitchRoll._2) - //translate(horizon, 0, (pitchRoll._1 * 180 / math.Pi).toInt) // 1deg === 1px - } - } - - val element = div( - style:= - "position: absolute; left: 0; right: 0; top: 0; bottom: 0;" + - "display: flex; align-content: stretch; align-items: stretch; flex-direction: row;"+ - "z-index: 0;" - )( - Horizon.element - ) -} - diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala deleted file mode 100644 index 902cb9c..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/Layout.scala +++ /dev/null @@ -1,266 +0,0 @@ -package mavigator.dashboard.ui - -import rx._ -import scalatags.JsDom.all._ -import mavigator.util.Environment -import mavigator.dashboard.MavlinkSocket -import mavigator.dashboard.ui.instruments._ -import org.mavlink.messages._ -import mavigator.dashboard.rxutil._ - -class Layout(socket: MavlinkSocket)(implicit env: Environment) { - - private def panel(contents: Frag*) = div(`class` := "d-panel")(contents: _*) - - private def mode(name: String, kind: String, on: Boolean = false) = div(`class` := s"mode $kind ${if (!on) "off"}")(name) - - val modes = div( - mode("MANUAL", "warning", true), - mode("STABILIZED", "info", true), - mode("GUIDED", "success", true), - mode("AUTO", "success", true)) - - val infos = div( - mode("BAY", "info"), - mode("RECOVERY", "danger"), - mode("NOGPS", "warning", true), - mode("OVERLOAD", "danger", true), - mode("BATTERY", "danger", false), - mode("LINK", "danger", true), - mode("SOCKET", "danger", true), - div(style := "float: right")(mode("CRITICAL", "danger", true))) - - val map = iframe( - "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: 100%; color: #ffffff; background-color: #c2c2c2; text-align: center;") - - val altimeter = new Altimeter( - Var(0.0) - ) - val horizon = new Horizon(socket.message.collect((0.0, 0.0)) { - case att: Attitude => (att.pitch, att.roll) - }) - val compass = new Compass(socket.message.collect(0.0) { - case att: Attitude => att.yaw - }) - val motor0 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { - case s: ServoOutputRaw => 100 * (s.servo1Raw - 1000) / 1000 - }) - val motor1 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { - case s: ServoOutputRaw => 100 * (s.servo2Raw - 1000) / 1000 - }) - val motor2 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { - case s: ServoOutputRaw => 100 * (s.servo3Raw - 1000) / 1000 - }) - val motor3 = new Generic(0, 50, 100, "%", socket.message.collect(0.0) { - case s: ServoOutputRaw => 100 * (s.servo4Raw - 1000) / 1000 - }) - val powerDistribution = new Distribution( - socket.message.collect((0.0, 0.0, 0.0, 0.0)) { - case s: ServoOutputRaw => - ( - 1.0 * (s.servo1Raw - 1000) / 1000, - 1.0 * (s.servo2Raw - 1000) / 1000, - 1.0 * (s.servo3Raw - 1000) / 1000, - 1.0 * (s.servo4Raw - 1000) / 1000 - ) - } - ) - val batteryLevel = new Bar( - Var(0.0) - ) - - val top = header( - div("Flight Control Panel"), - div((new Clock).element), - div("UAV " + socket.remoteSystemId) - ) - - val left = div( - panel( - table(`class` := "table-instrument")( - thead("Communication"), - tbody( - tr( - td("Uplink RSSI"), - td("89"), - td("Socket"), - td("5ms") - ), - tr( - td("Something else"), - td("unknown"), - td("Heartbeat"), - td(i(`class` := "fa fa-heart heartbeat")) - ) - ) - ), - table(`class` := "table-instrument")( - thead("Packets"), - tbody( - tr( - td("OK"), - Rx { td(socket.stats.packets()) }, - td("CRC"), - Rx { td(socket.stats.crcErrors()) }, - td("OFLW"), - Rx { td(socket.stats.overflows()) }, - td("BID"), - Rx { td(socket.stats.wrongIds()) } - ), - tr( - td("Ratio"), - Rx { - import socket.stats._ - val sum = packets() + crcErrors() + overflows() + wrongIds() - td(1.0 * packets() / sum formatted "%.2f") - }, - td(), - td(), - td(), - td(), - td(), - td() - ) - ) - ) - ), - panel( - table(`class` := "table-instrument")( - tbody( - tr( - td(compass.element), - td(horizon.element), - td(altimeter.element), - td(altimeter.element) - ) - ) - ) - ), - panel( - div(style := "width: 50%; display: inline-block;")( - table(`class` := "table-instrument")( - tbody( - tr( - td(motor1.element), - td(), - td(motor0.element) - ), - tr( - td(), - td(powerDistribution.element), - td() - ), - tr( - td(motor2.element), - td(), - td(motor3.element) - ) - ) - ) - ), - div(style := "width: 50%; display: inline-block;")( - table(`class` := "table-instrument")( - thead("Power"), - tbody( - tr( - td("VHIGH"), - td("12.6V"), - td("VLOW"), - td("9V") - ), - tr( - td("Voltage"), - td("11.2V"), - td("Remaining"), - td("80%") - ), - tr( - td("Flight"), - td("05:00"), - td("Endurance"), - td("12:00") - ) - ) - ), - table(`class` := "table-instrument")( - thead("Navigation"), - tbody( - tr( - td("Satellites"), - td("5"), - td("Precision"), - td("10cm") - ), - tr( - td("LON"), - td(""), - td("LAT"), - td("") - ), - tr( - td("GSpeed"), - td("3 m/s"), - td(), - td() - ), - tr( - td("Travelled"), - td("5000m"), - td("Home"), - td("1200m") - ) - ) - ) - ) - ) - ) - - - - val hud = { - def overlay(name: String, z: Int, thinnerThanWide: Boolean = false) = { - val direction = if (thinnerThanWide) "row" else "column" - div( - style:= - "position: absolute; left: 0; right: 0; top: 0; bottom: 0;" + - s"display: flex; align-content: center; align-items: stretch; flex-direction: $direction;"+ - s"z-index: $z;" - )( - `object`(`type`:="image/svg+xml", "data".attr:=env.asset("images/hud/" + name + ".svg"), style := "flex: 1 1 100%;") - ) - - } - - Seq( - overlay("horizon", 0), - overlay("roll", 1) - ) - } - - - - val element = div(`class` := "d-container d-column", style:="width: 100%; height: 100%;")( - div(`class` := "d-above")( - top - ), - div(`class` := "d-above d-container d-row")( - panel(modes), - panel(infos) - ), - div(`class` := "d-container d-row")( - div(`class` := "d-container d-left")( - left - ), - div(`class` := "d-main", style:="position: relative;")( - (new Hud(horizon.value)).element - ) - ) - ).render - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala deleted file mode 100644 index 9919d33..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Altimeter.scala +++ /dev/null @@ -1,19 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import mavigator.util.Environment - -import org.scalajs.dom.html -import rx._ - -class Altimeter(val value: Rx[Double])(implicit env: Environment) extends SvgInstrument[Double] { - import SvgInstrument._ - - lazy val element = svgObject("altimeter") - lazy val hand = part("hand") - lazy val moveable = Seq(hand) - - // 1m === 36deg = 2Pi / 10 ~= 0.62832 - protected def update(altitude: Double) = { - rotate(hand, (altitude * 0.62832).toInt) - } -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala deleted file mode 100644 index 0950eab..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Bar.scala +++ /dev/null @@ -1,18 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import mavigator.util.Environment -import org.scalajs.dom.html -import rx._ - -class Bar(val value: Rx[Double])(implicit env: Environment) extends SvgInstrument[Double] { - import SvgInstrument._ - - lazy val element = svgObject("bar") - lazy val level = part("level") - lazy val moveable = Seq(level) - - protected def update(value: Double) = { - translate(level, 0, (97 * (1 - value / 100)).toInt) - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala deleted file mode 100644 index 160fd2d..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Clock.scala +++ /dev/null @@ -1,23 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import org.scalajs.dom -import rx._ -import scala.scalajs.js.Date -import scalatags.JsDom.all._ - -class Clock extends Instrument[Date] { - - def format(date: Date) = date.toLocaleTimeString() - - val value = Var(new Date) - - val element = span(format(value())).render - - protected def update(value: Date) = { - element.innerHTML = format(value) - } - - dom.setInterval(() => {value() = new Date}, 1000) - ready() - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala deleted file mode 100644 index 117c7a3..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Compass.scala +++ /dev/null @@ -1,17 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import org.scalajs.dom.html -import rx._ -import mavigator.util.Environment - -class Compass(val value: Rx[Double])(implicit env: Environment) extends SvgInstrument[Double] { - import SvgInstrument._ - - lazy val element = svgObject("compass") - lazy val plate = part("heading") - lazy val moveable = Seq(plate) - - protected def update(heading: Double) = { - rotate(plate, heading) - } -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala deleted file mode 100644 index aadafe0..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Distribution.scala +++ /dev/null @@ -1,25 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import org.scalajs.dom.html -import rx._ -import mavigator.util.Environment - -class Distribution(val value: Rx[(Double, Double, Double, Double)])(implicit env: Environment) extends SvgInstrument[(Double, Double, Double, Double)] { - import SvgInstrument._ - - lazy val element = svgObject("distribution") - lazy val position = part("position") - lazy val moveable = Seq(position) - - private final val Radius = 50 //px - - protected def update(value: (Double, Double, Double, Double)) = { - val sum = value._1 + value._2 + value._3 + value._4 - val i = (value._1 - value._3) / sum - val j = (value._2 - value._4) / sum - val x = math.sqrt(2) / 2 * (i - j) - val y = math.sqrt(2) / 2 * (-i - j) - translate(position, (x * Radius).toInt, (y * Radius).toInt) - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala deleted file mode 100644 index 14f7de3..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Generic.scala +++ /dev/null @@ -1,40 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import org.scalajs.dom -import org.scalajs.dom.html -import rx._ -import mavigator.util.Environment - -class Generic( - min: Double, - med: Double, - max: Double, - unit: String, - val value: Rx[Double]) - (implicit env: Environment) - extends SvgInstrument[Double] { - - import SvgInstrument._ - - lazy val element = svgObject("generic") - lazy val handElement = part("hand") - lazy val unitElement = element.contentDocument.getElementById("unit") - lazy val valueElement = element.contentDocument.getElementById("value") - lazy val minElement = element.contentDocument.getElementById("min") - lazy val medElement = element.contentDocument.getElementById("med") - lazy val maxElement = element.contentDocument.getElementById("max") - lazy val 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 - super.load(e) - } - - protected def update(value: Double) = { - rotate(handElement, value / (max - min) * math.Pi * 3 / 2) - valueElement.textContent = value.toString - } -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala deleted file mode 100644 index 249f892..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Horizon.scala +++ /dev/null @@ -1,19 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import org.scalajs.dom.html -import rx._ -import mavigator.util.Environment - -class Horizon(val value: Rx[(Double, Double)])(implicit env: Environment) extends SvgInstrument[(Double, Double)] { - import SvgInstrument._ - - lazy val element = svgObject("horizon") - lazy val pitch = part("pitch") - lazy val roll = part("roll") - lazy val moveable = Seq(pitch, roll) - - protected def update(pitchRoll: (Double, Double)) = { - translate(pitch, 0, (pitchRoll._1 * 180 / math.Pi).toInt) // 1deg === 1px - rotate(roll, pitchRoll._2) - } -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala deleted file mode 100644 index 61f240c..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Instrument.scala +++ /dev/null @@ -1,25 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import rx._ -import org.scalajs.dom.html - -/** Common trait to all flight instruments. */ -trait Instrument[A] { - - /** Current value that is displayed in the instrument. */ - val value: Rx[A] - - /** HTML element that contains the rendered instrument */ - val element: html.Element - - /** Performs the actual UI update of this instrument. */ - protected def update(newValue: A): Unit - - /** Call when instrument has finished setting up its UI. */ - protected def ready() = { - Obs(value, skipInitial = true) { - update(value()) - } - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala deleted file mode 100644 index c6f7d45..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/Led.scala +++ /dev/null @@ -1,17 +0,0 @@ -package mavigator.dashboard.ui.instruments - -import rx._ -import scalatags.JsDom.all._ -import mavigator.util.Environment - -class Led(val value: Rx[String])(implicit env: Environment) extends SvgInstrument[String] { - - lazy val element = `object`(`type` := "image/svg+xml", "data".attr := env.asset("images/leds/led.svg"), width := 100.pct)( - "Error loading led.").render - protected def moveable = Seq() - - protected def update(color: String) = { - part("light").setAttribute("fill", color) - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala b/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala deleted file mode 100644 index 28c8d65..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/dashboard/ui/instruments/SvgInstrument.scala +++ /dev/null @@ -1,54 +0,0 @@ -package mavigator.dashboard.ui.instruments - - -import org.scalajs.dom -import org.scalajs.dom.html - -import scalatags.JsDom.all._ - -import mavigator.util.Environment - -/** An instrument backed by an SVG image. */ -trait SvgInstrument[A] extends Instrument[A] { - - /** SVG object element that contains the rendered instrument */ - val element: html.Object - - /** Retrieves an element of the underlying SVG document by ID. */ - protected def part(id: String) = element.contentDocument.getElementById(id).asInstanceOf[html.Element] - - /** Movable parts of the instrument */ - protected def moveable: Seq[html.Element] - - /** Called when element has been loaded. */ - protected def load(event: dom.Event): Unit = { - for (part <- moveable) { - part.style.transition = "transform 50ms ease-out" - } - ready() - } - - element.addEventListener("load", (e: dom.Event) => load(e)) -} - -/** Contains helpers for SVG instruments. */ -object SvgInstrument { - - /** Retrieves an SVG object element by its instrument's name. */ - def svgObject(name: String)(implicit app: Environment): html.Object = { - val path = app.asset("images/instruments/" + name + ".svg") - `object`(`type` := "image/svg+xml", "data".attr := path, width := 100.pct)( - "Error loading instrument " + name).render - } - - /** Applies translation styling to an element. */ - def translate(elem: html.Element, x: Int, y: Int): Unit = { - elem.style.transform = "translate(" + x + "px, " + y + "px)"; - } - - /** Applies rotation styling to an element. */ - def rotate(elem: html.Element, rad: Double): Unit = { - elem.style.transform = "rotateZ(" + rad + "rad)"; - } - -} diff --git a/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala b/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala index 870d3be..7e59ed8 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/index/ActiveVehicle.scala @@ -13,7 +13,8 @@ object ActiveVehicle { id, vehicleType(hb.`type`), autopilot(hb.autopilot), - state(hb.systemStatus)) + state(hb.systemStatus) + ) def vehicleType(tpe: Int) = tpe match { case MavType.MavTypeGeneric => "Generic" diff --git a/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala b/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala index 7724f89..452d517 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/index/Main.scala @@ -5,7 +5,6 @@ import scala.scalajs.js.annotation.JSExport import org.scalajs.dom.html -import mavigator.dashboard.ui.Layout import mavigator.util.Environment import mavigator.util.Application @@ -29,16 +28,17 @@ object Main extends Application { val parser = new Parser( packet => { val m: Message = Message.unpack(packet.messageId, packet.payload) - println(m) m match { case hb: Heartbeat => - active() += ActiveVehicle.fromHeartbeat(packet.systemId, hb) + active() = active.now + ActiveVehicle.fromHeartbeat(packet.systemId, hb) case _ => () } } ) override def main(args: Map[String, String])(implicit env: Environment): Unit = { + import rx.Ctx.Owner.Unsafe._ + val root = env.root val connection = new dom.WebSocket(args("socketUrl")) @@ -64,34 +64,45 @@ object Main extends Application { } - root.appendChild(div( - table(`class` := "table table-hover")( - thead( - tr( - th("System ID"), - th("Type"), - th("Autopilot"), - th("State"), - th("") - ) - ), - Rx { - tbody( - for (vehicle <- active().toSeq) yield { - tr( - td(vehicle.systemId), - td(vehicle.vehicleType), - td(vehicle.autopilot), - td(vehicle.state), - td(a(href := "/dashboard/" + vehicle.systemId, `class` := "btn btn-default")("Pilot")) - ) - } + val elem = + div(`class` := "container")( + div(`class` := "col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3")( + div(`class` := "panel panel-default")( + div(`class` := "panel-heading")( + h3(`class` := "panel-title")("Available vehicles") + ), + div(`class` := "panel-body")( + table(`class` := "table table-hover")( + thead( + tr( + th("System ID"), + th("Type"), + th("Autopilot"), + th("State"), + th("") + ) + ), + Rx { + tbody( + for (vehicle <- active().toSeq) yield { + tr( + td(vehicle.systemId), + td(vehicle.vehicleType), + td(vehicle.autopilot), + td(vehicle.state), + td(a(href := "/cockpit/" + vehicle.systemId, `class` := "btn btn-default")("Pilot")) + ) + } + ) + } + ), + p(i(`class`:="fa fa-spinner fa-spin")(), " Listening for heartbeats...") + ) ) - } - ), - i(`class`:="fa fa-spinner fa-spin")(), - " Listening for heartbeats..." - ).render) + ) + ).render + + root.appendChild(elem) } } diff --git a/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala b/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala index 16f3d5d..2671f2f 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/index/Util.scala @@ -13,7 +13,7 @@ import scala.util.Failure import scalatags.JsDom.all._ object Util { - + /** * Copied from https://github.com/lihaoyi/workbench-example-app/blob/todomvc/src/main/scala/example/Framework.scala * @@ -22,13 +22,13 @@ object Util { * the Obs onto the element itself so we have a reference to kill it when * the element leaves the DOM (e.g. it gets deleted). */ - implicit def rxMod[T <: html.Element](r: Rx[HtmlTag]): Frag = { + implicit def rxMod(r: Rx[HtmlTag])(implicit owner: Ctx.Owner): Frag = { def rSafe = r.toTry match { case Success(v) => v.render case Failure(e) => span(e.toString, backgroundColor := "red").render } var last = rSafe - Obs(r, skipInitial = true) { + r.triggerLater{ val newLast = rSafe last.parentElement.replaceChild(newLast, last) last = newLast diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala b/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala index 7cb6f27..68a39b1 100644 --- a/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala +++ b/mavigator-cockpit/src/main/scala/mavigator/util/Application.scala @@ -4,6 +4,7 @@ package util import scala.scalajs.js.annotation.JSExport import scala.scalajs.js +import org.scalajs.dom.console import org.scalajs.dom.html trait Application { @@ -13,13 +14,18 @@ trait Application { @JSExport final def start(settings: js.Dynamic): Unit = { - val env = new StaticEnvironment( + console.info("Initializing environment,,,") + val env = new Environment( root = settings.root.asInstanceOf[html.Element], - assetsBase = settings.assetsBase.asInstanceOf[String] + styleRoot = settings.styleRoot.asInstanceOf[html.Element], + baseUrl = settings.baseUrl.asInstanceOf[String] ) - val args = settings.args.asInstanceOf[js.Dictionary[Any]].mapValues(_.toString).toMap + console.info("Reading arguments...") + val args: Map[String, String] = + settings.args.asInstanceOf[js.Dictionary[Any]].mapValues(_.toString).toMap + console.info("Entering main...") main(args)(env) } diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/Environment.scala b/mavigator-cockpit/src/main/scala/mavigator/util/Environment.scala new file mode 100644 index 0000000..b1076fc --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/util/Environment.scala @@ -0,0 +1,15 @@ +package mavigator +package util + +import org.scalajs.dom +import org.scalajs.dom.html + +/** Represents an application's environment + * @param root The application's root element. + * @param styleRoot An html 'style' tag to which app-specific styles are appended. + * @param baseUrl Base URL. */ +case class Environment( + root: html.Element, + styleRoot: html.Element, + baseUrl: String +) diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/Page.scala b/mavigator-cockpit/src/main/scala/mavigator/util/Page.scala new file mode 100644 index 0000000..f8f0dee --- /dev/null +++ b/mavigator-cockpit/src/main/scala/mavigator/util/Page.scala @@ -0,0 +1,38 @@ +package mavigator +package util + +import org.scalajs.dom +import org.scalajs.dom.html + +/** Contents of a single-page app, consisting of styles and a main element. */ +trait Page { + + private var isAttached = false + private def attached[A](action: => A) = if (!isAttached) { + sys.error("Page has not been attached to an environment yet.") + } else { + action + } + + private var baseUrl: String = null + + def asset(path: String): String = attached { baseUrl + "/" + path } + + def styles: Seq[String] + + def elements: Seq[html.Element] + + /** Attach this page to a website. */ + def attach(env: Environment): Unit = { + baseUrl = env.baseUrl + isAttached = true + + val styleText = dom.document.createTextNode(styles.reduce(_ + _)) + env.styleRoot.appendChild(styleText) + + for (elem <- elements) { + env.root.appendChild(elem) + } + } + +} diff --git a/mavigator-cockpit/src/main/scala/mavigator/util/environment.scala b/mavigator-cockpit/src/main/scala/mavigator/util/environment.scala deleted file mode 100644 index 3d58c5a..0000000 --- a/mavigator-cockpit/src/main/scala/mavigator/util/environment.scala +++ /dev/null @@ -1,24 +0,0 @@ -package mavigator -package util - -import org.scalajs.dom.html - -/** Represents an application's environment */ -trait Environment { - - /** The application's root element. */ - def root: html.Element - - /** Retrieve an asset's URL based on its file location. */ - def asset(file: String): String - -} - -class StaticEnvironment( - override val root: html.Element, - assetsBase: String -) extends Environment { - - override def asset(file: String): String = assetsBase + "/" + file - -} |