Skip to main content

Software Overview

The IGVC 2026 software operates on a few different principles, namely

  • Subsystems | Similar to FRC, Subsystems are a way to make separate but interconnected systems that perform different tasks. For example, we may have multiple different hardware subsystems that interact with different physical devices (e.g. camera, controller, canbus). Or, we may have a set of vision subsystems that handle tasks like hsv transformations, YOLO, depth mapping, etc. Each of these control how things are ran, whether shared tasks (essentially threads) are used, etc.
  • Messages | The Messages folder is a set of automatically generated code that we use to communicate between the simulator, the core, and the frontend. These generated classes allow us to ensure that the content of our messages will always be the same between languages.

There are more principles than this found in each sub project (the core, the simulator, the frontend), but the ones listed are shared between all of the projects.

The Core

The Core, something that can surely be named better, is the code that handles all of the real autonomy and hardware integration. It can be broken down into a few major parts, although these are still evolving as we work on this new codebase.

Messages

Because the Messages system is used throughout the codebase, lets quickly go over that. The Messages are defined by flatbuffers (found in igvc_flatbuffers folder) and then compiled using the generate_local.sh script. Whenever these messages are sent over the network we use a special MessageWrapper class which just stores the data from the flatbuffer as a list of bytes and the type of message (e.g. ImageFrame, Metric, CommandReq/CommandAck, etc). Specifics for this can also be found in the Arc and Simulator documentation.

In order to actually construct these Messages, there is a helper class MessageConstructor which can build them for you. Converting them from their class forms to a MessageWrapper is a bit involved and is currently being worked on, ask Dylan for more information on that or look around the codebase for now.

Subsystems

As mentioned above, subsystems allow us to create a bunch of separate systems that can communicate with each other whilst remaining on separate threads. A barebones subsystem with a few overrides might look something like this

using igvc_csharp.Core;
namespace igvc_csharp.Subsystems.Hardware;

[Subsystem("RealsenseSubsystem", DependsOn = [], Disabled = false)]
public class RealsenseSubsystem : SubsystemBase
{
    public override Task Init(CancellationToken token)
    {
        return Task.CompletedTask;
    }

    public override Task Restart()
    {
        return Task.CompletedTask;
    }

    public override Task Shutdown()
    {
        return Task.CompletedTask;
    }
}

There are a few things to note. First, the class itself that you create (in this instance RealsenseSubsystem) must extend the SubsystemBase class in order to be created. Subsystems are automatically detected and initialied by a SubsystemManager class on program initialization. Second, you will notice a special attribute on the class, the Subsystem attribute. This defines a few helpers like the name for logging purposes, whether or not the subsystem is enabled (only checked on program initialization), and what other subsystems this one depends on. If your subsystem depends on another subsystem, and that subsystem was not created, it will not be created and a warning will be thrown.

Within the subsystem itself there are a few methods that you get access to.

  • Init() - The Init function is called whenever the robot itself is initialized and then initializes all subsystems in the order they were created (dependency order). A CancellationToken is passed for all task/threading purposes, you must use this cancellation token (or a child of it) whenever creating tasks so that everything shuts down properly.
  • Restart() - The Restart function is called whenever a subsystem is required to restart. This is typically called from the frontend, and most of the time your implementation should just be calling shutdown and then calling init. It is safe to ignore this if desired.
  • Shutdown() - As the name implies, this is called whenever the subsystem needs to shutdown. Your CancellationToken provided in the Init() method will be cancelled before this method is called, so keep that in mind whenever you are freeing resources and doing similar tasks.

None of the above methods should be blocking method (such that they block the thread they are on). All subsystems share the same thread on the above methods, and thus any long running tasks should have their own tasks created. See other subsystems for examples of this.


A less commonly used feature, but still a very helpful one, is the ability to inject subsystems into your subsystem via dependency injection. An example of this is given below

[Subsystem("ControllerSubsystem", DependsOn = [typeof(CanbusSubsystem)])]
public class ControllerSubsystem (CanbusSubsystem canbus) : SubsystemBase
{
    async Task WriteControllerLoop(CancellationToken token)
    {
        // This function would be called elsewhere
        while (!token.IsCancellationRequested)
        {
            // Uses the injected CanbusSubsystem to call a method
            canbus.WriteFrame(xyz);
        }
    }
}

In this example, we are injecting the CanbusSubsytem (note that it is marked as a dependency for this subsystem) and then using it later on. It can be said that if a subsystem is marked as a dependency and its requested to be injected, it will not be null. However, if it is not marked as a dependency you can not maintain that same logic and thus you should consider it as nullable with the following syntax

[Subsystem("ControllerSubsystem")]
public class ControllerSubsystem (CanbusSubsystem? canbus) : SubsystemBase
{
    async Task WriteControllerLoop(CancellationToken token)
    {
        // This function would be called elsewhere
        while (!token.IsCancellationRequested)
        {
            // Uses the injected CanbusSubsystem to call a method
            // NOTE: We now use the ? operator to conditionally call WriteFrame if canbus is not null
            canbus?.WriteFrame(xyz);
        }
    }
}

Lastly, there is also the concept of the SubsystemState. TODO FILL IN

Events (EventBus)

Events are one of many ways to pass data around the program. This is useful for things you may want to use in multiple areas instead of directly calling another subsystem. All events are passed to the EventBus (think of like a bus electronically) and propagated to subscribers. The SubsystemBase class has a few useful helpers to subscribe to the EventBus. Note that subscriptions should be made in the Init() method of your subsystem. You do not need to do any unsubscribing yourself unless desired, it will happen automatically on shutdown.

All events sent to or are received from the EventBus must implement the IRobotEvent interface, the compiler will not let you send or receive messages if this is not the case. Due to how the lower level functionality of the event bus works, you must also use the record keyboard instead of class (learn more about what record is here). Using the sealed keyword is also recommended but not required, it just prevents other classes from inheriting from it. An example is found below.

public sealed record ConfigChangedEvent(
    string Path,
    object Value
) : IRobotEvent;

Before we go over how to subscribe to events, lets quickly cover publishing events. To do this, all you need to do is access the EventBus instance and then publish a instance of your event.

ConfigChangedEvent ev = new ConfigChangedEvent("test", "test");
EventBus.Instance.Publish(ev);

NOTE: Your class WILL receive your own events that you publish, so please be careful of recursive event loops.

Subscribe

The standard Subscribe<T> function allows you to subscribe to a specific message type with no additional filtering. This is useful if you know that you want all messages of a specific type.

    public override async Task Init(CancellationToken token)
    {
        // Subscribe to PerformanceSampleEvent
        Subscribe<PerformanceSampleEvent>(
            OnPerformanceSampleEvent,
            token
        );
    }

    private Task OnPerformanceSampleEvent(PerformanceSampleEvent e, CancellationToken token)
    {
        return Task.CompletedTask;
    }

SubscribeMessage

The SubscribeMessage<T> function allows you to subscribe to a shared Message (from the Messages folder). Due to semantics and how the compiler works, we cannot magically check for the underlying MessageType so you must also specify that. All this function does is wrap the above Subscribe<MessageWrapper> function and filter based on the type you provide and then cast to the provided data.

    public override async Task Init(CancellationToken token)
    {
        // Subscribe to ImageEvents
        SubscribeMessage<ImageFrame>(
            MessageType.ImageFrame,
            OnImageReceived,
            token
        );
    }

    private Task OnImageReceived(ImageFrame frame, CancellationToken token)
    {
        return Task.CompletedTask;
    }

SubscribeImage

Similar to SubscribeMessage, SubscribeImage will allow you to subscribe to a specific ImageFrame with a specific image identifier. This is useful if say you only want to subscribe to images that have been thresholded. This function is just a wrapper for SubscribeMessage<ImageFrame> that adds an additional check for the image identifier.

    public override async Task Init(CancellationToken token)
    {
        // Subscribe to only hsv_view image identifiers
        SubscribeImage(
            "hsv_view",
            OnImageReceived,
            token
        );
    }

    private Task OnImageReceived(ImageFrame frame, CancellationToken token)
    {
        return Task.CompletedTask;
    }

Arc

Arc, or the "Autonomous Robotics Controller" (I think is what it stood for) is the system that allows our frontend interface and the core robot code to communicate. As noted before, it communicates via the Messages system and similarly has generated classes for all involved languages (C#, Typescript). The ArcSubsystem handles listening to the EventBus to send messages to the frontend as well as listen for messages from the frontend and announce them in terms of Commands.

Commands

Commands are a way for the frontend to communicate to the core to perform some action. This may as simple as shutdown the robot, or as complicated as a calibration suite for the cameras. They have a command id and optional command data. I apparently got rid of this in the most recent version of the codebase so it currently does not exist, but it will soon look like the following.

// Listens for CommandId.RestartRobot
[Command(CommandId.RestartRobot)]
public Task OnRestartCommandTriggered(ArcCommand command, CancellationToken token)
{
  // Do stuff
  return Task.CompletedTask;
}

// Listens for CommandId.SpinRobot and forces a cooldown of 5 seconds (5000 ms).
[Command(CommandId.SpinRobot, Cooldown = TimeSpan.FromMilliseconds(5000)]
public Task OnSpinCommandTriggered(ArcCommand command, CancellationToken token)
{
  // Do stuff
  return Task.CompletedTask;
}

This will also support replying, but not sure how yet.

Metrics

Metrics are a way for us to track things like the utilization of the canbus, how often things are happening, etc. This system is going to get reworked to be a bit more clear on how it works. The idea is that you construct a metric like

// Emits the total bits received and written (so bus utilization) over the last second every 250ms
[Metric(
  "Total Bits Per Second",  // Display Name
  "bits",  // Unit
  Group = "Hardware", // Group 
  Aggregate = MetricAggregate.Sum, // How do we process this data?
  EmitEveryMs = 250, // How often do we emit this data to the eventbus?
  MaxAgeSeconds = 1 // How long do we keep this data?
)]
private PerformanceMetric<double> _bits;

...

_bits.AddSample(bytesWritten * 8);

Chronos

Chronos is a replay system that lets us store all of the information the robot is recording while it is operating (such as camera feeds, gps, hardware data, metrics, etc) and then use custom software to view it later. While not created yet, this system will work similar to the Metric's system and have support for either automatically extracted variables at an interval or manually report data.