Gregory Pierce
Published © MIT

Stellar Cartography

Stellar Cartography is a way for teachers to deliver information about the solar system in a fun and compelling way.

IntermediateShowcase (no instructions)8 hours635
Stellar Cartography

Things used in this project

Hardware components

Echo Dot
Amazon Alexa Echo Dot
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
Unity
Unity
AWS SQS (Simple Queue Service)
Amazon Web Services AWS SQS (Simple Queue Service)
AWS Lambda
Amazon Web Services AWS Lambda

Story

Read more

Schematics

Simple VUI documentation

Code

Unity3D SQS Bridge

C#
This is a modification of the original SQS code that will allow polling from the SQS infrastructure and convert the command received from the Lambda into a RemoteCommand (custom class)
//
// Copyright 2014-2015 Amazon.com, 
// Inc. or its affiliates. All Rights Reserved.
// 
// Licensed under the Amazon Software License (the "License"). 
// You may not use this file except in compliance with the 
// License. A copy of the License is located at
// 
//     http://aws.amazon.com/asl/
// 
// or in the "license" file accompanying this file. This file is 
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
// CONDITIONS OF ANY KIND, express or implied. See the License 
// for the specific language governing permissions and 
// limitations under the License.
//
using UnityEngine;
using System.Collections;
using Amazon;
using Amazon.Runtime;
using Amazon.CognitoIdentity;
using Amazon.SQS;
using UnityEngine.UI;

namespace AWSSDK.Examples
{
	public class SQSExample : MonoBehaviour
	{

		Camera currentCamera;
		public string target;

		//identity pool id for cognito credentials

		//changeThis
		private string IdentityPoolId = "us-east-1:cae1b4dc-33bb-43fc-8dcc-561ecea34a7a";

		public string CognitoIdentityRegion = RegionEndpoint.USEast1.SystemName;

		private RegionEndpoint _CognitoIdentityRegion
		{
			get { return RegionEndpoint.GetBySystemName(CognitoIdentityRegion); }
		}

		public string SQSRegion = RegionEndpoint.USEast1.SystemName;

		private RegionEndpoint _SQSRegion
		{
			get { return RegionEndpoint.GetBySystemName(SQSRegion); }
		}


		//name of the queue you want to create

		//changeThis
		private string QueueName = "Alexa-UnityCloudBridge";

		private AWSCredentials _credentials;

		private AWSCredentials Credentials
		{
			get
			{
				if (_credentials == null)
					_credentials = new CognitoAWSCredentials(IdentityPoolId, _CognitoIdentityRegion);
				return _credentials;
			}
		}

		private IAmazonSQS _sqsClient;

		private IAmazonSQS SqsClient
		{
			get
			{
				if (_sqsClient == null)
					_sqsClient = new AmazonSQSClient(Credentials, _SQSRegion);
				return _sqsClient;
			}
		}

		public Button CreateQueue;
		public Button SendMessage;
		public Button RetrieveMessage;
		public Button DeleteQueue;
		public InputField Message;

		//changeThis
		private string queueUrl = "PICK YOUR OWN QUEUE URL";

		// Use this for initialization
		void Start()
		{
			UnityInitializer.AttachToGameObject(this.gameObject);
			//CreateQueue.onClick.AddListener(CreateQueueListener);
			//SendMessage.onClick.AddListener(SendMessageListener);
			//RetrieveMessage.onClick.AddListener(RetrieveMessageListener);
			//DeleteQueue.onClick.AddListener(DeleteQueueListener);


			StartCoroutine(RepeatRetrieveMessage(0.1F));
		}

		private void CreateQueueListener()
		{
			//            SqsClient.CreateQueueAsync(QueueName, (result) =>
			//            {
			//                if (result.Exception == null)
			//                {
			//                    Debug.Log(@"Queue Created");
			//                    queueUrl = result.Response.QueueUrl;
			//                }
			//                else
			//                {
			//                    Debug.LogException(result.Exception);
			//                }
			//            });
		}

		private void DeleteQueueListener()
		{
			//            if (!string.IsNullOrEmpty(queueUrl))
			//            {
			//                SqsClient.DeleteQueueAsync(queueUrl, (result) =>
			//                {
			//                    if (result.Exception == null)
			//                    {
			//                       Debug.Log(@"Queue Deleted");
			//                    }
			//                    else
			//                    {
			//                        Debug.LogException(result.Exception);
			//                    }
			//                });
			//            }
			//            else
			//            {
			//                Debug.Log(@"Queue Url is empty, make sure that the queue is created first");
			//            }
		}

		private void SendMessageListener()
		{

			if (!string.IsNullOrEmpty(queueUrl))
			{
				var message = Message.text;
				if (string.IsNullOrEmpty(message))
				{
					Debug.Log("No Message to send");
					return;
				}

				SqsClient.SendMessageAsync(queueUrl, message, (result) =>
					{
						if (result.Exception == null)
						{
							Debug.Log("Message Sent");
						}
						else
						{
							Debug.LogException(result.Exception);
						}
					});
			}
			else
			{
				Debug.Log(@"Queue Url is empty, make sure that the queue is created first");
			}
		}



		IEnumerator RepeatRetrieveMessage(float waitTime) {
			bool checkSQS = true;
			while (checkSQS) 
			{
				yield return new WaitForSeconds(waitTime);

				if (!string.IsNullOrEmpty (queueUrl)) {
					SqsClient.ReceiveMessageAsync (queueUrl, (result) => {
						if (result.Exception == null) {
							//Read the message
							var messages = result.Response.Messages;
							messages.ForEach (m => {
								Debug.Log (@"Message Id  = " + m.MessageId);
								Debug.Log (@"Mesage = " + m.Body);
								Debug.Log (@"Recreate command from " + m.Body );
								
                // Turn the JSON Received into a RemoteCommand
								RemoteCommand command = RemoteCommand.CreateFromJSON( m.Body );

								target = command.target.ToLower();

								Debug.Log(@"Target " + target );

								GameObject myCameraObj = GameObject.Find( "Camera " + target );

								Debug.Log(@"Found camera " + myCameraObj.name );
								if (myCameraObj != null )
								{
									if ( currentCamera != null )
									{
										currentCamera.enabled = false;
									}

									Camera myCamera = myCameraObj.GetComponent<Camera>();
									myCamera.enabled = true;

									currentCamera = myCamera;

									Debug.Log(@"Current camera set to " + currentCamera.name );
								}
								else
								{
									Debug.Log(@"Unable to find a camera for target " + target );
								}

								Debug.Log(@"Done switching camera");

								//Delete the message
								var delRequest = new Amazon.SQS.Model.DeleteMessageRequest {
									QueueUrl = queueUrl,
									ReceiptHandle = m.ReceiptHandle

								};

								SqsClient.DeleteMessageAsync (delRequest, (delResult) => {
									if (delResult.Exception == null) {
									} else {
									}
								});


								Debug.Log(@"Done processing message ");

							});

						} else {
							Debug.Log( result.Exception );
							Debug.LogException (result.Exception);
						}


					});
				} else {
					Debug.Log (@"Queue Url is empty, make sure that the queue is created first");
				}

				//Debug.Log (".");
			}
		}


		private void RetrieveMessageListener()
		{
			StartCoroutine(RepeatRetrieveMessage(0.1F));


		}

	}

}

Lambda handler source

Python
This is the Lambda function which receives the request from ASK and performs the quick lookup for facts or translates the intent into a remotecommand that is sent to the remote Unity device
from __future__ import print_function

import json
import urllib
import boto3


print('Loading function')

QUEUE_URL = 'USE YOUR OWN QUEUE URL'

print ('Configured for queue ' + QUEUE_URL )

sqs = boto3.client('sqs')

queue = {}

planets = { "mercury": "Mercury is the smallest and innermost planet in the Solar System. It makes one trip around the Sun once every 87.969 days. Even though Mercury is the closest planet to the Sun, it is not the warmest - that would be Venus. This is because it has no greenhouse effect, so any heat that the Sun gives to it quickly escapes into space.",
            "venus": "Venus is the second planet from the Sun. Astronomers have known Venus for thousands of years. The ancient Romans named it after their goddess Venus. Venus is the brightest thing in the night sky except for the Moon. It is sometimes called the morning star or the evening star as it is easily seen just before the sun comes up in the morning, and just after the sun goes down in the evening. ",
            "earth": "Earth is the planet we live on. It is the third planet from the sun. It is the only planet known to have life on it. Earth formed around 4.5 billion years ago. Earth is the only planet in our solar system that has a large amount of liquid water. About 71% of the surface of Earth is covered by oceans. Because of this, it is sometimes called the blue planet",
            "moon": "The Moon also known as luna is Earth's satellite, and we can usually see it in the night sky. Our moon is about a quarter the size of the Earth. Because it is far away it looks small. The gravity on the moon is one-sixth of the Earth's gravity. It means that something will be six times lighter on the Moon than on Earth. ",
            "mars": "Mars is the fourth planet from the Sun and the second-smallest planet in the Solar System, after Mercury. Named after the Roman god of war, it is often referred to as the Red Planet because the iron oxide prevalent on its surface gives it a reddish appearance. Mars is a terrestrial planet with a thin atmosphere, having surface features reminiscent both of the impact craters of the Moon and the valleys, deserts, and polar ice caps of Earth.",
            "jupiter": "Jupiter is the largest planet in the Solar System. It is the fifth planet from the Sun. Jupiter is classified as a gas giant, both because it is so large and due to the fact that it is made up mostly of gas. Jupiter has a mass of about 318 Earths. This is twice the mass of all the other planets in the Solar System put together. ",
            "saturn": "Saturn is the sixth planet from the Sun in the Solar System. It is the second largest planet in the Solar System, after Jupiter. Saturns most well known feature is its rings. These rings are made of ice with smaller amounts of rocks and dust.",
            "uranus": "Uranus is the seventh planet from the Sun in the Solar System. It is a gas giant. It is the third largest planet in the solar system. The planet is tilted on its axis so much that it is sideways.",
            "neptune": "Neptune is the eighth and last planet from the Sun in the Solar System. It is a gas giant. It is the fourth largest planet and third heaviest. Neptune has four rings which are hard to see from the Earth. It is 17 times heavier than Earth and is a little bit heavier than Uranus. It was named after the Roman God of the Sea.",
            "pluto": "Pluto is a dwarf planet in the Solar System. Its formal name is 134340 Pluto. The dwarf planet is the ninth-largest body that moves around the Sun. At first, Pluto was called a planet. Now, it is just the largest body in the Kuiper belt. Kinda sad.",
            "solar system": "The Solar System is the Sun and all the objects in orbit around it. The Sun is orbited by planets, asteroids, comets and other things. There are eight planets in the Solar System. From closest to farthest from the Sun, they are Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus and Neptune. Pluto is also included because I like Pluto."
            
            }


def lambda_handler(event, context):
    """ Route the incoming request based on type (LaunchRequest, IntentRequest,
    etc.) The JSON body of the request is provided in the event parameter.
    """
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])
          
    print(context)

    """
    Uncomment this if statement and populate with your skill's application ID to
    prevent someone else from configuring a skill that sends requests to this
    function.
    """
    # if (event['session']['application']['applicationId'] !=
    #         "amzn1.echo-sdk-ams.app.[unique-value-here]"):
    #     raise ValueError("Invalid Application ID")

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']},
                           event['session'])

    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'], event['session'])
    elif event['request']['type'] == "SessionEndedRequest":
        return on_session_ended(event['request'], event['session'])


def on_session_started(session_started_request, session):
    """ Called when the session starts """

    print("on_session_started requestId=" + session_started_request['requestId']
          + ", sessionId=" + session['sessionId'])


def on_launch(launch_request, session):
    """ Called when the user launches the skill without specifying what they
    want
    """

    print("on_launch requestId=" + launch_request['requestId'] +
          ", sessionId=" + session['sessionId'])
    # Dispatch to your skill's launch
    return get_welcome_response()


def on_intent(intent_request, session):
    """ Called when the user specifies an intent for this skill """

    print("on_intent requestId=" + intent_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']

    if intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    elif intent_name == "AMAZON.CancelIntent":
        return get_cancel_response()
    elif intent_name == "displayObjectIntent":
        print("Display object intent")
        return display_stellar_object(intent, session)
    elif intent_name == "describeObjectIntent":
        print("Describe object intent")
        return describe_stellar_object(intent, session)
    elif intent_name == "orbitObjectIntent":
        print("Orbit object intent")
        return orbit_stellar_object(intent, session)
    else:
        raise ValueError("Invalid intent")



def on_session_ended(session_ended_request, session):
    """ Called when the user ends the session.

    Is not called when the skill returns should_end_session=true
    """
    print("on_session_ended requestId=" + session_ended_request['requestId'] +
          ", sessionId=" + session['sessionId'])
    # add cleanup logic here

# --------------- Functions that control the skill's behavior ------------------


def get_welcome_response():
    """ If we wanted to initialize the session to have some attributes we could
    add those here
    """

    session_attributes = {}
    card_title = "Stellar Cartography"
    speech_output = "Welcome to Stellar Cartography. " \
                    "You can ask me for information about any of the planets in the solar system, " 
  
    # If the user either does not reply to the welcome message or says something
    # that is not understood, they will be prompted again with this text.
    reprompt_text = "You can ask me more about the planets or the solar system by saying, " \
                    "display the planet Earth."
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))




# --------------- Helpers that build all of the responses ----------------------


def build_speechlet_response(title, output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'card': {
            'type': 'Simple',
            'title': 'SessionSpeechlet - ' + title,
            'content': 'SessionSpeechlet - ' + output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }

def generate_cancel_response(intent, session):

    print("generating cancel request " )

    session_attributes = {}
    card_title = ""
    speech_output = "Cancelling session. "

    # If the user either does not reply to the welcome message or says something
    # that is not understood, they will be prompted again with this text.
    reprompt_text = ""
                    
    should_end_session = True
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))

def display_stellar_object(intent, session):
    print("generating cartography request " )
    
    targetPlanet = intent['slots']['planet']['value']

    print("Sending request to display " + targetPlanet )

    message = "{\"command\": \"DISPLAY\", \"target\": \"" + targetPlanet + "\" }"
    response = sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=message,
        DelaySeconds=0,
    )

    session_attributes = {}
    card_title = targetPlanet + " Details"
    speech_output = "Understood. Shifting perspective to view target object " + targetPlanet

    # If the user either does not reply to the welcome message or says something
    # that is not understood, they will be prompted again with this text.
    reprompt_text = "You can as me to display another planet or ask me for details about this planet by saying, " \
                    "display the planet Earth or tell me more about this planet."
                    
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))
        
def describe_stellar_object( intent, session ):
    print("describing stellar object ")
    
    targetObject = intent['slots']['planet']['value']
    print("Describing the object " + targetObject )

    session_attributes = {}
    card_title = targetObject + " Orbit"
    speech_output = planets[targetObject.lower()]

    # If the user either does not reply to the welcome message or says something
    # that is not understood, they will be prompted again with this text.
    reprompt_text = "You can as me to display another planet or ask me for details about this planet by saying, " \
                    "display the planet Earth or tell me more about this planet."
                    
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))
    
def orbit_stellar_object( intent, session ):
    print("orbiting stellar object ")

    targetObject = intent['slots']['planet']['value']
    
    print("Sending request to orbit " + targetObject )

    message = "{\"command\": \"ORBIT\", \"target\": \"" + targetPlanet + "\" }"
    response = sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=message,
        DelaySeconds=0,
    )   
    
    session_attributes = {}
    card_title = targetObject + " Orbit"
    speech_output = "Entering orbit around the target " + targetObject

    # If the user either does not reply to the welcome message or says something
    # that is not understood, they will be prompted again with this text.
    reprompt_text = "You can as me to display another planet or ask me for details about this planet by saying, " \
                    "display the planet Earth or tell me more about this planet."
                    
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))        

def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }

Intent Schema

JavaScript
This is the base intent schema
{  
  "intents": [  
    { "intent": "describeObjectIntent", "slots": [{ "name": "planet", "type": "LIST_OF_PLANETS" }] }, 
    { "intent": "displayObjectIntent", "slots": [{ "name": "planet", "type": "LIST_OF_PLANETS" }] }, 
    { "intent": "orbitObjectIntent", "slots": [{ "name": "planet", "type": "LIST_OF_PLANETS" }] },   
    { "intent": "AMAZON.StopIntent", "slots": []},
    { "intent": "AMAZON.YesIntent", "slots": [] },  
    { "intent": "AMAZON.NoIntent", "slots": [] },  
    { "intent": "AMAZON.HelpIntent", "slots": [] },  
    { "intent": "AMAZON.RepeatIntent", "slots": [] }
  ]  
}

Unity RemoteCommand

C#
Takes a custom JSON command structure from SQS and turns it into a RemoteCommand object that can be used to translate the remote Alexa intent
using UnityEngine;
using System.Collections;

[System.Serializable]
public class RemoteCommand 
{
	public string command;
	public string target;

	public static RemoteCommand CreateFromJSON(string jsonString)
	{
		RemoteCommand cmd = null;

		try{
	 		cmd = JsonUtility.FromJson<RemoteCommand> (jsonString);
		}
		catch (System.Exception e ) {
			Debug.Log (@"Something went wrong " + e);
		}

		return cmd;
	}
}

Sample Uterances

Plain text
These are the utterances that are used to determine which intent should be invoked in the lambda and thus remotely on the Unity device
describeObjectIntent tell me about the planet {planet}
describeObjectIntent tell me about {planet}
describeObjectIntent tell me about the {planet}

displayObjectIntent show me the planet {planet}
displayObjectIntent show me {planet}
displayObjectIntent show me the {planet}

orbitObjectIntent enter orbit around {planet}
orbitObjectIntent enter orbit around the {planet}
orbitObjectIntent enter orbit around the planet {planet}
orbitObjectIntent orbit around {planet}
orbitObjectIntent orbit around the {planet}
orbitObjectIntent orbit around the planet {planet}
orbitObjectIntent orbit {planet}
orbitObjectIntent orbit the {planet}
orbitObjectIntent orbit around {planet}
orbitObjectIntent orbit around the {planet}
orbitObjectIntent orbit planet {planet}

Credits

Gregory Pierce

Gregory Pierce

1 project • 1 follower
Thanks to Unity Technologies, Paul Custinger, and AWS Alexa SA group.

Comments

Add projectSign up / Login