From 4238db415450ddddfe4c3ebd80fb7f641dd671ec Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 26 Dec 2014 14:58:11 +0100 Subject: refactor ui --- vfd-backend/app/controllers/Application.scala | 8 +- vfd-backend/app/plugins/UavClientConnection.scala | 4 + vfd-backend/app/plugins/UavPlugin.scala | 12 +- vfd-backend/app/views/main.scala.html | 2 +- vfd-backend/app/views/uav.scala.html | 15 +- vfd-backend/conf/application.conf | 6 +- vfd-backend/conf/routes | 8 +- vfd-backend/public/images/instruments/balance.svg | 169 +++ vfd-backend/public/images/instruments/bar.svg | 63 +- vfd-backend/public/images/instruments/basic.svg | 314 ----- vfd-backend/public/images/instruments/generic.svg | 1217 ++++---------------- vfd-backend/public/images/instruments/generic2.svg | 1063 +++++++++++++++++ vfd-backend/public/stylesheets/main.css | 43 +- .../src/main/scala/vfd/frontend/Environment.scala | 14 + .../src/main/scala/vfd/frontend/Launcher.scala | 4 +- .../src/main/scala/vfd/frontend/Main.scala | 69 +- .../main/scala/vfd/frontend/MavlinkSocket.scala | 23 +- .../main/scala/vfd/frontend/ui/Components.scala | 105 -- .../src/main/scala/vfd/frontend/ui/Layout.scala | 57 + .../vfd/frontend/ui/components/SvgInstrument.scala | 55 + .../vfd/frontend/ui/components/instruments.scala | 124 ++ .../vfd/frontend/ui/panels/Communication.scala | 128 +- .../scala/vfd/frontend/ui/panels/Primary.scala | 49 +- .../main/scala/vfd/frontend/util/Environment.scala | 14 - .../main/scala/vfd/frontend/util/Framework.scala | 70 -- .../src/main/scala/vfd/frontend/util/package.scala | 15 - 26 files changed, 1915 insertions(+), 1736 deletions(-) create mode 100644 vfd-backend/public/images/instruments/balance.svg delete mode 100644 vfd-backend/public/images/instruments/basic.svg create mode 100644 vfd-backend/public/images/instruments/generic2.svg create mode 100644 vfd-frontend/src/main/scala/vfd/frontend/Environment.scala delete mode 100644 vfd-frontend/src/main/scala/vfd/frontend/ui/Components.scala create mode 100644 vfd-frontend/src/main/scala/vfd/frontend/ui/Layout.scala create mode 100644 vfd-frontend/src/main/scala/vfd/frontend/ui/components/SvgInstrument.scala create mode 100644 vfd-frontend/src/main/scala/vfd/frontend/ui/components/instruments.scala delete mode 100644 vfd-frontend/src/main/scala/vfd/frontend/util/Environment.scala delete mode 100644 vfd-frontend/src/main/scala/vfd/frontend/util/Framework.scala delete mode 100644 vfd-frontend/src/main/scala/vfd/frontend/util/package.scala diff --git a/vfd-backend/app/controllers/Application.scala b/vfd-backend/app/controllers/Application.scala index 09e0e0d..827950e 100644 --- a/vfd-backend/app/controllers/Application.scala +++ b/vfd-backend/app/controllers/Application.scala @@ -11,18 +11,18 @@ import plugins.UavPlugin object Application extends Controller { - private def uav = current.plugin[UavPlugin].getOrElse(throw new RuntimeException("UAV plugin is not available")) + private def plugin = current.plugin[UavPlugin].getOrElse(throw new RuntimeException("UAV plugin is not available")) def index = Action { implicit request => Redirect(routes.Application.uav(0)) } - def uav(sysId: Int) = Action { implicit request => - Ok(views.html.uav(routes.Application.mavlink.webSocketURL(), sysId)) + def uav(remoteSystemId: Int) = Action { implicit request => + Ok(views.html.uav(routes.Application.mavlink.webSocketURL(), remoteSystemId.toByte, plugin.systemId, 0.toByte)) } def mavlink = WebSocket.acceptWithActor[Array[Byte], Array[Byte]] { implicit request => - out => uav.register(out) + out => plugin.register(out) } } \ No newline at end of file diff --git a/vfd-backend/app/plugins/UavClientConnection.scala b/vfd-backend/app/plugins/UavClientConnection.scala index b479aa5..76975e1 100644 --- a/vfd-backend/app/plugins/UavClientConnection.scala +++ b/vfd-backend/app/plugins/UavClientConnection.scala @@ -5,6 +5,7 @@ import akka.actor.ActorLogging import akka.actor.ActorRef import akka.actor.actorRef2Scala import vfd.uav.Connection +import akka.util.ByteString /** * Interfaces traffic from a websocket with a connection to a UAV. @@ -23,6 +24,9 @@ class UavClientConnection(websocket: ActorRef, uav: ActorRef) extends Actor with case Connection.Closed(msg) => log.warning(msg) context stop self + + case fromClient: Array[Byte] => + uav ! Connection.Send(ByteString(fromClient)) } diff --git a/vfd-backend/app/plugins/UavPlugin.scala b/vfd-backend/app/plugins/UavPlugin.scala index 43e015c..9b45627 100644 --- a/vfd-backend/app/plugins/UavPlugin.scala +++ b/vfd-backend/app/plugins/UavPlugin.scala @@ -12,29 +12,31 @@ class UavPlugin(app: Application) extends Plugin { private lazy val config = app.configuration.getConfig("uav") - lazy val systemId = config.flatMap(_.getInt("system_id")).getOrElse(1) + lazy val systemId = config.flatMap(_.getInt("system_id")).getOrElse(1).toByte private lazy val connection = { val conn = config.flatMap(_.getConfig("connection")) val tpe = conn.flatMap(_.getString("type")).getOrElse("mock") val heartbeat = conn.flatMap(_.getInt("heartbeat")).getOrElse(2000) - val id = conn.flatMap(_.getInt("component_id")).getOrElse(99).toByte + val compId = conn.flatMap(_.getInt("component_id")).getOrElse(1).toByte val props = tpe match { case "mock" => - MockConnection.apply + val remote = config.flatMap(_.getInt("mock.remote_system_id")).getOrElse(42).toByte + MockConnection(systemId, compId, remote) case "serial" => val serial = config.flatMap(_.getConfig("serial")) SerialConnection( - id, + systemId, + compId, heartbeat, serial.flatMap(_.getString("port")).getOrElse("/dev/ttyUSB0"), serial.flatMap(_.getInt("baud")).getOrElse(115200), serial.flatMap(_.getBoolean("two_stop_bits")).getOrElse(false), serial.flatMap(_.getInt("parity")).getOrElse(0)) - case unknown => throw new RuntimeException("Unsupported connection type '" + unknown + "'") + case unknown => throw new IllegalArgumentException("Unsupported connection type '" + unknown + "'") } Akka.system(app).actorOf(props, name = "uav-connection") diff --git a/vfd-backend/app/views/main.scala.html b/vfd-backend/app/views/main.scala.html index 8537db6..1be335b 100644 --- a/vfd-backend/app/views/main.scala.html +++ b/vfd-backend/app/views/main.scala.html @@ -28,7 +28,7 @@ - Virtual Flight Deck + Flight Control Panel diff --git a/vfd-backend/app/views/uav.scala.html b/vfd-backend/app/views/uav.scala.html index 410e3c0..0ce7927 100644 --- a/vfd-backend/app/views/uav.scala.html +++ b/vfd-backend/app/views/uav.scala.html @@ -1,4 +1,4 @@ -@(socket: String, remoteSystemId: Int) +@(socket: String, remoteSystemId: Byte, systemId: Byte, componentId: Byte) @main("Main", "Remote System " + remoteSystemId){ @@ -10,8 +10,15 @@

-
- Loading... +
+
+ +
diff --git a/vfd-backend/conf/application.conf b/vfd-backend/conf/application.conf index e069b23..c2026a3 100644 --- a/vfd-backend/conf/application.conf +++ b/vfd-backend/conf/application.conf @@ -68,12 +68,12 @@ logger.application=DEBUG uav.system_id=1 # Type of connection to use -# 'mock' for a sample connection -uav.connection.type=serial +# 'mock' or 'serial' +uav.connection.type=mock # Mavlink component id used by this connection (not the web frontend), # in case it needs to inject messages -uav.connection.component_id=99 +uav.connection.component_id=1 # Delay in milliseconds between heartbeat messages injected by # the connection diff --git a/vfd-backend/conf/routes b/vfd-backend/conf/routes index eea8d8e..3d38889 100644 --- a/vfd-backend/conf/routes +++ b/vfd-backend/conf/routes @@ -3,9 +3,9 @@ # ~~~~ # Home page -GET / controllers.Application.index -GET /uav/:sysId controllers.Application.uav(sysId: Int) -GET /mavlink controllers.Application.mavlink +GET / controllers.Application.index +GET /uav/:remoteSystemId controllers.Application.uav(remoteSystemId: Int) +GET /mavlink controllers.Application.mavlink # Map static resources from the /public folder to the /assets URL path -GET /assets/*file controllers.Assets.at(path="/public", file) +GET /assets/*file controllers.Assets.at(path="/public", file) diff --git a/vfd-backend/public/images/instruments/balance.svg b/vfd-backend/public/images/instruments/balance.svg new file mode 100644 index 0000000..57511e9 --- /dev/null +++ b/vfd-backend/public/images/instruments/balance.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vfd-backend/public/images/instruments/bar.svg b/vfd-backend/public/images/instruments/bar.svg index 03d311a..50ecebf 100644 --- a/vfd-backend/public/images/instruments/bar.svg +++ b/vfd-backend/public/images/instruments/bar.svg @@ -9,33 +9,26 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="100" + width="32" height="100" id="svg4387" version="1.1" inkscape:version="0.48.5 r10040" - viewBox="-50 -50 100 100" - sodipodi:docname="voltage.svg"> + viewBox="0 0 32 100" + sodipodi:docname="bar.svg"> - + id="clipPath2997"> + id="rect2999" + style="fill:#00ffff;fill-opacity:1;stroke:none" /> - - 0 - % - + x="1.5" + y="1.5" + clip-path="url(#clipPath2997)" /> + id="fixed" + transform="translate(5,50)"> - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 100 - 0 - 0 - % - 50 - - - - - diff --git a/vfd-backend/public/images/instruments/generic.svg b/vfd-backend/public/images/instruments/generic.svg index 02e4caa..ac04b60 100644 --- a/vfd-backend/public/images/instruments/generic.svg +++ b/vfd-backend/public/images/instruments/generic.svg @@ -9,43 +9,47 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="110" - height="110" - id="svg2987" + width="100" + height="100" + id="svg4387" version="1.1" inkscape:version="0.48.5 r10040" - sodipodi:docname="generic.svg" - viewBox="-55 -55 110 110"> + viewBox="-50 -50 100 100" + sodipodi:docname="basic.svg"> + id="defs4389"> + + + inkscape:snap-grids="false"> + id="metadata4392"> @@ -67,997 +71,244 @@ + id="lower"> + inkscape:transform-center-y="44.418149" + inkscape:transform-center-x="11.044759" + inkscape:connector-curvature="0" + id="path5010" + d="m -0.52403983,48.510133 0.0154511,-3.487667 C -7.2448512,44.942356 -13.911142,43.340039 -19.989534,40.326163 l -1.591395,3.126201 c 6.563792,3.254436 13.7823459,4.974145 21.05688917,5.057769 z" + style="fill:#00d400;fill-opacity:1;stroke:none" /> + inkscape:transform-center-y="-24.174304" + inkscape:transform-center-x="39.074649" + inkscape:connector-curvature="0" + id="path5002" + d="m -45.973942,-15.488849 3.312195,1.092444 c 2.157808,-6.381812 5.741699,-12.226687 10.486391,-17.076243 l -2.481425,-2.479556 c -5.123476,5.236862 -8.989672,11.570695 -11.317161,18.463355 z" + style="fill:#00d400;fill-opacity:1;stroke:none" /> + inkscape:transform-center-x="45.733006" + inkscape:transform-center-y="-3.7233588" /> + id="path5006" + d="m -39.553546,28.089583 2.830664,-2.037499 c -3.894666,-5.496838 -6.516711,-11.831799 -7.651224,-18.5208328 l -3.464548,0.5500689 c 1.225206,7.2231289 4.076892,14.0738809 8.285108,20.0082629 z" + style="fill:#00d400;fill-opacity:1;stroke:none" /> + style="fill:#00d400;fill-opacity:1;stroke:none" + d="m -22.490063,42.984936 1.597135,-3.10052 c -5.965685,-3.129577 -11.177957,-7.583685 -15.225573,-13.0286 l -2.837209,2.062987 c 4.370899,5.879623 10.021946,10.68905 16.465647,14.066133 z" + id="path5008" + inkscape:connector-curvature="0" + inkscape:transform-center-x="29.924319" + inkscape:transform-center-y="34.920377" /> + + + + + + - - - - - + inkscape:connector-curvature="0" /> + + + + inkscape:transform-center-x="21.110558" + transform="matrix(0.89100652,0.4539905,-0.4539905,0.89100652,0,0)" + style="fill:#1a1a1a;fill-opacity:1;stroke:none" + id="rect4961" + width="1" + height="5" + x="-0.5" + y="44" + inkscape:transform-center-y="41.431803" /> + + inkscape:transform-center-x="45.927507" + transform="matrix(0.15643446,0.98768834,-0.98768834,0.15643446,0,0)" + style="fill:#1a1a1a;fill-opacity:1;stroke:none" + id="rect4965" + width="1" + height="5" + x="-0.49999964" + y="44" + inkscape:transform-center-y="7.2742025" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + y="44" + x="-0.49999928" + height="5" + width="1" + id="rect4967" + style="fill:#1a1a1a;fill-opacity:1;stroke:none" + transform="matrix(-0.309017,0.95105651,-0.95105651,-0.309017,0,0)" + inkscape:transform-center-x="44.224127" /> + inkscape:transform-center-x="14.36929" + transform="matrix(-0.95105652,0.30901699,-0.30901699,-0.95105652,0,0)" + style="fill:#1a1a1a;fill-opacity:1;stroke:none" + id="rect4969" + width="1" + height="5" + x="-0.49999872" + y="44" + inkscape:transform-center-y="-44.224128" /> + - 4 - 5 - 6 + inkscape:transform-center-x="-27.332015" + transform="matrix(-0.80901699,-0.58778526,0.58778526,-0.80901699,0,0)" + style="fill:#1a1a1a;fill-opacity:1;stroke:none" + id="rect4973" + width="1" + height="5" + x="-0.49999899" + y="44" + inkscape:transform-center-y="-37.619291" /> + + + 7 + id="max" + x="40.52644" + y="3.5468256">100 8 + id="text5102" + y="39.272289" + x="0.0016270902" + style="font-size:10px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#4d4d4d;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" + xml:space="preserve">0 0 - 1 2 + id="text5106" + y="38.902164" + x="25.957726" + style="font-size:10px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#4d4d4d;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" + xml:space="preserve">% 3 - + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;text-anchor:start;font-family:Sans;-inkscape-font-specification:Sans">50 + + + d="m 48,50 c 0,13.34999 2,42 2,42 0,0 2,-28.65001 2,-42 0,-3 -4,-3 -4,0 z" + id="path5012" + inkscape:connector-curvature="0" + transform="translate(-50,-50)" + sodipodi:nodetypes="scss" /> diff --git a/vfd-backend/public/images/instruments/generic2.svg b/vfd-backend/public/images/instruments/generic2.svg new file mode 100644 index 0000000..02e4caa --- /dev/null +++ b/vfd-backend/public/images/instruments/generic2.svg @@ -0,0 +1,1063 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + 5 + 6 + 7 + 8 + 0 + 1 + 2 + 3 + + + + diff --git a/vfd-backend/public/stylesheets/main.css b/vfd-backend/public/stylesheets/main.css index 295bc03..e92cd24 100644 --- a/vfd-backend/public/stylesheets/main.css +++ b/vfd-backend/public/stylesheets/main.css @@ -8,59 +8,48 @@ html { } body { - min-height: 100%; + height: 100%; } body { background-color: #e6e6e6; } -nav.side { - position: absolute; - left: 0; - width: 15%; - height: 100%; - background: #FFFFCC; - border: 1px solid #969696; - box-sizing: border-box; - -moz-box-sizing:border-box; +.loader { + width: 100%; + font-size: 50px; + text-align: center; } -.control-table { - border-collapse: collapse; - background: #e5e5e5; - border: 1px solid #969696; - margin-right: 10px; +.table-instrument { + table-layout: fixed; + width: 100%; } -.control-table td { - padding: 10px; +.table-instrument td { + width: 100%; } -.heartbeat{ +.heartbeat { color: rgba(165, 25, 25, 1); - animation: heartbeat 1s linear; - opacity: 0; + animation: heartbeat 2s linear infinite; } @keyframes heartbeat { 0% { transform: scale(1); - opacity: 1; } - 25% { + 7% { transform: scale(1.3); } - 50% { + 14% { transform: scale(1); } - 75% { + 21% { transform: scale(1.3); - opacity: 1; } - 100% { + 28% { transform: scale(1); - opacity: 0; } } diff --git a/vfd-frontend/src/main/scala/vfd/frontend/Environment.scala b/vfd-frontend/src/main/scala/vfd/frontend/Environment.scala new file mode 100644 index 0000000..53571f8 --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/Environment.scala @@ -0,0 +1,14 @@ +package vfd.frontend + +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-frontend/src/main/scala/vfd/frontend/Launcher.scala b/vfd-frontend/src/main/scala/vfd/frontend/Launcher.scala index e1715d7..cf7ce94 100644 --- a/vfd-frontend/src/main/scala/vfd/frontend/Launcher.scala +++ b/vfd-frontend/src/main/scala/vfd/frontend/Launcher.scala @@ -4,9 +4,7 @@ import scala.scalajs.js.annotation.JSExport import org.scalajs.dom -import vfd.frontend.util.Environment - -@JSExport +@JSExport("Launcher") class Launcher(rootId: String, assetsBase: String) { lazy val env = new Environment { diff --git a/vfd-frontend/src/main/scala/vfd/frontend/Main.scala b/vfd-frontend/src/main/scala/vfd/frontend/Main.scala index 7b7990e..f4ef84d 100644 --- a/vfd-frontend/src/main/scala/vfd/frontend/Main.scala +++ b/vfd-frontend/src/main/scala/vfd/frontend/Main.scala @@ -1,74 +1,13 @@ package vfd.frontend -import org.mavlink.messages.Message -import rx.ops.RxOps -import scalatags.JsDom.all.ExtendedString -import scalatags.JsDom.all.button -import scalatags.JsDom.all.`class` -import scalatags.JsDom.all.div -import scalatags.JsDom.all.height -import scalatags.JsDom.all.iframe -import scalatags.JsDom.all.img -import scalatags.JsDom.all.src -import scalatags.JsDom.all.stringAttr -import scalatags.JsDom.all.stringFrag -import scalatags.JsDom.all.stringStyle -import scalatags.JsDom.all.width -import vfd.frontend.ui.panels -import vfd.frontend.util.Environment -import rx.core.Obs +import vfd.frontend.ui.Layout object Main { def main(args: Map[String, String])(implicit env: Environment) = { - val socketUrl = args("socketurl") - val remoteSystemId = args("remotesystemid").toInt + val socket = new MavlinkSocket(args("socketurl"), args("remotesystemid").toInt) + val layout = new Layout(socket) - val socket = new MavlinkSocket(socketUrl, remoteSystemId) - - val message = socket.packet.map { p => - Message.unpack(socket.packet().messageId, socket.packet().payload) - } - - Obs(message) { - println(message().toString()) - } - - val element = div(`class` := "container-fluid")( - div(`class` := "row")( - div(`class` := "col-xs-12")( - div(`class` := "panel panel-default")( - div(`class` := "panel-body")( - button(`class` := "btn")("LOCKED"), - button(`class` := "btn")("MANUAL"), - button(`class` := "btn")("GUIDED"), - button(`class` := "btn")("AUTO"))))), - div(`class` := "row")( - div(`class` := "col-xs-4")( - div(`class` := "panel panel-default")( - div(`class` := "panel-body")( - 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")))), - div(`class` := "col-xs-5")( - div(`class` := "panel panel-default")( - div(`class` := "panel-body")( - panels.Primary(message)))), - div(`class` := "col-xs-3")( - div(`class` := "panel panel-default")( - div(`class` := "panel-body")( - panels.Communication( - socket.stats.packets, - socket.stats.crcErrors, - socket.stats.overflows, - socket.stats.wrongIds, - message)))))) - - env.root.appendChild(element.render) + env.root.appendChild(layout.element) } } \ No newline at end of file diff --git a/vfd-frontend/src/main/scala/vfd/frontend/MavlinkSocket.scala b/vfd-frontend/src/main/scala/vfd/frontend/MavlinkSocket.scala index 6dcc5fd..903020f 100644 --- a/vfd-frontend/src/main/scala/vfd/frontend/MavlinkSocket.scala +++ b/vfd-frontend/src/main/scala/vfd/frontend/MavlinkSocket.scala @@ -5,19 +5,26 @@ 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) { - val packet: Var[Packet] = Var(Packet.Empty) + 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( @@ -26,7 +33,8 @@ class MavlinkSocket(url: String, remoteSystemId: Int) { case Packet(seq, `remoteSystemId`, compId, msgId, payload) => packet() = pckt stats.packets() += 1 - case _ => stats.wrongIds() += 1 + case _ => + stats.wrongIds() += 1 } }, err => { @@ -39,16 +47,19 @@ class MavlinkSocket(url: String, remoteSystemId: Int) { 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 dv = new js.typedarray.DataView(buffer) + val view = new js.typedarray.DataView(buffer) - for (i <- 0 until dv.byteLength) { - parser.push(dv.getInt8(i)) + for (i <- 0 until view.byteLength) { + parser.push(view.getInt8(i)) } } connection.onclose = (e: dom.CloseEvent) => { - dom.alert("closed") + stats.open() = false } } \ No newline at end of file diff --git a/vfd-frontend/src/main/scala/vfd/frontend/ui/Components.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/Components.scala deleted file mode 100644 index f911865..0000000 --- a/vfd-frontend/src/main/scala/vfd/frontend/ui/Components.scala +++ /dev/null @@ -1,105 +0,0 @@ -package vfd.frontend.ui - -import org.scalajs.dom.HTMLElement -import rx.Obs -import rx.Rx -import rx.Rx -import scalatags.JsDom.all.ExtendedString -import scalatags.JsDom.all.bindNode -import scalatags.JsDom.all.div -import scalatags.JsDom.all.`object` -import scalatags.JsDom.all.stringAttr -import scalatags.JsDom.all.stringFrag -import scalatags.JsDom.all.stringStyle -import scalatags.JsDom.all.style -import scalatags.JsDom.all.`type` -import scalatags.JsDom.all.width -import vfd.frontend.util.Environment - -object Components { - - private def instrument(name: String)(implicit app: Environment) = { - val path = app.asset("images/instruments/" + name + ".svg") - `object`(`type` := "image/svg+xml", "data".attr := path, width := "100%")( - "Error loading image " + name).render - } - - private def frame(elem: HTMLElement, size: String) = { - div(style := s"width: $size; height: $size; display: inline-block;")( - elem) - } - - def led(color: Rx[String], size: String)(implicit app: Environment) = { - val elem = `object`(`type` := "image/svg+xml", "data".attr := app.asset("leds/led.svg"), width := size)( - "Error loading image.").render - - Obs(color, skipInitial = true) { - val svg = elem.contentDocument - svg.getElementById("light").setAttribute("fill", color()) - } - elem - } - - def horizon(pitchRoll: Rx[(Double, Double)], size: String)(implicit app: Environment) = { - val inst = instrument("horizon") - Obs(pitchRoll, skipInitial = true) { - val svg = inst.contentDocument - val pitch = svg.getElementById("pitch") - val roll = svg.getElementById("roll") - pitch.style.transition = "transform 250ms ease-out" - roll.style.transition = "transform 250ms ease-out" - pitch.style.transform = "translate(0px, " + pitchRoll()._1 + "px)" - roll.style.transform = "rotate(" + pitchRoll()._2 + "deg)" - } - frame(inst, size) - } - - def altimeter(value: Rx[Double], size: String)(implicit app: Environment) = { - val inst = instrument("altimeter") - Obs(value, skipInitial = true) { - val svg = inst.contentDocument - // 36deg === 1m - svg.getElementById("hand").setAttribute("transform", "rotate(" + value() * 36 + ")"); - } - frame(inst, size) - } - - def compass(value: Rx[Double], size: String)(implicit app: Environment) = { - val inst = instrument("compass") - Obs(value, skipInitial = true) { - val svg = inst.contentDocument - val heading = svg.getElementById("heading") - heading.style.transition = "transform 250ms ease-out" - heading.style.transform = "rotate(" + value() + "deg)" - } - frame(inst, size) - } - - def basic(value: Rx[Double], size: String)(implicit app: Environment) = { - val inst = instrument("basic") - Obs(value, skipInitial = true) { - val svg = inst.contentDocument - val hand = svg.getElementById("hand") - hand.style.transform = "rotate(" + value() * 270 / 100 + "deg)"; - hand.style.transition = "transform 250ms ease-out" - svg.getElementById("unit").textContent = "%" - svg.getElementById("value").textContent = value().toString - } - frame(inst, size) - } - - def bar(value: Rx[Double], size: String)(implicit app: Environment) = { - val inst = instrument("bar") - Obs(value, skipInitial = true) { - val svg = inst.contentDocument - val level = svg.getElementById("level") - level.style.transform = "translate(0px, " + 97 * (1 - value() / 100) + "px)"; - level.style.transition = "transform 250ms ease-out" - svg.getElementById("unit").textContent = "%" - svg.getElementById("value").textContent = value().toString - } - frame(inst, size) - } - -} - diff --git a/vfd-frontend/src/main/scala/vfd/frontend/ui/Layout.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/Layout.scala new file mode 100644 index 0000000..5892402 --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/ui/Layout.scala @@ -0,0 +1,57 @@ +package vfd.frontend.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.frontend.Environment +import vfd.frontend.MavlinkSocket +import vfd.frontend.ui.panels.Communication +import vfd.frontend.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-frontend/src/main/scala/vfd/frontend/ui/components/SvgInstrument.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/components/SvgInstrument.scala new file mode 100644 index 0000000..c22daa0 --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/ui/components/SvgInstrument.scala @@ -0,0 +1,55 @@ +package vfd.frontend.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.frontend.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-frontend/src/main/scala/vfd/frontend/ui/components/instruments.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/components/instruments.scala new file mode 100644 index 0000000..08e164a --- /dev/null +++ b/vfd-frontend/src/main/scala/vfd/frontend/ui/components/instruments.scala @@ -0,0 +1,124 @@ +package vfd.frontend.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.frontend.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-frontend/src/main/scala/vfd/frontend/ui/panels/Communication.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/panels/Communication.scala index 813b695..7fec52a 100644 --- a/vfd-frontend/src/main/scala/vfd/frontend/ui/panels/Communication.scala +++ b/vfd-frontend/src/main/scala/vfd/frontend/ui/panels/Communication.scala @@ -1,74 +1,106 @@ package vfd.frontend.ui.panels -import rx.Rx -import rx.Rx +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.i -import scalatags.JsDom.all.intFrag import scalatags.JsDom.all.stringAttr -import scalatags.JsDom.all.stringFrag -import scalatags.JsDom.all.style import scalatags.JsDom.all.table import scalatags.JsDom.all.tbody import scalatags.JsDom.all.td -import scalatags.JsDom.all.tr -import vfd.frontend.util.Environment -import vfd.frontend.util.Framework.RxStr -import vfd.frontend.ui.Components -import rx.core.Var -import rx.core.Obs -import org.mavlink.messages._ +import scalatags.JsDom.all._ +import vfd.frontend.Environment +import vfd.frontend.MavlinkSocket +import vfd.frontend.ui.components.Generic +import vfd.frontend.ui.components.Balance +import vfd.frontend.ui.components.Bar +import vfd.frontend.ui.components.Led object Communication { - def apply(packets: Rx[Int], crcs: Rx[Int], overflows: Rx[Int], wrongIds: Rx[Int], message: Rx[Message])(implicit app: Environment) = { - - val m0 = Var(0.0) - val m1 = Var(0.0) - val m2 = Var(0.0) - val m3 = Var(0.0) - val battery = Var(0.0) + def apply(socket: MavlinkSocket)(implicit app: Environment): HTMLElement = { + + val hb = i(`class` := "fa fa-heart heartbeat").render - Obs(message) { - message() match { - case Motor(_m0, _m1, _m2, _m3) => - m0() = _m0 - m1() = _m1 - m2() = _m2 - m3() = _m3 - case Power(mV) => battery() = mV / 120 - case _ => () + 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( - "Link Status", - table(`class` := "table table-condensed")( + table(`class` := "table")( + thead("Communication"), tbody( tr( - td("Uplink"), - td("-20 dBm"), + td("Conn"), + div(width := "20px")(td((new Led()).element)), td("Server"), td("5 ms")), tr( + td("Uplink"), + td("-20 dBm"), td("Heartbeat"), - td(i(`class` := "fa fa-heart", style := "color: #ff0000;"))))), - "Packet Statistics", - table(`class` := "table table-condensed")( + td(hb)))), + table(`class` := "table-instrument", style := "height: 100px")( tbody( tr( - td("OK"), - td(packets), - td(`class` := "danger")("CRC"), - td(crcs), - td("OFLW"), - td(overflows), - td("BID"), - td(wrongIds)))), - div(Components.bar(battery, "25%")), - div( - Components.basic(m0, "25%"),Components.basic(m1, "25%"),Components.basic(m2, "25%"),Components.basic(m3, "25%"))) + 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-frontend/src/main/scala/vfd/frontend/ui/panels/Primary.scala b/vfd-frontend/src/main/scala/vfd/frontend/ui/panels/Primary.scala index 4517ea0..f5f260c 100644 --- a/vfd-frontend/src/main/scala/vfd/frontend/ui/panels/Primary.scala +++ b/vfd-frontend/src/main/scala/vfd/frontend/ui/panels/Primary.scala @@ -1,33 +1,46 @@ package vfd.frontend.ui.panels -import org.mavlink.messages._ +import org.mavlink.messages.Attitude +import org.scalajs.dom.HTMLElement + import rx.core.Obs -import rx.core.Rx -import rx.core.Var -import scalatags.JsDom.all.div -import vfd.frontend.ui.Components -import vfd.frontend.util.Environment +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.frontend.Environment +import vfd.frontend.MavlinkSocket +import vfd.frontend.ui.components.Altimeter +import vfd.frontend.ui.components.Compass +import vfd.frontend.ui.components.Horizon object Primary { - def apply(message: Rx[Message])(implicit env: Environment) = { - val pitchRoll = Var((0.0, 0.0)) // Rx{(attitude().pitch.toDouble, attitude().roll.toDouble)} - val heading = Var(0.0) //Rx{attitude().heading.toDouble} - val altitude = Var(0.0) //Rx{pressure().pressure.toDouble} + def apply(socket: MavlinkSocket)(implicit env: Environment): HTMLElement = { + + val compass = new Compass + val horizon = new Horizon + val altimeter = new Altimeter - Obs(message) { - message() match { + Obs(socket.message, skipInitial = true) { + socket.message() match { case Attitude(roll, pitch, yaw) => - pitchRoll() = (roll, pitch) - heading() = yaw + horizon.update(pitch, roll) + compass.update(yaw) case _ => () } } - div( - Components.compass(heading, "33%"), - Components.horizon(pitchRoll, "33%"), - Components.altimeter(altitude, "33%")) + table(`class` := "table-instrument")( + tbody( + tr( + td(compass.element), + td(horizon.element), + td(altimeter.element)))).render + } } \ No newline at end of file diff --git a/vfd-frontend/src/main/scala/vfd/frontend/util/Environment.scala b/vfd-frontend/src/main/scala/vfd/frontend/util/Environment.scala deleted file mode 100644 index fdd908a..0000000 --- a/vfd-frontend/src/main/scala/vfd/frontend/util/Environment.scala +++ /dev/null @@ -1,14 +0,0 @@ -package vfd.frontend.util - -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-frontend/src/main/scala/vfd/frontend/util/Framework.scala b/vfd-frontend/src/main/scala/vfd/frontend/util/Framework.scala deleted file mode 100644 index 066478e..0000000 --- a/vfd-frontend/src/main/scala/vfd/frontend/util/Framework.scala +++ /dev/null @@ -1,70 +0,0 @@ -package vfd.frontend.util - -import scala.language.implicitConversions -import scala.util.Failure -import scala.util.Success - -import org.scalajs.dom -import org.scalajs.dom.Element - -import rx.Rx -import rx.Rx -import rx.core.Obs -import scalatags.JsDom.all.Attr -import scalatags.JsDom.all.AttrValue -import scalatags.JsDom.all.Frag -import scalatags.JsDom.all.HtmlTag -import scalatags.JsDom.all.Style -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 -import scalatags.JsDom.all.StyleValue - -/** - * A minimal binding between Scala.Rx and Scalatags and Scala-Js-Dom - * taken from https://github.com/lihaoyi/workbench-example-app/blob/todomvc/src/main/scala/example/Framework.scala, by Li Haoyi - */ -object Framework { - - /** - * Wraps reactive strings in spans, so they can be referenced/replaced - * when the Rx changes. - */ - implicit def RxStr[T](r: Rx[T])(implicit f: T => Frag): Frag = { - rxMod(Rx(span(r()))) - } - - /** - * 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 <: dom.HTMLElement](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) - } - - implicit def RxAttrValue[T: AttrValue] = new AttrValue[Rx[T]] { - def apply(t: Element, a: Attr, r: Rx[T]): Unit = { - Obs(r) { implicitly[AttrValue[T]].apply(t, a, r()) } - } - } - - implicit def RxStyleValue[T: StyleValue] = new StyleValue[Rx[T]] { - def apply(t: Element, s: Style, r: Rx[T]): Unit = { - Obs(r) { implicitly[StyleValue[T]].apply(t, s, r()) } - } - } -} diff --git a/vfd-frontend/src/main/scala/vfd/frontend/util/package.scala b/vfd-frontend/src/main/scala/vfd/frontend/util/package.scala deleted file mode 100644 index 2d98e2f..0000000 --- a/vfd-frontend/src/main/scala/vfd/frontend/util/package.scala +++ /dev/null @@ -1,15 +0,0 @@ -package vfd.frontend - -import scala.reflect.ClassTag - -import rx.Rx -import rx.Rx -import rx.ops.RxOps - -package object util { - - implicit class richRx[A](val input: Rx[A]) extends AnyVal { - def only[B <: A](implicit ct: ClassTag[B]): Rx[B] = input filter (_.isInstanceOf[B]) map (_.asInstanceOf[B]) - } - -} \ No newline at end of file -- cgit v1.2.3