Welcome to Keen Software House Forums! Log in or Sign up to interact with the KSH community.
  1. You are currently browsing our forum as a guest. Create your own forum account to access all forum functionality.

[TUTORIAL] Antenna Communication (update 1.189)

Discussion in 'Programming Guides and Tools' started by Paxroy, Mar 9, 2019.

  1. Paxroy Trainee Engineer

    Messages:
    4
    All right, after messing around for a long while with the new intergrid communication system, I finally have a lot of it figured out. I'm not a C# wizard or experienced programmer, so I'll explain this in a way I think it makes sense to fellow noobs. I.e, if you're a wizard, go easy on a poor hobbit, will you?

    First off, some key concepts need explaining. I'll link as needed to Malware's API index for your convenience. I won't go into every single command in detail, so I recommend checking it out.

    • IGC - As u/jamesmuell pointed out to me (thanks man), there's an "instance variable called IGC of type IMyIntergridCommunicationSystem in MyGridProgram". It is now through this variable that we call the basic methods used for intergrid communications, instead of via the antenna block itself as per before update 1.189.
    • MyIGCMessage - This is now the struct for messages. Prior to 1.189, a message was just a string. Now this struct contains 3 variables:
    1. Data - This is the actual contents of your message. Your input is automatically converted to type object, which means you can send different types of variables as "generic" object now, not just strings. However, on the receiving end, you'll probably want to convert it to its original type. I think this back and forth converting business is called "Boxing and Unboxing" if you need help on that.
    2. Tag - A keyword of type string you choose when sending a message. It is basically like a channel or frequency on a radio. When you broadcast a message, receivers must "know" the tag to read the message, just like you have to know what radio station you want to listen to when you're driving. More on that later.
    3. Source - Type is long, and this is just a bunch of digits unique to the Programmable Block that the message was sent from. Kind of like an IP address I suppose.
    • Listener - If the tag is the channel/frequency, the listener is the CIA analyst listening in on that frequency for you, the director. When you register a new listener, you specify what tag it should listen to. All incoming messages with that tag are stored in the listener instance you created. There are two types of Listeners: IMyBroadcastListener and IMyUnicastListener.
    • Broadcast - What the name suggests. When you send a broadcast message, you specify 3 arguments:
    1. The tag as string.
    2. The data as object.
    3. TransmissionDistance - This essentially governs what other Programmable Blocks are going to receive your broadcast. E.g. TransmissionDistance.TransmissionDistanceMax will maximize the range, and so the number of receiving PB:s.
    • Unicast - This is a transmission to a specific receiving Programmable Block, i.e. it's private. When you send a unicast, you specify 3 arguments:
    1. The addressee (the receiving PB) as long - This is the Source number, or "IP address", as described above. More on how to obtain it later.
    2. The tag as string.
    3. The data as object.
    • Callback - This is something you can activate for each Listener. In short, callback is a Listener method that you provide with a string argument. Whenever the Listener receives a message, your script will be triggered the next tick and get passed the string argument you provided.
    OK! Let's set up a basic script with some useful variables before we get into it. Just remember that this is very generic code. You'll want to make yours much fancier, with proper error management etc.
    Code:
    public Program()
    {
    	// Set the script to run every 100 ticks, so no timer needed.
    	Runtime.UpdateFrequency = UpdateFrequency.Update100;
    }
    
    // Declare variables.
    bool setupcomplete = false;
    IMyRadioAntenna antenna;
    IMyProgrammableBlock pb;
    
    public void Main(string arg)
    {
    	// If setupcomplete is false, run Setup method.
    	if(!setupcomplete)
    	{
    		Echo("Running setup.");
    		Setup();
    	}
    	else
    	{
    		// Script magic will happen here.
    	}
    }
    
    public void Setup()
    {
    	antenna = GridTerminalSystem.GetBlockWithName("Antenna") as IMyRadioAntenna;
    	pb = GridTerminalSystem.GetBlockWithName("Programmable block") as IMyProgrammableBlock;
    
    	// Connect the PB to the antenna. This can also be done from the grid terminal.
    	antenna.AttachedProgrammableBlock = pb.EntityId;
    
    	if(antenna != null)
    	{
    		Echo("Setup complete.");
    		setupcomplete = true;
    	}
    	else
    	{
    		Echo("Setup failed. No antenna found.");
    	}
    }
    Now, let's try broadcasting a message to a friend using a tag of our choosing. We also want to be able to receive our friend's broadcasts, so we'll create a Broadcast Listener with the same tag. Remember, you can find and read up on all the methods and properties we're using here in Malware's API index.

    Code:
    public void Main(string arg)
    {
    	// If setupcomplete is false, run Setup method.
    	if(setupcomplete == false)
    	{
    		Echo("Running setup.");
    		Setup();
    	}
    	else
    	{
    		// Script magic will happen here.
    
    		// Create a tag. Our friend will use this in his script in order to receive our messages.
    		string tag1 = "channel 1";
    
    		// Create our message.										
    		string messageOut = "Hello friend!";
    
    		// Through the IGC variable we issue the broadcast method. IGC is "pre-made",
    		// so we don't have to declare it ourselves, just go ahead and use it.
    		IGC.SendBroadcastMessage(tag1, messageOut, TransmissionDistance.TransmissionDistanceMax);
    
    		// To create a listener, we use IGC to access the relevant method.
    		// We pass the same tag argument we used for our message.
    		IGC.RegisterBroadcastListener(tag1);			
    	}
    }
    NOTE: This code executes every time the script is run. That means broadcasting the same message and registering the same listener over and over again. You might want to adapt your own code to only broadcast when you want, and do the listener registration in the Setup() method for instance. These code examples are just simple tidbits to get you started.

    In order to read our friend's response, we need to get the message from the Listener we made. As a side note, you might have noticed in the code above that we didn't instantiate any Listener object ourselves. This is because IGC stores our Listeners in a list of its own, which we can access, as we'll see below.

    Code:
    public void Main(string arg)
    {
    	// If setupcomplete is false, run Setup method.
    	if(setupcomplete == false)
    	{
    		Echo("Running setup.");
    		Setup();
    	}
    	else
    	{
    	// ......ALL OUR EARLIER SCRIPT MAGIC IS HIDDEN HERE......		
    
    	// Create a list for broadcast listeners.
    	List<IMyBroadcastListener> listeners = new List<IMyBroadcastListener>();
    
    	// The method argument below is the list we wish IGC to populate with all Listeners we've made.
    	// Our Listener we created will be at index 0, because we only have that one Listener so far.
    	IGC.GetBroadcastListeners(listeners);
    
    	// We don't want to try receiving messages if we don't have any.
    	// So we access the HasPendingMessage property to check if our Listener has any unread messages.
    	if(listeners[0].HasPendingMessage)
    	{
    		// Let's create a variable for our new message.
    		// Remember, messages have the type MyIGCMessage.
    		MyIGCMessage message = new MyIGCMessage;
    
    		// Time to get our message from our Listener (at index 0 of our Listener list).
    		// We do this with the following method:
    		message = listeners[0].AcceptMessage();
    
    		// A message is a struct of 3 variables. To read the actual data,
    		// we access the Data field, convert it to type string (unboxing),
    		// and store it in the variable messagetext.
    		string messagetext = message.Data.ToString();
    
    		// We can also access the tag that the message was sent with.
    		string messagetag = message.Tag;
    
    		//Here we store the "address" to the Programmable Block (our friend's) that sent the message.
    		long sender = message.Source;
    
    		//Do something with the information!
    		Echo("Message received with tag" + messagetag + "\n\r");
    		Echo("from address " + sender.ToString() + ": \n\r");
    		Echo(messagetext);
    		}
    	}
    }
    Note: It's possible to retrieve more than one message in one execution by looping the AcceptMessage() method. Question is, if we have more than one message stored in a Listener, which do we get first when we call the method? According to the API, Space Engineers has a message queue for every Listener. It is ordered by arrival time, so the AcceptMessage() method will give us the oldest message first. Also, when a message is accepted, it is moved to the back of the queue, but it is not deleted.

    So if we continuously loop through and accept messages, we'll start over from the oldest message eventually, and on it goes. Here's where the HasPendingMessage is very useful. It returns true if we have any unread messages, and false if we have read them all.

    Note that there is a maximum number of messages that the queue will hold. There is a listener property called MaxWaitingMessages that returns an integer representing that number. If the number of messages exceeds the maximum, the oldest will be discarded first. However, there's no way to manually delete messages from the queue from what I can tell. Neither have I found a way to see how many messages there are in the queue.

    Lastly, we'll look at Unicasts. They're very similar to broadcasts, except that we don't register Listeners manually. Instead, there is a "pre-made" unicast Listener, and we can't make more. This means that all unicast transmissions, regardless of their tag, ends up with that Listener. To use our earlier CIA metaphor, we only need one CIA analyst to listen in on our private channel.

    Code:
    public void Main(string arg)
    {
    	// If setupcomplete is false, run Setup method.
    	if(setupcomplete == false)
    	{
    		Echo("Running setup.");
    		Setup();
    	}
    	else
    	{
    		// ......ALL OUR EARLIER SCRIPT MAGIC IS HIDDEN HERE......
    
    		// The unicast Listener is pre-made and accessed through IGC.
    		// We store it as unisource for easy access.
    		IMyUnicastListener unisource = IGC.UnicastListener;
    
    		if(unisource.HasPendingMessage)
    		{
    			// Just like earlier, we create a variable for our message and accept the new
    			// message from our Listener. We do the message unboxing as we write it out.
    			MyIGCMessage messageUni = unisource.AcceptMessage();
    			Echo("Unicast received from address " + messageUni.Source.ToString() + "\n\r");
    			Echo("Tag: " + messageUni.Tag + "\n\r");
    			Echo("Data: " + messageUni.Data.ToString());
    		}
    
    		// To unicast a message to our friend, we need an address for his Programmable Block.
    		// We'll pretend here that he has copied it and sent it to us via Steam chat.
    		long friendAddress = 3672132753819237;
    
    		// Here, we'll use the tag to convey information about what we're sending to our friend.
    		string tagUni = "Int";
    
    		// We're sending a number instead of a string.
    		int number = 1337;
    
    		// We access the unicast method through IGC and input our address, tag and data.
    		IGC.SendUnicastMessage(friendAddress, tagUni, number);
    	}
    }
    Receiving unicasts is very similar to broadcasts, except that we only have a pre-made Listener. Sending them is a bit more interesting though. Remember that a Broadcast Listener only "listens" on a specific tag. If you don't have the tag your friend is broadcasting on, you can't receive his messages. However, the Unicast Listener listens on all tags. You might think that makes tags superfluous for unicasts. But they can actually be useful, as we see in the example above. Since we are allowed to send our message data as type object, we can pack a lot of different things in messages. E.g, you could send a vector, an array, a list, etc. Since unicasts don't need a specific tag to get through, we can use the tag to tell the receiver what type of data we've sent. That makes it easier for him to unbox it. Above, our tag read "Int". Our friend could easily make his script interpret that as data needing conversion to the Int32 type.

    You could of course do this for broadcasts as well, and have a listener for every type of data you want to send. In a multiplayer scenario however, someone might be listening in if they figure that out. There's something to think about :)

    Finally, a few words about the unicast addresses. You can obtain your own address through the IGC.Me property. It returns your address as long. You could then print it in the Custom Data of your Programmable Block using IMyProgrammableBlock.CustomData, and send it to friends over Steam chat or something. A neater way of going about things is to extract addresses from messages. Remember that the MyIGCMessage.Source property contains the address for whoever sent a message. So if you and a friend want to set up unicast communication, you could start with sending a single broadcast to him. He would then extract your address from the message, and send a unicast back to it. Finally, you extract his address from his unicast message.

    That concludes this tutorial. I hope you found it informative. There are some more stuff to go into like transmission distance, but this should be enough to get you started. Remember, use Malware's API index! It's a great tool to find all the methods and properties you might need.

    Please let me know of any errors you find or suggestions you might have. Good luck!
     
    Last edited: Mar 10, 2019
    • Like Like x 3
  2. Neotician Apprentice Engineer

    Messages:
    440
    Awesome stuff! I was just having to use antennae for communication for the first time and got the message about the methods in existing guides being deprecated, your timing couldn't be better!
    I got communication up and running easily after reading your tutorial, so thanks a ton!

    A couple of things tho:
    The game does not like me declaring the Data as an "object", it'll throw an exeption saying "..Message type System.Object is not allowed! at..", it works fine if i define it as a string though, may just be me misunderstanding, that you meant object as any object, and not object itself.

    As for max length of the waiting queue, i found that listener.MaxWaitingMessages returns the max queue length, eg.
    Code:
    Echo("Max msg: " + listeners[0].MaxWaitingMessages.ToString());
    Lastly, you defined programmable block as an antenna :)
     
    • Like Like x 1
  3. Paxroy Trainee Engineer

    Messages:
    4
    Glad I could help! :)

    You're right, I've updated the example code according to your input. Thanks man! I actually didn't box my message data as object in my testing. Just assumed it would work I guess, #badengineer :p

    Also found another testing error I made. The Listener property HasPendingMessage actually works OK, and returns true only when you have "unread" messages. So no need to set up callback for accepting messages.

    EDIT: Callback is still sort of useful. Not that checking HasPendingMessages every run is very expensive, even for multiple listeners, but it's just neater to have a specific trigger for checking messages.
     
    Last edited: Mar 10, 2019
  4. cheerkin Trainee Engineer

    Messages:
    62
    Thank you for your guide!
    Today I found that old antenna method was marked obsolete. Is there any TL;DR for that changes? Any benefits apart from having typed argument and callbacks?
    Are there particular "hardware" requirements for connection? What happens if I try send multicast with laser antenna onboard, or no antennas at all?

    Better just use property Me (if you meant the PB containing this code).
     
  5. Paxroy Trainee Engineer

    Messages:
    4
    Happy to help!

    Yeah, it was marked obsolete at the moment of the update. I tested it right after launch though and it still worked. Haven't checked recently though, no idea if Keen in general keeps obsolete functionality operational or not.

    Not sure what you mean by typed argument, but overall the new system just provides so much more functionality and increases performance. For functionality, being able to send any data type (string, arrays, vectors, etc) is much simpler since you don't have to write so much data-to-string and string-to-data conversion code, which is a huge relief, at least for me personally (Regex patterns aren't my cup of tea). The Listener and Tag functionalities also help with this. Then you have the Unicast option, which just makes more sense for single target transmissions, and also helps with performance.

    The only hardware needed is a programmable block and an antenna on both ends. As for laser antennas, I have actually never used one in my entire Space Engineers life, so I'm not sure. But it seems very likely that a "broadcast" between laser antennas only reaches each other. There's still a difference from sending a Unicast, since you don't need the sources/addresses for the PBs.


    You're right! Just a habit of mine from learning to write C# code in SE.
     
  6. Inflex Developer Staff

    Messages:
    397
    Hello, it's nice to see ppl sharing their knowledge like this. Nicely summed up, +1 for that.
    Even tho your concepts are mostly correct, there is few things I'd like to point out, so you and others can improve:

    This observation is incorrect. A message once accepted is removed from the queue and never returned again. This means that once you accept all messages there are in the queue so that none is left, the `HasPendingMessage` will start returning `false` (as you correctly observed) and the subsequent messages returned from the `AcceptMessage` will be mere dummies. This means that all the fields will be null or 0 respectively. You won't get any valid data, definitely not old messages.

    The new IGC system is built up in a way that you don't need to worry about the actual HW requirements. You take care of the program logic and the data processing and the IGC system will take care of the data delivery by any means available, physical connection, radio connection, laser connection, radio-connected chain of friendly engineer suits, robo-wolfs howling morse code behind the horizon, you name it (combinations allowed). As long as there is any viable data path the message will be delivered. To check this use `IsEndpointReachable` method (https://github.com/malware-dev/MDK-...ergridCommunicationSystem.IsEndpointReachable).

    And one more. `RegisterBroadcastListener` method also returns the broadcast listener for given tag. This means that you can save it directly without need to query all registered listeners. As, as the documentation states:
    "In case there is already another active broadcast lister with given tag new listener is NOT registered and the already active one is returned instead."
    This means that if you know the tag of a listener you're looking for, you can just call `RegisterBroadcastListener` again and the existing listeners will be returned to you over and over again.

    And finally, this one is more for newcomers, here you can find few documented examples to help you started:
    https://github.com/InflexCZE/IGC_ShowCase/tree/master/IGC_ShowCase

    I hope this clarified a few things for you. If you have any further questions, feel free to ask. I'll check this tread again soon:tm:
    Also, for any fast questions you might have, feel free to join us on our official KSH Discord server. There is a lot of ppl ready to help any time.
     
  7. cheerkin Trainee Engineer

    Messages:
    62
    Although the TData is quite constrained. From current game assembly I can see that it's either primitive type or one of ingame-whitelisted.

    Would be nice if we could have some clean DTO wrappers. I often need to pass a set of vectors or matrices, and sending a separate messages does not feel very right.

    Code:
    private static bool IsPrimitiveOfSafeStruct(Type type)
    {
    	if (type.IsPrimitive)
    	{
    		return true;
    	}
    	if (!(type == typeof(string)) && !(type == typeof(Ray)) && !(type == typeof(RayD)) && !(type == typeof(Line)) && !(type == typeof(LineD)) && !(type == typeof(Color)) && !(type == typeof(Plane)) && !(type == typeof(Point)) && !(type == typeof(PlaneD)) && !(type == typeof(MyQuad)) && !(type == typeof(Matrix)) && !(type == typeof(MatrixD)) && !(type == typeof(MatrixI)) && !(type == typeof(MyQuadD)) && !(type == typeof(Capsule)) && !(type == typeof(Vector2)) && !(type == typeof(Vector3)) && !(type == typeof(Vector4)) && !(type == typeof(CapsuleD)) && !(type == typeof(Vector2D)) && !(type == typeof(Vector2B)) && !(type == typeof(Vector3L)) && !(type == typeof(Vector4D)) && !(type == typeof(Vector3D)) && !(type == typeof(MyShort4)) && !(type == typeof(MyBounds)) && !(type == typeof(Vector3B)) && !(type == typeof(Vector3S)) && !(type == typeof(Vector2I)) && !(type == typeof(Vector4I)) && !(type == typeof(CubeFace)) && !(type == typeof(Vector3I)) && !(type == typeof(Matrix3x3)) && !(type == typeof(MyUShort4)) && !(type == typeof(Rectangle)) && !(type == typeof(Quaternion)) && !(type == typeof(RectangleF)) && !(type == typeof(BoundingBox)) && !(type == typeof(QuaternionD)) && !(type == typeof(MyTransform)) && !(type == typeof(BoundingBox2)) && !(type == typeof(BoundingBoxI)) && !(type == typeof(BoundingBoxD)) && !(type == typeof(MyTransformD)) && !(type == typeof(Vector3UByte)) && !(type == typeof(CurveTangent)) && !(type == typeof(Vector4UByte)) && !(type == typeof(BoundingBox2I)) && !(type == typeof(BoundingBox2D)) && !(type == typeof(Vector3Ushort)) && !(type == typeof(CurveLoopType)) && !(type == typeof(BoundingSphere)) && !(type == typeof(BoundingSphereD)) && !(type == typeof(ContainmentType)) && !(type == typeof(CurveContinuity)) && !(type == typeof(MyBlockOrientation)) && !(type == typeof(Base6Directions.Axis)) && !(type == typeof(MyOrientedBoundingBox)) && !(type == typeof(PlaneIntersectionType)) && !(type == typeof(MyOrientedBoundingBoxD)) && !(type == typeof(Vector3I_RangeIterator)) && !(type == typeof(Base6Directions.Direction)) && !(type == typeof(Base27Directions.Direction)) && !(type == typeof(CompressedPositionOrientation)) && !(type == typeof(Base6Directions.DirectionFlags)) && !(type == typeof(HalfVector3)) && !(type == typeof(HalfVector2)))
    	{
    		return type == typeof(HalfVector4);
    	}
    	return true;
    }
    --- Automerge ---
    Okay, found out that we can use immutable collections

    Code:
    // Sandbox.Game.SessionComponents.MyIGCSystemSessionComponent.MessageTypeChecker<TMessageType>
    private static bool IsImmutableCollection(Type type)
    {
    	if (!(type == typeof(System.Collections.Immutable.ImmutableArray<>)) && !(type == typeof(ImmutableList)) && !(type == typeof(ImmutableQueue)) && !(type == typeof(ImmutableStack)) && !(type == typeof(ImmutableHashSet)) && !(type == typeof(ImmutableSortedSet)) && !(type == typeof(ImmutableDictionary)))
    	{
    		return type == typeof(ImmutableSortedDictionary);
    	}
    	return true;
    }
    
    To use them in IDE we need to get System.Collections.Immutable package by Microsoft. No need to fully qualify name, namespace is handled in-game.
    --- Automerge ---
    ImmutableDictionary can't really be used, unfortunately, as it is a static class which creates non-whitelisted generic variant ImmutableDictionary<,>

    Malware, Inflex, am I doing it wrong?
    ImmutableArray<> is cool, but ImmutableDictionary would've been awesome.
    The said problem is shared across all whitelisted Immutable Collections.
     
  8. Martin R Wolfe Trainee Engineer

    Messages:
    81
    As a work around for ImmutableDictionary<,> you could use ImmutableArray<KeyValuePair<TKey, TValue>> to sent the dictionary via IGC. The pain being you will have to extract the key,value pairs from the from the Immutable at the receiver and add them to a local dictionary. It has to be this way as one of the requiered parts of the extension method ToDictionary<TKey,TElement,T>(ImmutableArray<T>, Func<T,TKey>, Func<T,TElement>, IEqualityComparer<TKey>) the EqualityComparer<TKey> is not white listed ( well in mytests not for EqualityComparer<string> when I put in null to use the default comparer.