Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Hello World

This sample is the simplest possible starting point for creating a \psi application. The application pipeline in this sample contains a single Timer component that generates a message each second, and a single Do() operator that prints "Hello World" to the console for each message generated by the timer, along with the originating time of the message.

Ensuring that this sample builds and runs correctly is a good way to verify that your development environment has been setup correctly.

Setting up the project

To build \psi applications, we recommend using Visual Studio 2022 on Windows (the free, Community Edition is sufficient). Under Linux, we recommend using Visual Studio Code. We will build this sample application using the available \psi Nuget packages.

Steps for Windows

Follow these steps to set up the Visual Studio project on Windows:

  1. First, create a simple .NET console app by launching Visual Studio and clicking "Create a new project" (or if you already have a solution open in Visual Studio, go to File -> New -> Project). Select "Console App (C#)," name your project, and select the Target Framework (e.g. .NET 8.0).

  2. Add a reference to the Microsoft.Psi.Runtime NuGet package that contains the core Platform for Situated Intelligence infrastructure. You can do so by right-clicking on the project in the Solution Explorer (or clicking "Project" in the menu bar at the top) and selecting Manage NuGet Packages, then go to Browse. Make sure the Include prerelease checkbox is checked, as \psi packages are still in beta, pre-release versions. Type in Microsoft.Psi.Runtime and install.

Steps for Linux

In Linux, we will use the dotnet command-line tool to create the initial project and Program.cs, and add the Microsoft.Psi.Runtime NuGet package that contains the core Platform for Situated Intelligence infrastructure:

$ dotnet new console -n HelloWorld && cd HelloWorld
$ dotnet add package Microsoft.Psi.Runtime --version=0.19.100.1-beta

Note that due to an issue with NuGet, you'll need to specify the exact version of the NuGet package you wish to install, otherwise you will get a really old version. It's advised to specify the latest version that we have released, which can be found on: https://www.nuget.org/.

If there are problems in this step, check that dotnet-sdk is installed. Follow the instructions here: https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu. Once fully set up with the NuGet packages, open the source folder in Visual Studio Code.

Creating a pipeline

Let's begin writing the code by adding the necessary using clauses at the top:

using System;
using Microsoft.Psi;

A \psi application consists of a pipeline (computation graph) that contains a set of components (nodes in the graph), connected via time-aware streams of data (edges in the graph). Let's start by creating the pipeline object, which is responsible for starting and stopping the components, for running them concurrently, and for coordinating message passing between them. To create the pipeline, we use the Pipeline.Create factory method, like below:

public static void Main()
{
    // Create a pipeline
    var p = Pipeline.Create();
}

Adding a source component

Now let's construct a source component to generate some messages in this pipeline. We'll create a Timer component with the Timers.Timer factory method. In this case, the first parameter in the call is the pipeline object p (a common pattern in \psi when instantiating components inside pipelines) and the second parameter is the time interval to use when generating messages, in this case 1 second.

public static void Main()
{
    // Create a pipeline
    var p = Pipeline.Create();

    // Create a timer component that produces a message every second
    var timer = Timers.Timer(p, TimeSpan.FromSeconds(1));
}

You may notice that the timer variable we've just created has the type IProducer<TimeSpan>. This object type indicates that the component emits a stream of TimeSpan messages, in this case. In general, streams are of a generic type (you can have a stream of any type T) and are strongly typed, and therefore the connections between components are statically checked.

A critical aspect of all \psi streams is that they are time-aware: timing information is carried along with the data on each stream, enabling both the developer and the runtime to correctly reason about the timing of the data. This time awareness enables proper synchronization and data fusion. Specifically, the way this works is that each message emitted by source components -- in this case the Timer, but would also apply to sensors like cameras and microphones -- is timestamped with an originating time that captures when the data carried by the message occured in the real world.

Adding a stream operator

We will next apply a stream operator called Do() to the output stream of our timer component, which executes a function for each message posted on the stream. In this case, the function to apply to each message is specified inline, via an anonymous delegate with arguments (t, e), where t is the message payload and e is the message envelope. In this example, we will ignore t and simply use the originating time specified in e to print a timestamped message to console for each message (note that \psi messages are timestamped in UTC):

public static void Main()
{
    // Create a pipeline
    var p = Pipeline.Create();

    // Create a timer component that produces a message every second
    var timer = Timers.Timer(p, TimeSpan.FromSeconds(1));

    // For each message created by the timer, print "Hello world!"
    // along with the message's originating time.
    timer.Do((t, e) =>
    {
        Console.WriteLine($"{e.OriginatingTime}: Hello world!");
    });
}

Under the covers, each stream operator is backed by a component: in this case, the Do() extension method creates a Do component that subscribes to the Timer component. In reality, the pipeline we have just constructed looks like this:

Example pipeline

Running and disposing the pipeline

Next we need to execute the pipeline, which can be done in a few different ways. Calling p.Run() will tell the pipeline to execute until all components have completed sending messages. Running this pipeline will cause the timer component to start generating messages, which in turn are processed by the Do operators. In this case, since the timer component will emit an infinite number of messages, the application would continue to run indefinitely until the application was killed by the user. Instead, we will use the asynchronous, non-blocking p.RunAsync() command, which will start executing the pipeline and then immediately return. We will then wait for the user to strike any key, upon which we gracefully shutdown the pipeline using p.Dispose():

public static void Main()
{
    // Create a pipeline
    var p = Pipeline.Create();

    // Create a timer component that produces a message every second
    var timer = Timers.Timer(p, TimeSpan.FromSeconds(1));

    // For each message created by the timer, print "Hello world!"
    // along with the message's originating time.
    timer.Do((t, e) =>
    {
        Console.WriteLine($"{e.OriginatingTime}: Hello world!");
    });

    // Run the pipeline, but don't block here
    p.RunAsync();

    // Wait for the user to hit a key before closing the pipeline
    Console.ReadKey();

    // Close the pipeline
    p.Dispose();
}

Try it out! Running this application should result in a "Hello World" message being printed to the console every second. Striking any key will close the application.

Running program on Linux

To run the program on Linux, make sure to save the program and navigate to the source folder on the terminal. Use the following command:

$ dotnet run

To run the program from Visual Studio Code, install the C# for Visual Studio Code extension. From the tool bar, choose "Run without Debugging." This should automatically produce a launch.json file.

Navigate to the launch.json file and change the console type to integratedTerminal.

"console": "integratedTerminal"

Run the program by dropping down from the Run option in the toolbar and choosing Run without Debugging.

Next steps

For a more thorough introduction to \psi, including topics on synchronization, data persistence and replay, and visualization, please see the Brief Introduction. For some slightly more complex (but still quite small) samples involving capturing and processing video and audio data from webcams and microphones, please refer to the cross-platform SimpleVAD sample and the WebcamWithAudio samples (Windows and Linux versions).