Try   HackMD

A Detailed Guide to the Emulator

Overview

The Emulator is a simulation environment designed to compile and run C programs.

This document provides details on the communication protocol used between the Emulator and your C program.

Terms

Emulator: The application which simulates your code.
User Program: The C program you and your software groupmates will be writing. It may consist of multiple files, possibly within subfolders.
Command: An instruction to the Emulator.
Component: Any motor, sensor, or mechanism. Each component has a unique ID in the emulator.
GM: Grabbing Mechanism.
TM: Throwing Mechanism.
Turn: A cycle of information exchange (I/O) between the Emulator and User Program. Equivalent to one iteration of loop().

General Flow

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

At the beginning, the Emulator sends an ACK signal to the User Program. Then the User Program is allowed to perform a setup procedure.

  1. [Emulator] -> [User Program] ACK
  2. [User Program] -> [Emulator] Setup (INIT, CONFIG)
  3. [User Program] -> [Emulator] Finish Setup (STOPINIT)

Once setup is finished, the User Program and Emulator communicate in turns. Within one turn, the following occurs:

  1. [User Program] (Optional) Read Data
  2. [User Program] (Optional) Logic and Calculations
  3. [User Program] -> [Emulator] Send Commands
    • Write Data (SET),
    • Request Data (REQUEST, REQUESTVAR), and/or
    • Actions (WAIT, GRAB, THROW)
  4. [Emulator] Process Commands
  5. [Emulator] -> [User Program] Return Requested Data

Note that simulation is kept separate from communication, in a separate thread.

The concept of requesting data and reading data is clarified in the following section.

Demo Mode

This feature was introduced in v1.3.0.

This section introduces the new UI.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. A new switch has been added. Use this to toggle between Debug Mode (the original UI) and Demo Mode.
  2. A table replaces debugging information. This is where important game events will be logged (e.g. when points are gained).
    • Timestamps are formatted as mm:ss.zzz (minutes, seconds, milliseconds) followed by the number of turns after the slash. One turn is defined as one input/output cycle between the Emulator and your program.
    • Table tennis balls placed in baskets only score points if the basket containing them is deposited on the rack.
    • If your team has achieved the End Game condition, it will be logged as a separate event.
  3. Some events may have long messages. Hover your mouse over its description to see a box pop up with the full sentence.
  4. Your team's points are shown here.
  5. Seeding functionality and the reset button are disabled in Demo Mode.
    • The Run button (▶️) will automatically refresh the seed and reset the game state.
  6. The version number has been moved to the bottom right corner, in case you were wondering.

Starter Kit

You may download the starter kit here.

The starter kit contains a compact library for Emulator I/O. It also abstracts away the main function (similar to Arduino), so that you don't need to bother yourself with the most basic level of program flow.

You are not allowed to modify certain files (main.c, emulator.h, emulator,c) but are allowed to make your own functions to substitute emwrite or emread-series, for example. You are also allowed to create additional .h and .c files to better organise your code.

emwrite and emread-series Functions

The most basic Emulator I/O functions. They are, however, not necessarily "good" implementations. Here are some examples of using them:

Example: Writing Output

emwrite(1, 5); // Initialise the camera.
emwrite(1, 100); // Initialise the motors.
emwrite(1, 101); 
emwrite(2, 5, 0, 20, 0.0);  // Position the camera at an offset of (0, 20) cm and with no angular offset.

emwrite(3, 20, 0, 24.0); // Set wheel motor 0 voltage to 24V.
emwrite(3, 21, 0, 12.0); // Set wheel motor 1 voltage to 12V.
emwrite(4, 5); // Request camera input on the next iteration.

emwrite(10, 0); // Release the grabber.
emwrite(10, 1); // Grab a tennis ball.

emwrite(11); // Throw the ball.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Pay attention to the value of the types you pass into emwrite. Accidentally passing an int instead of a float can cause undefined behaviour. Read the Commands section to learn about what types to pass.

Example: Reading Input

Note that you need to request for input first, before being able to read the input. You'll only be able to read input on the next turn, after requesting input.

The input will be sent in the same order as the requests.

// Read RGB565 camera data into a 32x32 buffer.
BMP* bmp;
if (!emread_camera(&bmp))
{
    emdebug("Failed to read camera data!\n");
    return;
}
else
{
    // Process image...
    BMP_Free(bmp);  // Free bmp after usage.
}

// Read 1-bit data from a line-tracking sensor.
bool is_line;
if (emread_line_sensor(&id, &is_line))
{
    emdebug("Read Line Sensor (id=%d): %s\n", id, (is_line ? "true" : "false"));
}

Example: Requesting Data

bool is_data_requested = false;

void loop()
{
    // On the first turn, is_data_requested will be 
    // false, so this block wouldn't be executed.
    // But on the second turn and after, is_data_requested
    // will be true, so we can safely read the data.
    if (is_data_requested)
    {
        int id;
        bool is_line;
        if (emread_line_sensor(&id, &is_line))
            emdebug("Line sensor: %s\n", is_line ? "true" : "false");
    }
    
    // Request data for a line sensor.
    emwrite(4, 10);
    // The data WON'T be immediately returned.
    // It will only be available on the next turn.
    
    is_data_requested = true;
}

Emulator Commands

The user program communicates to the Emulator by printing a series of commands to standard output. These are described in detail below.

Component IDs are listed at the end.

WAIT

Placeholder command. The rest of the commands in the current batch will be ignored.

Code: 0
Format: WAIT
Parameters: (none)

INIT

Initialises a component (i.e. motor, sensor, or mechanism) with a given id.

You should initialise components at the start of your program in the setup() function.

Code: 1
Type: Any
Format: INIT id
Parameters:

  • id: int. The id of the component to initialise.

CONFIG

Places a component at an offset relative to the centre of the robot.

You should configure components at the start of your program in the setup() function.

Code: 2
Type: Sensor, Mechanism, Robot
Format: CONFIG id x y angle
Parameters:

  • id: int. The id of the component to configure. Passing 0 would configure the robot.
  • x: int. The x offset, in centimetres.
  • y: int. The y offset, in centimetres.
  • angle: float. The angular offset, in radians.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Configuring the robot itself (id = 0) will reset all components. So it is suggested that you CONFIG the robot first before initialising your desired components.

The robot is configured by taking its centre relative to the field. For example, configuring the robot to (0, 0,

π) will position the robot's centre at the top left of the field, facing south. The default configuration of the robot is (50, 50,
π2
).

Components, on the other hand, are configured at an offset relative to the robot's centre. Attempting to configure components at an offset outside the robot will result in a "Configuration out of range" error.

CONFIGCAM

This command was introduced in v1.1.0.

A specialised configuration command to set different camera attributes.

You should configure the camera at the start of your program in the setup() function.

Code: 13
Type: Camera
Format: CONFIGCAM attr value
Parameters:

  • attr: int. The camera's attribute-id to configure.
  • value: float. A value, appropriate to the specified attribute.
attribute-id attribute min max
0 Height (metres) 0 2.0
1 Angle of Depression (radians) 1.25π/8 2.95π/8

SET

Code: 3
Type: Motors
Format: SET id attr value
Parameters:

  • id: int. The id of the motor to set.
  • attr: int. The motor attribute to set.
  • value: double. The value of the attribute to set.
# attribute value
0 Motor Voltage min: -24.0V, max: +24.0V

REQUEST

Code: 4
Type: Sensors
Format: REQUEST id
Parameters:

  • id: int. The id of the sensor to request data from. Data from requests will be returned in the same order they were requested. Be sure to use the appropriate emread function.

REQUESTVAR

Code: 5
Format: REQUESTVAR var
Parameters:

  • var: int. The variable to request.
# variable
10 GrabCode → int[3]

Since v1.2.0, requesting GrabCode will now buffer three ints from standard input, one int for each grab slot. If a slot is unoccupied, a 0 is buffered. These are not necessarily aligned. The ints will be pushed forward as objects are released/thrown.

A new helper function, emread_grab_code(), has been provided. It works similarly to emread_int() except it receives an int[3] instead of an int*.

GrabCode The object currently grabbed
0 Nothing
1 Tennis Ball
2 Table-Tennis Ball
3 Basket

GRAB

Code: 10
Format: GRAB target
Parameters:

  • target: int. The target to grab. Only targets 1–3 and 9–11 are meaningful. Targets 0 and 8 have been deprecated since v1.2.0.
    Note that targets 8–11 will be simulated as if the grabber was at a higher spot. This is only useful for placing (see PLACE).
    To clarify, for grabbing, only 1–3 should be used. For placing, 1–3 and 9–11 should be used.
# Low / High target
0 / 8 None (void)
1 / 9 Tennis
2 / 10 Table-Tennis
3 / 11 Basket

PLACE

This command was introduced in v1.2.0.

Code: 11
Format: PLACE target
Parameters:

  • target: int. The target to grab. Values are the same as those in the GRAB command.

TMLOAD

This command was introduced in v1.2.0.

Code: 20
Format: TMLOAD strength
Parameters:

  • strength: float. How much power to load the TM with. Valid range is [0.0, 24.0]. The higher the value, the farther the ball will "fly".

This command loads the TM, after which it cannot be interacted with for 2 turns. On the third turn after, the TM will be ready to fire (with TMTHROW).

For the command to succeed, it has to be called in the correct state (when the TM is idle) and a tennis ball has to be available from the GM.

See Throwing Mechanism for more details.

TMTHROW

This command was introduced in v1.2.0.

Code: 21
Format: TMTHROW
Parameters: (none)

This activates the TM and launches a tennis ball. The ball should have been loaded using the TMLOAD command. After a throw, the TM will take 1 turn to rest and can only be loaded on the turn after.

See Throwing Mechanism for more details.

Components

In this context, a component is a motor, sensor, or mechanism. Each component has an ID.

The sections below further elaborate on each component.

Components IDs and Overview

The following table lists the IDs of the available components and their corresponding kind and type.

ID Kind Type Colour
5 Sensor Camera Dark Green
6-9 Sensor Magnetic Yellow
10-14 Sensor Line Light Blue
15-19 Sensor IR Dark Blue (Light Green since v1.2)
100-101 Motor Wheel Black
200 Mechanism Grabbing Magenta
201 Mechanism Throwing -

Camera

Read Format: CAMERA id
Parameters:

  • id: int. The id of the camera. This will always be 5.

The camera uses a 2D perspective, only flat image data is simulated.

When requested, image data will be saved to camera.bmp, in the same folder as the application executable. In macOS, the image will be inside the app bundle, in Contents/macOS/.

Note that some graphics drawn on the window are not copied to the image (e.g. the magnetic lines).

Magnetic Sensors

Read Format: MAGNETIC id value
Parameters:

  • id: int. The id of the sensor associated with the following data.
  • value: int. A value in the range 0 to 255, indicating the strength of the magnetic field detected. [v1.0.2+]

For the Emulator, the angular configuration of the magnetic sensors will not affect the output value.

Line Sensors

Read Format: LINE id value
Parameters:

  • id: int. The id of the sensor associated with the following data.
  • value: bool. 0 or 1. Indicating whether the sensor detects a black surface.

IR Sensors

Read Format: IR id value
Parameters:

  • id: int. The id of the sensor associated with the following data.
  • value: bool. 0 or 1. Indicating whether an obstacle can be detected in front of the IR.

Grabbing Mechanism

In the Emulator, the retrieve baskets are fixed to the ground.

The GM allows grabbing of multiple objects. Here is a brief description of what it can and cannot do.

It can:

  • Grab up to 3 balls (from any combination of Tennis balls and Ping-Pong balls),
  • Grab up to 1 basket, and
  • Expend grabbed balls in any order (implementation defined).

But it cannot:

  • Grab basket(s) and balls at the same instance,
  • Grab more than 3 balls
  • Grab more than 1 basket

Usage

Grabbing can be done by repeatedly using the GRAB and PLACE commands. However, the Emulator will emulate only one of these each turn.

For example, if your team would like to grab 2 ping-pong balls, you would call emwrite(10, 2) twice across two iterations of loop(). Similarly, to place 2 ping-pong balls, you would call emwrite(12, 2) twice across two iterations of loop().

Throwing Mechanism

This mechanism was introduced in v1.2.0.

The TM operates in 3 states: Idle, Load, or Shoot. The TM may only be loaded (TMLOAD) in the Idle state. It can only be shot (TMTHROW) in the Load state.

Like the GM, the Emulator will emulate only one of either TMLOAD or TMTHROW each turn. Both commands need some extra turns to buffer. TMLOAD requires 2 turns to load. On the third turn after, TMTHROW can be called. TMTHROW requires 1 turn to reset.

The following table shows an optimal throwing sequence. Waiting extra turns before loading/throwing is allowed.

Turn Throw Action
100 TMLOAD
101 -
102 -
103 TMTHROW
104 -
105 TMLOAD
106 -

For your reference, this is what the Tennis Ball looks like when thrown:

Seeding

This feature was introduced in v1.1.0.

What dis?

To select the chosen basket or to generate motor/sensor noise, our Emulator will be using random number generators. RNGs are great, but suppose we want to replicate certain random situations, then we would need to repeat the same sequence of numbers.

We can do this by using a pseudo-random number generator (PRNG), which generates numbers based on a seed. The sequence of numbers generated will be the same for a given seed.

You can use this feature to replicate and debug situations where your robot failed or where improvements could be made.

Usage

By default, the seed is 0. You can input your own seed if you wish or press the refresh-seed button (↺) to let the Emulator generate one randomly. The seed should be a 32-bit unsigned int.

Every time you press the reset button (⏪), the PRNG will reseed itself with the given seed (or 0 if none was provided).

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Note that using the same seed does not guarantee that two simulations will be perfectly replicated. Other factors in play (e.g. physics) cannot be controlled by the seed.

Error Messages

  • "Timeout after XXXms of inactivity.": To put a time constraint, the Emulator limits each turn to at most 500ms. If your program does not output anything within 500ms of the previous output, then emulation will stop.
  • "Parse error!": Error parsing your output. If you're using emwrite(), this shouldn't be a problem as formatting is handled.
  • "Not enough arguments!": Insufficient arguments for a given command.
  • "Component not found!": A component with the given id could not be found.
  • "Component not enabled!": A component with the given id was found, but wasn't enabled.
  • "This command can only be emitted from the setup() function.": Whatever command this message is referring to, don't call it from loop().
  • "This command can only be emitted from the loop() function.": Whatever command this message is referring to, don't call it from setup().
  • "GM was already used this turn.": You're attempting to use a GM command twice in one turn.
  • "TM was already used this turn.": You're attempting to use a TM command twice in one turn.
  • "Configuration out of range!": Emitted when trying to configure a component outside the body of the robot.
  • "Unknown option encountered!": Emitted when an unknown option is passed (e.g. to CONFIGCAM).
  • "Bad sequencing.": When the TM was not expecting a state (e.g. loading when already loaded, throwing when not loaded).
  • "No tennis balls.": When the TM cannot load any tennis balls.
  • "TM still loading.": When you attempt to throw while the TM is loading.

More Examples

Example: Delaying

There is no actual delay function in the Emulator. It will timeout if no output is provided within 500ms. If, for some reason, you'd like to delay for—say 3 seconds—you'd need to WAIT across multiple turns.

This example uses clock_t and clock() from the time.h library. clock_t is an integral type (i.e. similar to int and long) used to store units of clock time. We use CLOCKS_PER_SEC to convert between clock time and seconds.

const uint64_t msec_to_wait = 10000;    // Amount of time to wait.

static int iter = 0;
static int finished = 0;

void loop(void)
{
    static clock_t start = 0;

    if (start == 0)
    {
        // Set a reference point with the current clock time.
        start = clock();
    }

    // Convert ms to clock time.
    const uint64_t clocktime_to_wait = msec_to_wait * (CLOCKS_PER_SEC / 1000);

    // Retrieve the current clock time and compare it with `start`.
    // If the elapsed time exceeds our target wait time, then we're finished waiting.
    const clock_t now = clock();
    if (now - start > clocktime_to_wait)
    {
        // Finished waiting.
        // Do stuff...
        printf("Finished!\n");
        finished = 1;
    }
    else
    {
        emwrite(0); // Send a WAIT command, so that the Emulator is aware that our 
                    // program is still alive.
    }
}

Note that the delay wouldn't be exact.

FAQ