Language: C++

Introduction

SimpleAgent is a class that encapsulates the regular COSMOS agents. While COSMOS agents can be difficult to use, SimpleAgent is designed with the user in mind. Here we'll be referring to SimpleAgent as an agent -- a SimpleAgent really is just a COSMOS agent; only the way you use it changes.

SimpleAgent Devices

A SimpleAgent device is a C++ object which can hold certain properties. The types of properties a device can have depend on the type of device. For example, a Battery device has a capacity property, but you can bet a TemperatureSensor device doesn't have a capacity property.

Using SimpleAgent

Now that we've gotten definitions out of the way, we can start by creating a SimpleAgent.

Creating the SimpleAgent

First, you'll need to include the header file with all of the SimpleAgent definitions:

#include "utility/SimpleAgent.h"

Next, you'll need to create a new SimpleAgent object in the main function (or elsewhere, as long as you create it before you use it):

SimpleAgent *agent = new SimpleAgent("my_agent");

Here, we've created a new agent with the name my_agent. Make sure you never create more than one SimpleAgent per program, as this will raise an exception.

We can then set how often our agent will perform its tasks by using the SetLoopPeriod function:

agent->SetLoopPeriod(1.5)

The line of code above will set our agent to run at 1.5 second intervals. It's okay if your agent takes longer than this time to perform its tasks. This only really matters if your agent takes less than this time: the agent will wait until the appropriate time has passed before starting the next loop iteration.

Adding Devices

Now that the agent is created, we can start loading it up with devices. You can have as many devices as you'd like, but in the example below we'll just add one TemperatureSensor device with the name my_sensor:

TemperatureSensor *my_sensor = agent->NewDevice<TemperatureSensor>("my_sensor");

Next we can define properties we would like to post. You don't actually need to post any properties unless you want to access them externally.

mysensor->Post(my_sensor->utc = Time::Now());
mysensor->Post(my_sensor->temperature);

The first line above assigns the current time to the sensor's utc property and posts it all in one go. You don't need to assign the value of a property immediately (although setting an initial value is recommended). The second line above demonstrates this by posting an uninitialized temperature property.

You can also set custom properties if your device doesn't already have the properties you want, as shown in the example below. Unfortunately due to limitations of COSMOS, these custom properties cannot currently be posted 😢.

my_sensor->SetCustomProperty<bool>("is_initialized", true);
// ...
bool is_initialized = my_sensor->GetCustomProperty<bool>("is_initialized");

If you find that a device you'd like to use isn't supported, you can always use the CustomDevice device type. This device only supports a few post-able properties, but you can use as many custom properties as you wish.

Setting Node Properties

For properties about the COSMOS node that don't fit into a device, you can use node properties:

agent->SetNodeProperty<Node::UTC>(Time::Now());
Time utc = agent->GetNodeProperty<Node::UTC>();

Finalizing Properties

Once you are done adding devices, device properties, and node properties, you should finalize the agent. This just means that any properties that are posted will get handled properly. You can still set, get, or add properties as usual, but posting any new properties won't be handled properly unless you finalize the agent again. Finalizing the agent is just a simple function call:

agent->Finalize();

Viewing COSMOS Names

The names used internally by COSMOS are required for certain operations, such as for displaying properties in COSMOS Web or for making requests to other agents. You can see all of the names COSMOS uses (as well as previously-added requests) in SimpleAgent's convenient DebugPrint function:

agent->DebugPrint()

The Main Loop

The main loop of an agent is where all the agent's tasks are performed. The example below shows how the main loop should look:

while ( agent->StartLoop() ) {
    // Perform tasks here...
}

The StartLoop function handles any waiting that may be necessary (e.g. if your agent performs its tasks faster than the time set by SetLoopPeriod), and whether or not the agent needs to shut down. After this loop you should probably wrap up any necessary hardware interaction, and also delete the SimpleAgent you've created:

delete agent;

Using Requests

Adding Requests

A request function can take on one of two signatures (i.e. the form of the function):

  1. string MyRequestWithNoArguments()
  2. string MyRequestWithArguments(vector<string> arguments)

The first request takes no arguments, and returns a string indicating the response. The second request takes a vector of arguments and returns a string indicating the response. If you decide you need arguments in your request, for safety you should check that the number arguments and their types are correct.

You can add either type of request to the agent with the same function, and (optionally) provide a synopsis and detailed description of the request:

agent->AddRequest("myrequest_1", MyRequestWithNoArguments, "A synopsis", "A detailed description");
agent->AddRequest("myrequest_2", MyRequestWithArguments);

The first line above adds the MyRequestWithNoArguments request using the name myrequest_1, and provides a synopsis and detailed description of the request. The second line adds the MyRequestWithArguments request using the name myrequest_2, and provides no synopsis or detailed description. Although providing the synopsis and detailed description strings is optional, it is good practice to use these.

You can add the same request under a variety of names (called request aliases) as long as one of these names isn't already taken by another request. There's actually a shorthand for this:

agent->AddRequest({"alias_1", "alias_2", "alias_3"}, MyRequestWithArguments, "A synopsis", "A detailed description");

This will add our request function with the aliases alias_1, alias_2, and alias_3. You can add as many aliases as you wish and, as before, you can choose to omit the synopsis and detailed description strings.

Calling a Request From the Terminal

Once your agent is running, you can call a previously-added request from a terminal. For example, if we've added a request called my_request to an agent called my_agent running on the node cubesat, we can run the following command to call this request (optionally providing arguments argument1 and argument2, which get redirected to the request function):

$ agent cubesat my_agent my_request argument1 argument2

This will call the request function and print the string returned from the request.

To view a listing of all the requests available for the agent, you can run the following command:

$ agent cubesat my_agent

This will display all available requests in a nicely formatted table, along with their synopses and detailed descriptions if provided. Here is an example of the output from agent_cpu, a built-in agent:

    help 
            list of available requests for this agent

    help_json 
            list of available requests for this agent (but in json)

    shutdown 
            request to shutdown this agent

    idle 
            request to transition this agent to idle state

    init 
            request to transition this agent to init state

    monitor 
            request to transition this agent to monitor state

    run 
            request to transition this agent to run state

    status 
            request the status of this agent

    (clipped)

Communicating With Other Agents

When working with many agents, you will often need to send requests back and forth between agents running in separate programs. SimpleAgent provides a friendly way of doing this using the class RemoteAgent.

To find another agent you can use SimpleAgent's FindAgent function:

static RemoteAgent other_agent = agent->FindAgent("other_agent", "cubesat")

In the code above, we attempt to find the agent named other_agent running on the node cubesat. You don't need to provide the node name; if the node name is omitted, then the node that our agent is running on will be used by default.

After calling the FindAgent function, you should check whether or not the remote agent was found:

if ( other_agent.Connect() ) {
  // Do something here that depends on the other agent...
}

For safety, you should place any code that depends on the other agent existing inside of the if block as shown above.

Getting Properties from Another Agent

After connecting to another agent, you can get retrieve its properties. To do this, you will need to know the unique COSMOS name of the property (we're working on making this easier). You can get the COSMOS name using the agent's DebugPrint function or built-in print request. In the example below, we retrieve the properties with COSMOS names device_batt_temp_000 and device_imu_temp_000 from another agent:

auto values = other_agent.GetCOSMOSValues({"device_batt_temp_000", "device_imu_temp_000"});

This function returns an unordered_map<string, Json::Value> object, but we don't really care about this, so we can use the auto keyword to let the compiler infer which type we're using. We can get the actual values from the values variable using the COSMOS names:

float batt_temp = values["device_batt_temp_000"].nvalue;
float imu_temp = values["device_imu_temp_000"].nvalue;

The nvalue member stands for number value, which is actually a double type. You can also use the svalue member for string values.

It's also a good idea to check whether the properties you want were actually returned by the GetCOSMOSValues function:

float batt_temp = 0; // Set a fallback value in case the property isn't present
if ( values.find("device_batt_temp_000") != values.end() )
    batt_temp = values["device_batt_temp_000"].nvalue;

// Do something with batt_temp...

Sending Requests

After connecting to another agent, you can also send requests to it. Besides being able to call SimpleAgent requests, you can also call the default COSMOS requests. In the example below we send a request to another agent:

string response = other_agent.SendRequest("request_name", "argument_1", "argument_2");

The code above asks other_agent to execute a request with the name request_name, providing it with arguments argument_1 and argument_2. The response of the request is stored into the variable response. You can provide as many arguments as you'd like, but you should make sure that they are string values (we're working on adding support for more types).

After calling a request, you should check whether the request was performed successfully by checking if the response string is empty:

// Check if an error occurred
if ( response.empty() ) {
    // Handle the error
}
else {
    // Do something with the response string
    cout << "Got a response from the other agent: " << response << endl;
}

Putting it all together

Example 1

Here is a working example of an agent program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "utility/SimpleAgent.h"

using namespace std;
using namespace cubesat;

// A simple request that gives a friendly greeting
string SayHi() {
    printf("Hey there! (inside agent)\n");
    return "Hey there! (outside agent)";
}

int main() {
    SimpleAgent *agent = new SimpleAgent("my_agent"); // Create the SimpleAgent
    agent->SetLoopPeriod(2); // Set the agent to run at 2 second intervals
    
    // Create a temperature sensor
    TemperatureSensor *my_sensor = agent->NewDevice<TemperatureSensor>("my_sensor");
    my_sensor->Post(my_sensor->utc = Time::Now()); // Set and post the UTC timestamp property
    my_sensor->Post(my_sensor->temperature = 0); // Set and post the temperature property
    
    // Add a request that can be called externally
    agent->AddRequest("say_hi", SayHi, "Gives you a friendly greeting");
    
    // Finish setting up the SimpleAgent
    agent->Finalize(); // Let the agent know we're done posting properties
    agent->DebugPrint(); // Print out all of the properties and requests
    
    int counter = 0;
    
    // Here comes the main loop...
    while ( agent->StartLoop() ) {
        
        // Update values in the sensor. Since these values were posted,
        // the new values can be viewed externally
        my_sensor->utc = Time::Now(); // Set the timestamp to the current time
        my_sensor->temperature = counter; // Set the temperature to a counter value
        
        // Increment the counter (in place of actually
        // doing stuff with hardware)
        ++counter;
    }
    
    // Free any memory associated with the SimpleAgent
    delete agent;
    
    return 0;
}

Here is the initial output of the agent program above:

Devices
|       | Device 'my_sensor'
|       |       | Posted Properties
|       |       |       | Temperature (aka device_tsen_temp_005)
|       |       |       | UTC (aka device_tsen_utc_005)
Requests
|       | Request 'getproperty'
|       | Request 'print'
|       | Request 'say_hi'
Node Properties
|       | Property 'UTC' (aka node_utc): 59053.871956

This output is all due to calling the DebugPrint function. All of the available COSMOS names are listed next to the property names (for example, device_tsen_temp_005). The say_hi request is also listed in this tree.

We can call the say_hi request externally from a terminal window by running:

$ agent cubesat my_agent say_hi

After running this command, the SimpleAgent will output Hey there! (inside agent), and the output from the terminal window is

{"output": "Hey there! (outside agent)","status": "OK"}

This is a JSON-formatted string, and the actual output from the request is located under the "output" field.

See Also


 


Tags: