diff --git a/doc/source/_images/commands/uplink_channel_button.png b/doc/source/_images/commands/uplink_channel_button.png new file mode 100644 index 000000000..2af0a6a79 Binary files /dev/null and b/doc/source/_images/commands/uplink_channel_button.png differ diff --git a/doc/source/_images/commands/uplink_window.png b/doc/source/_images/commands/uplink_window.png new file mode 100644 index 000000000..cac59034f Binary files /dev/null and b/doc/source/_images/commands/uplink_window.png differ diff --git a/doc/source/commands/communication.rst b/doc/source/commands/communication.rst index d391b5e5b..a4e2fc367 100644 --- a/doc/source/commands/communication.rst +++ b/doc/source/commands/communication.rst @@ -4,7 +4,8 @@ Communication ============= kOS allows you to write scripts that communicate with scripts running on other processors within the same vessel -(inter-processor communication) or on other vessels (inter-vessel communication). +(inter-processor communication) or on other vessels (inter-vessel communication) or as a receiver of messages from +the KSC. Limitations ----------- @@ -144,6 +145,39 @@ The receiving CPU will use :attr:`CORE:MESSAGES` to access its message queue:: PRINT "Unexpected message: " + RECEIVED:CONTENT. } +Sending messages to vessel from KSC +----------------------------------- + +.. versionadded:: ?? + +It is possible to send messages to the active vessel from the Kerbal Space Center, e.g. for sending instructions. +To do this, first change to the vessel you want to send the message to. Then open an uplink channel via a button +in the main kOS window: + +.. figure:: /_images/commands/uplink_channel_button.png + :width: 30 % + + +This opens a new window where you can type and send the message: + +.. figure:: /_images/commands/uplink_window.png + :width: 50 % + +Key Notes: + + 1. Name of the vessel the uplink is open to. + 2. Time to receive the last message sent (will be ``NO MESSAGE SENT`` if none has been sent yet and ``RECEIVED`` + if the last message sent was already received). + 3. You can type the message here. + 4. Sends the message. + 5. Closes the uplink window. + +The message (its ``CONTENT``) will be a string. It is the job of the receiver to parse it into something useful (if needed). +The message can be sent only if the active vessel's :global:`HOMECONNECTION` is connected. The message behaves in the same +way as if it was sent from another vessel, except for :attr:`Message:SENDER` and :attr:`Message:HASSENDER` that indicate +that the source is the KSC and not a vessel (see their reference for details). + + .. _connectivityManagers: Connectivity Managers diff --git a/doc/source/general/settingsWindows.rst b/doc/source/general/settingsWindows.rst index 985c0dbd4..986b83903 100644 --- a/doc/source/general/settingsWindows.rst +++ b/doc/source/general/settingsWindows.rst @@ -19,6 +19,8 @@ and visa versa.) Here is an annotated image of the control panel and what it does: +TODO - replace with new image + .. figure:: /_images/general/controlPanelWindow.png :width: 80 % diff --git a/doc/source/structures/communication/message.rst b/doc/source/structures/communication/message.rst index f738a97f0..249dc2ca5 100644 --- a/doc/source/structures/communication/message.rst +++ b/doc/source/structures/communication/message.rst @@ -42,7 +42,7 @@ Structure - date this message was received at * - :attr:`SENDER` - :struct:`Vessel` or :struct:`Boolean` - - vessel which has sent this message, or Boolean false if sender vessel is now gone + - vessel which has sent this message, or Boolean false if sender vessel is now gone, or Boolean true if the sender is the KSC * - :attr:`HASSENDER` - :struct:`Boolean` - Tests whether or not the sender vessel still exists. @@ -71,11 +71,16 @@ Structure :type: :struct:`Vessel` or :struct:`Boolean` Vessel which has sent this message, or a boolean false value if - the sender vessel no longer exists. + the sender vessel no longer exists, or a boolean true value if the + message was sent from KSC. - If the sender of the message doesn't exist anymore (see the explanation - for :attr:`HASSENDER`), this suffix will return a different type - altogether. It will be a :struct:`Boolean` (which is false). + If the sender of the message is an existing vessel, this suffix will + return that vessel. In all other cases, this suffix will return a + :struct:`Boolean` with the value: + + * ``false`` if the sender of the message is a vessel that no + longer exists (see :attr:`HASSENDER` for explanation), + * ``true`` if the message was sent from KSC. You can check for this condition either by using the :attr:`HASSENDER` suffix, or by checking the ``:ISTYPE`` suffix of the sender to @@ -89,11 +94,13 @@ Structure when it was processed by the receiving script, it's possibile that the vessel that sent the message might not exist anymore. It could have either exploded, or been recovered, or been merged into another - vessel via docking. You can check the value of the ``:HASSENDER`` - suffix to find out if the sender of the message is still a valid vessel. - If :attr:`HASSENDER` is false, then :attr:`SENDER` won't give you an - object of type :struct:`Vessel` and instead will give you just a - :struct:`Boolean` false. + vessel via docking. Another possible case is that the message was not + sent from a vessel at all but from the KSC. + + You can check the value of the ``:HASSENDER`` suffix to find out if + the sender of the message is still a valid vessel. If :attr:`HASSENDER` + is false, then :attr:`SENDER` won't give you an object of type + :struct:`Vessel` and instead will give you just a :struct:`Boolean`. .. attribute:: Message:CONTENT diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json new file mode 100644 index 000000000..0c0e79f7f --- /dev/null +++ b/src/.vscode/tasks.json @@ -0,0 +1,26 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "0.1.0", + "windows": { + "command": "msbuild" + }, + "linux": { + "command": "xbuild" + }, + "args": [ + // Ask msbuild to generate full paths for file names. + "/property:GenerateFullPaths=true" + ], + "taskSelector": "/t:", + "showOutput": "silent", + "tasks": [ + { + "taskName": "build", + // Show the output window only if unrecognized errors occur. + "showOutput": "silent", + // Use the standard MS compiler pattern to detect errors, warnings and infos + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/kOS/Communication/KscTarget.cs b/src/kOS/Communication/KscTarget.cs new file mode 100644 index 000000000..60db65d90 --- /dev/null +++ b/src/kOS/Communication/KscTarget.cs @@ -0,0 +1,25 @@ +using System; +using kOS.Suffixed; + +namespace kOS.Communication +{ + [Safe.Utilities.KOSNomenclature("KscTarget", KOSToCSharp = false)] + public class KscTarget : VesselTarget { + private static readonly KscTarget kscTarget = new KscTarget(); + + public static KscTarget Instance + { + get { + return kscTarget; + } + } + + private KscTarget() + {} + + public override Guid GetGuid() + { + return Guid.Empty; + } + } +} \ No newline at end of file diff --git a/src/kOS/Communication/MessageStructure.cs b/src/kOS/Communication/MessageStructure.cs index 4625c9076..e930f1e65 100644 --- a/src/kOS/Communication/MessageStructure.cs +++ b/src/kOS/Communication/MessageStructure.cs @@ -58,7 +58,7 @@ public Structure GetVesselTarget() if (vessel == null) { - return new BooleanValue(false); + return new BooleanValue(KscTarget.Instance.GetGuid().ToString().Equals(Message.Vessel)); } return new VesselTarget(vessel, shared); diff --git a/src/kOS/Screen/KOSToolbarWindow.cs b/src/kOS/Screen/KOSToolbarWindow.cs index febdd6cf4..5335c486a 100644 --- a/src/kOS/Screen/KOSToolbarWindow.cs +++ b/src/kOS/Screen/KOSToolbarWindow.cs @@ -84,6 +84,8 @@ public class KOSToolbarWindow : MonoBehaviour private bool uiGloballyHidden = false; + private UplinkWindow uplinkWindow; + /// /// Unity hates it when a MonoBehaviour has a constructor, /// so all the construction work is here instead: @@ -114,6 +116,8 @@ public void Awake() GameObject.DontDestroyOnLoad(this); fontPicker = null; + + uplinkWindow = gameObject.AddComponent(); } // TODO - Remove this next method after verifying KSP 1.1 works without it: @@ -427,7 +431,10 @@ public void DrawWindow(int windowID) CountBeginVertical(); CountBeginHorizontal(); + CountBeginVertical(); DrawActiveCPUsOnPanel(); + DrawActiveVesselLink(); + CountEndVertical(); CountBeginVertical("", 150); GUILayout.Label("CONFIG VALUES", headingLabelStyle); @@ -609,7 +616,7 @@ private string TelnetStatusMessage() private void DrawActiveCPUsOnPanel() { - scrollPos = GUILayout.BeginScrollView(scrollPos, panelSkin.scrollView, GUILayout.MinWidth(260), GUILayout.Height(windowRect.height - 60)); + scrollPos = GUILayout.BeginScrollView(scrollPos, panelSkin.scrollView, GUILayout.MinWidth(260), GUILayout.Height(windowRect.height - 90)); CountBeginVertical(); Vessel prevVessel = null; @@ -640,6 +647,29 @@ private void DrawActiveCPUsOnPanel() GUILayout.EndScrollView(); } + private void DrawActiveVesselLink() + { + Vessel thisVessel = FlightGlobals.ActiveVessel; + + CountBeginVertical(); + if (thisVessel == null) + { + GUILayout.Label("No active vessel."); + } + else + { + if (GUILayout.Button(uplinkWindow.IsOpen ? + new GUIContent("Close uplink channel") : + new GUIContent("Open uplink channel to active vessel"))) + { + SafeHouse.Logger.SuperVerbose("KOSToolBarWindow: toggle uplink"); + uplinkWindow.AttachTo(thisVessel); + uplinkWindow.Toggle(); + } + } + CountEndVertical(); + } + private void DrawPartRow(Part part) { CountBeginHorizontal(); diff --git a/src/kOS/Screen/UplinkWindow.cs b/src/kOS/Screen/UplinkWindow.cs new file mode 100644 index 000000000..54acfabed --- /dev/null +++ b/src/kOS/Screen/UplinkWindow.cs @@ -0,0 +1,403 @@ +using System; +using UnityEngine; +using kOS.Safe.Screen; +using kOS.Communication; +using kOS.Safe.Encapsulation; +using kOS.Safe.Module; +using kOS.Safe.Persistence; +using kOS.Safe.Utilities; +using System.Linq; +using kOS.Module; + +namespace kOS.Screen +{ + public class UplinkWindow : KOSManagedWindow + { + private const int FRAME_THICKNESS = 8; + private const int FONT_HEIGHT = 12; + private const string EXIT_BUTTON_TEXT = "Exit"; + private const string SEND_BUTTON_TEXT = "Send"; + + private Vessel vessel; + + private Rect innerCoords; + private Rect sendCoords; + private Rect exitCoords; + private Rect titleLabelCoords; + private Rect delayLabelCoords; + private Rect resizeButtonCoords; + private Texture2D resizeImage; + private bool resizeMouseDown; + private Vector2 resizeOldSize; // width and height it had when the mouse button went down on the resize button. + private Vector2 scrollPosition; // tracks where within the text box it's scrolled to. + private string contents = ""; + private bool frozen; + private bool consumeEvent; + private bool connection; + private double lastMsgReceiveTime = double.NaN; + + public UplinkWindow() + { + } + + public void Toggle() + { + if (IsOpen) Close(); + else Open(); + } + + public void Freeze(bool newVal) + { + frozen = newVal; + } + + public void Awake() + { + WindowRect = new Rect(100, 60, 470, 180); + + // Load dummy textures + resizeImage = new Texture2D(0, 0, TextureFormat.DXT1, false); + + var urlGetter = new WWW(string.Format("file://{0}GameData/kOS/GFX/resize-button.png", KSPUtil.ApplicationRootPath.Replace("\\", "/"))); + urlGetter.LoadImageIntoTexture(resizeImage); + } + + public override void GetFocus() + { + Freeze(false); + } + + public override void LoseFocus() + { + Freeze(true); + } + + public void AttachTo(Vessel vessel) { + if (this.vessel != vessel) + { + lastMsgReceiveTime = double.NaN; + contents = ""; + } + this.vessel = vessel; + } + + public override void Open() + { + base.Open(); + BringToFront(); + } + + public override void Close() + { + base.Close(); + resizeMouseDown = false; + } + + public int GetUniqueId() + { + return UniqueId; + } + + public void SetUniqueId(int newValue) + { + UniqueId = newValue; + } + + public void Update() + { + // if the active vessel changed (or there is none), close the window + Vessel activeVessel = FlightGlobals.ActiveVessel; + if (activeVessel == null || activeVessel != vessel) + { + Close(); + } + UpdateLogic(); + } + + public void OnGUI() + { + if (!IsOpen) return; + + CalcInnerCoords(); + + WindowRect = GUI.Window(UniqueId, WindowRect, ProcessWindow, ""); + // Some mouse global state data used by several of the checks: + + if (consumeEvent) + { + consumeEvent = false; + Event.current.Use(); + } + } + + protected void CalcInnerCoords() + { + if (!IsOpen) return; + + Vector2 titleLabSize = GUI.skin.label.CalcSize(new GUIContent(BuildTitle(false))); + Vector2 delayLabSize = GUI.skin.label.CalcSize(new GUIContent(BuildDelay(true))); + Vector2 exitSize = GUI.skin.box.CalcSize(new GUIContent(EXIT_BUTTON_TEXT)); + exitSize = new Vector2(exitSize.x + 4, exitSize.y + 4); + Vector2 sendSize = GUI.skin.box.CalcSize(new GUIContent(SEND_BUTTON_TEXT)); + sendSize = new Vector2(sendSize.x + 4, sendSize.y + 4); + + titleLabelCoords = new Rect(5, 1, titleLabSize.x, titleLabSize.y); + delayLabelCoords = new Rect(5, 1 + titleLabSize.y, delayLabSize.x, delayLabSize.y); + innerCoords = new Rect(FRAME_THICKNESS, + delayLabelCoords.y + 1.5f * FONT_HEIGHT, + WindowRect.width - 2 * FRAME_THICKNESS, + WindowRect.height - 2 * FRAME_THICKNESS - 2 * FONT_HEIGHT); + + + float buttonXCounter = WindowRect.width; // Keep track of the x coord of leftmost button so far. + + buttonXCounter -= (exitSize.x + 5); + exitCoords = new Rect(buttonXCounter, 1, exitSize.x, exitSize.y); + + buttonXCounter -= (sendSize.x + 2); + sendCoords = new Rect(buttonXCounter, 1, sendSize.x, sendSize.y); + + resizeButtonCoords = new Rect(WindowRect.width - resizeImage.width, + WindowRect.height - resizeImage.height, + resizeImage.width, + resizeImage.height); + } + + protected string BuildTitle(bool connected = true) + { + if (!connected) + { + return "Uplink to: " + vessel.vesselName + " NO CONNECTION"; + } + return "Uplink to: " + vessel.vesselName; + } + + protected string BuildDelay(bool forceNoMessage = false) + { + double timeLeft; + timeLeft = forceNoMessage ? double.NaN : lastMsgReceiveTime - Planetarium.GetUniversalTime(); + + string delay; + if (timeLeft <= 0) + { + delay = "Receive in: RECEIVED"; + } + else if (timeLeft > 0) + { + delay = String.Format("Receive in: {0:0.###}", timeLeft); + } + else + { + delay = "Receive in: NO MESSAGE SENT"; + } + return delay; + } + + private void ProcessWindow(int windowId) + { + if (!frozen) + { + CheckKeyboard(); + } + + DrawWindow(windowId); + + CheckResizeDrag(); + GUI.DragWindow(); + } + + protected void CheckKeyboard() + { + if (Event.current.type == EventType.KeyDown) + { + switch (Event.current.keyCode) + { + case KeyCode.PageUp: + DoPageUp(); + Event.current.Use(); + break; + + case KeyCode.PageDown: + DoPageDown(); + Event.current.Use(); + break; + /* + case KeyCode.E: + if (Event.current.control) + Close(); + Event.current.Use(); + break; + + case KeyCode.S: + if (Event.current.control) + Send(); + Event.current.Use(); + break; + */ + } + } + } + + protected void DoPageUp() + { + var editor = GetWidgetController(); + + // Seems to be no way to move more than one line at + // a time - so have to do this: + int pos = Math.Min(editor.cursorIndex, contents.Length - 1); + int rows = ((int)innerCoords.height) / FONT_HEIGHT; + while (rows > 0 && pos >= 0) + { + if (contents[pos] == '\n') + rows--; + pos--; + editor.MoveLeft(); // there is a MoveUp but it doesn't work. + } + } + + protected void DoPageDown() + { + var editor = GetWidgetController(); + + // Seems to be no way to move more than one line at + // a time - so have to do this: + int pos = Math.Min(editor.cursorIndex, contents.Length - 1); + int rows = ((int)innerCoords.height) / FONT_HEIGHT; + while (rows > 0 && pos < contents.Length) + { + if (contents[pos] == '\n') + rows--; + pos++; + editor.MoveRight(); // there is a MoveDown but it doesn't work. + } + } + + protected void CheckResizeDrag() + { + Event e = Event.current; + if (e.type == EventType.mouseDown && e.button == 0) + { + if (resizeButtonCoords.Contains(MouseButtonDownPosRelative)) + { + // Remember the fact that this mouseDown started on the resize button: + resizeMouseDown = true; + resizeOldSize = new Vector2(WindowRect.width, WindowRect.height); + Event.current.Use(); + } + } + if (e.type == EventType.mouseUp && e.button == 0) // mouse button went from Down to Up just now. + { + if (resizeMouseDown) + { + resizeMouseDown = false; + Event.current.Use(); + } + } + // For some reason the Event style of checking won't let you + // see drags extending outside the current window, while the Input style + // will. That's why this looks different from the others. + if (Input.GetMouseButton(0)) + { + if (resizeMouseDown) + { + var mousePos = new Vector2(Event.current.mousePosition.x, Event.current.mousePosition.y); + Vector2 dragDelta = mousePos - MouseButtonDownPosRelative; + WindowRect = new Rect(WindowRect.xMin, + WindowRect.yMin, + Math.Max(resizeOldSize.x + dragDelta.x, 100), + Math.Max(resizeOldSize.y + dragDelta.y, 30)); + CalcInnerCoords(); + Event.current.Use(); + } + } + } + + protected void DrawWindow(int windowId) + { + connection = HasConnection(); + GUI.contentColor = Color.green; + + GUILayout.BeginArea(innerCoords); + scrollPosition = GUILayout.BeginScrollView(scrollPosition); + int preLength = contents.Length; + contents = GUILayout.TextArea(contents); + int postLength = contents.Length; + GUILayout.EndScrollView(); + GUILayout.EndArea(); + + GUI.contentColor = connection ? Color.green : Color.red; + GUI.enabled = connection; + GUI.Label(titleLabelCoords, BuildTitle(connection)); + GUI.contentColor = Color.green; + GUI.Label(delayLabelCoords, BuildDelay()); + + if (GUI.Button(sendCoords, SEND_BUTTON_TEXT)) + { + Send(); + } + GUI.enabled = true; + if (GUI.Button(exitCoords, EXIT_BUTTON_TEXT)) + { + Close(); + } + KeepCursorScrolledInView(); + + GUI.Box(resizeButtonCoords, resizeImage); + } + + protected void KeepCursorScrolledInView() + { + // It's utterly ridiculous that Unity's TextArea widget doesn't + // just do this automatically. It's basic behavior for a scrolling + // text widget that when the text cursor moves out of the viewport you + // scroll to keep it in view. Oh well, have to do it manually: + // + // NOTE: This method is what is interfering with the scrollbar's ability + // to scroll with the mouse - this routine is locking the scrollbar + // to only be allowed to move as far as the cursor is still in view. + // Fixing that would take a bit of work. + // + + var editor = GetWidgetController(); + Vector2 pos = editor.graphicalCursorPos; + float usableHeight = innerCoords.height - 2.5f * FONT_HEIGHT; + if (pos.y < scrollPosition.y) + scrollPosition.y = pos.y; + else if (pos.y > scrollPosition.y + usableHeight) + scrollPosition.y = pos.y - usableHeight; + } + + // Return type needs full namespace path because kOS namespace has a TextEditor class too: + protected UnityEngine.TextEditor GetWidgetController() + { + // Whichever TextEdit widget has current focus (should be this one if processing input): + // There seems to be no way to grab the text edit controller of a Unity Widget by + // specific ID. + return (UnityEngine.TextEditor) + GUIUtility.GetStateObject(typeof(UnityEngine.TextEditor), GUIUtility.keyboardControl); + } + + private bool HasConnection() + { + return ConnectivityManager.HasConnectionToHome(vessel); + } + + public void Send() + { + if (!HasConnection()) { + return; + } + + Structure payload = new StringValue(contents); + + MessageQueueStructure queue = InterVesselManager.Instance.GetQueue(vessel, null); + + double delay = ConnectivityManager.GetDelayToHome(vessel); + double sentAt = Planetarium.GetUniversalTime(); + double receivedAt = sentAt + delay; + Message msg = Message.Create(payload, sentAt, receivedAt, KscTarget.Instance, null); + queue.Push(msg); + lastMsgReceiveTime = receivedAt; + } + } +} diff --git a/src/kOS/Suffixed/VesselTarget.cs b/src/kOS/Suffixed/VesselTarget.cs index 46711c722..aaaaa69f9 100644 --- a/src/kOS/Suffixed/VesselTarget.cs +++ b/src/kOS/Suffixed/VesselTarget.cs @@ -34,7 +34,7 @@ public override StringValue GetName() return Vessel.vesselName; } - public Guid GetGuid() + public virtual Guid GetGuid() { return Vessel.id; } diff --git a/src/kOS/kOS.csproj b/src/kOS/kOS.csproj index 035585a3c..8d60c89e3 100644 --- a/src/kOS/kOS.csproj +++ b/src/kOS/kOS.csproj @@ -238,6 +238,8 @@ + +