`robot.Rws.Controller` gives access to the controller itself, not to the robot program: identity, clock, network, installed options and systems, restart, backups, safety controller and virtual time. Most of these calls work on an IRC5 and on an OmniCore without changing anything in your code.

Some resources only exist on a real controller. When you call them on a RobotStudio virtual controller, the SDK throws an `RwsException` saying that the resource is not implemented, instead of a raw 404.

## Identity and information

`GetInfo` returns a summary of the controller: system time, name, type and level. `GetIdentity` returns the same name plus the controller id and the MAC address of the main network interface.

**C# : ControllerGetIdentity**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class ControllerGetIdentity
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // Overview of the controller: system time, name, type and level
        ControllerInfo info = robot.Rws.Controller.GetInfo();
        Console.WriteLine($"{info.Name} ({info.Type}), level {info.Level}");
        Console.WriteLine($"Controller time (UTC) : {info.SystemTime}");

        // Identity of the controller, with its id and its MAC address
        ControllerIdentity identity = robot.Rws.Controller.GetIdentity();
        Console.WriteLine($"Id : {identity.Id}");
        Console.WriteLine($"MAC address : {identity.MacAddress}");

        // Type tells a real controller from a RobotStudio virtual controller
        bool isVirtual = identity.Type == ControllerType.VirtualController;
        Console.WriteLine($"Virtual controller : {isVirtual}");
        /**/

        /**/
        // Rename the controller. Only a real controller accepts it.
        robot.Rws.Controller.SetIdentity("CELL_01");
        /**/

        robot.Disconnect();
    }
}
```

**Python : ControllerGetIdentity**
```python
from underautomation.abb.abb_controller import AbbController
from underautomation.abb.rws.data.controller_type import ControllerType

robot = AbbController()
robot.connect("192.168.0.1")

##
# Overview of the controller: system time, name, type and level
info = robot.rws.controller.get_info()
print(f"{info.name} ({info.type}), level {info.level}")
print(f"Controller time (UTC) : {info.system_time}")

# Identity of the controller, with its id and its MAC address
identity = robot.rws.controller.get_identity()
print(f"Id : {identity.id}")
print(f"MAC address : {identity.mac_address}")

# The type tells a real controller from a RobotStudio virtual controller
is_virtual = identity.type == ControllerType.VirtualController
print(f"Virtual controller : {is_virtual}")
##

##
# Rename the controller. Only a real controller accepts it.
robot.rws.controller.set_identity("CELL_01")
##

robot.disconnect()
```

`Type` tells a real controller from a virtual one, which is useful before calling a method that needs real hardware.

| `ControllerType`    | Meaning                                                                                                                      |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `RealController`    | A physical IRC5 or OmniCore cabinet                                                                                          |
| `VirtualController` | A controller running in RobotStudio, see [Test with a RobotStudio virtual controller](/abb/documentation/virtual-controller) |
| `Unknown`           | The controller reported a value the SDK does not know                                                                        |

`SetIdentity` renames the controller. It works only on a real controller. The `id` argument is accepted by RWS 1.0 only, an OmniCore may ignore or refuse it.

`GetEnvironmentVariable` reads a controller environment variable such as `$TEMP` or `$HOME`, with or without the leading dollar sign. It gives the real path behind these names, which is handy before writing a file with the [file system service](/abb/documentation/rws-files).

**Methods of ControllerService**
```csharp
// Gets the value of a controller environment variable (synchronous)
string GetEnvironmentVariable(string name);

// Gets the identity of the controller: name, id, type, MAC address and level (synchronous)
ControllerIdentity GetIdentity();

// Gets an overview of the controller resources (synchronous) Contains the current system time, the controller identity and the list of available sub resources.
ControllerInfo GetInfo();

// Sets the identity of the controller (synchronous) Available only on a real controller.
void SetIdentity(string name, string id = null);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

**Members of Rws.Data.ControllerInfo**
```csharp
public class ControllerInfo {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.ControllerInfo" data-throw-if-not-resolved="false"></xref> class
    public ControllerInfo()

    // Indicates whether the controller runs at system level or in bootserver mode
    public ControllerLevel Level { get; set; }

    // Name of the controller
    public string Name { get; set; }

    // Names of the sub resources exposed by the controller ("clock", "identity", "network", ...)
    public string[] Resources { get; set; }

    // Current system time of the controller (UTC), if available
    public DateTime? SystemTime { get; set; }

    // Returns a string representation of this controller information
    public override string ToString()

    // Indicates whether the controller is a real or a virtual controller
    public ControllerType Type { get; set; }
}
```

**Members of Rws.Data.ControllerIdentity**
```csharp
public class ControllerIdentity {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.ControllerIdentity" data-throw-if-not-resolved="false"></xref> class
    public ControllerIdentity()

    // Controller id, available only for a real controller
    public string Id { get; set; }

    // Indicates whether the controller runs at system level or in bootserver mode
    public ControllerLevel Level { get; set; }

    // MAC address of the controller, available only for a real controller
    public string MacAddress { get; set; }

    // Name of the controller
    public string Name { get; set; }

    // Returns a string representation of this controller identity
    public override string ToString()

    // Indicates whether the controller is a real or a virtual controller
    public ControllerType Type { get; set; }
}
```

**Members of Rws.Data.ControllerType**
```csharp
public enum ControllerType {
    // Physical robot controller (RC)
    RealController = 1

    // The controller type could not be determined
    Unknown = 0

    // Virtual controller (VC), for example running in RobotStudio
    VirtualController = 2
}
```

**Members of Rws.Data.ControllerLevel**
```csharp
public enum ControllerLevel {
    // The controller runs the boot application (bootserver mode)
    BootLevel = 2

    // A system is loaded and running (system level)
    SystemLevel = 1

    // The controller level could not be determined
    Unknown = 0
}
```

## Date, time and time server

> **Available on** RWS 1.0 (IRC5) : yes | RWS 2.0 (OmniCore) : yes. Reading a specific time server needs an OmniCore controller

The controller clock is always UTC. `GetClock` returns a UTC `DateTime`, and `SetClock` expects one. The time zone is read and written apart, with the name used by the tz database, for example `Europe/Stockholm`.

**C# : ControllerClock**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class ControllerClock
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // The controller clock is always UTC
        DateTime clock = robot.Rws.Controller.GetClock();
        Console.WriteLine($"Controller time (UTC) : {clock}");

        // Set the clock from the PC time
        robot.Rws.Controller.SetClock(DateTime.UtcNow);

        // Time zone, named as in the tz database
        Console.WriteLine($"Time zone : {robot.Rws.Controller.GetTimeZone()}");
        robot.Rws.Controller.SetTimeZone("Europe/Stockholm");
        /**/

        /**/
        // Time server the controller synchronizes its clock with
        robot.Rws.Controller.SetTimeServer("132.163.4.101");

        TimeServerInfo timeServer = robot.Rws.Controller.GetTimeServer();

        // null when no time server is configured
        if (timeServer != null)
        {
            Console.WriteLine($"{timeServer.Address} answers {timeServer.Time}");
        }
        /**/

        robot.Disconnect();
    }
}
```

**Python : ControllerClock**
```python
from datetime import datetime

from underautomation.abb.abb_controller import AbbController

robot = AbbController()
robot.connect("192.168.0.1")

##
# The controller clock is always UTC
clock = robot.rws.controller.get_clock()
print(f"Controller time (UTC) : {clock}")

# Set the clock from the PC time
robot.rws.controller.set_clock(datetime.utcnow())

# Time zone, named as in the tz database
print(f"Time zone : {robot.rws.controller.get_time_zone()}")
robot.rws.controller.set_time_zone("Europe/Stockholm")
##

##
# Time server the controller synchronizes its clock with
robot.rws.controller.set_time_server("132.163.4.101")

time_server = robot.rws.controller.get_time_server()

# None when no time server is configured
if time_server is not None:
    print(f"{time_server.address} answers {time_server.time}")
##

robot.disconnect()
```

Instead of setting the clock from your application, you can give the controller a time server with `SetTimeServer`. `GetTimeServer` returns `null` when no time server is configured. Passing an IP address to `GetTimeServer` queries one specific server, this needs a connection opened as RWS 2.0. On an RWS 1.0 connection the SDK throws instead of quietly returning the default server.

The clock, the time zone and the time server are not settable on a virtual controller.

**Methods of ControllerService**
```csharp
// Gets the current system time of the controller (synchronous) The time returned by the controller is always UTC.
DateTime GetClock();

// Gets the time server used by the controller to synchronize its clock (synchronous) Available only on a real controller.
TimeServerInfo GetTimeServer(string serverIp = null);

// Gets the time zone used by the controller (synchronous)
string GetTimeZone();

// Sets the system time of the controller (synchronous) The controller clock is always UTC, pass a UTC date and time. Instead of setting the time explicitly, a time server can be configured with SetTimeServer(System.String).
void SetClock(DateTime dateTime);

// Sets the time server used by the controller to synchronize its clock (synchronous) Available only on a real controller.
void SetTimeServer(string timeServer);

// Sets the time zone used by the controller (synchronous) Available only on a real controller.
void SetTimeZone(string timeZone);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

**Members of Rws.Data.TimeServerInfo**
```csharp
public class TimeServerInfo {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.TimeServerInfo" data-throw-if-not-resolved="false"></xref> class
    public TimeServerInfo()

    // Address of the time server
    public string Address { get; set; }

    // Time reported by the time server (UTC), if available.
    // 
    // <p>Only available when connected with version 2.</p>
    public DateTime? Time { get; set; }

    // Returns a string representation of this time server
    public override string ToString()
}
```

## Network

`GetNetworkInterfaces` lists the IP configuration of every network interface of the controller.

**C# : ControllerNetwork**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class ControllerNetwork
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // IP configuration of every network interface of the controller
        NetworkInterfaceItem[] interfaces = robot.Rws.Controller.GetNetworkInterfaces();

        foreach (NetworkInterfaceItem item in interfaces)
        {
            Console.WriteLine($"{item.Port} {item.LogicalName} : {item.Address} / {item.Mask}");
            Console.WriteLine($"   gateway {item.Gateway}, DHCP {item.DhcpEnabled}");
        }
        /**/

        /**/
        // Fixed address on the LAN adapter
        robot.Rws.Controller.SetNetworkConfiguration(NetworkConfigurationMethod.FixIp,
                                                     "192.168.0.10",
                                                     "255.255.255.0",
                                                     "192.168.0.254");

        // Or let a DHCP server give the address
        robot.Rws.Controller.SetNetworkConfiguration(NetworkConfigurationMethod.Dhcp);

        // The new configuration is used after the next restart
        robot.Rws.Controller.Restart(ControllerRestartMode.Restart);
        /**/

        robot.Disconnect();
    }
}
```

**Python : ControllerNetwork**
```python
from underautomation.abb.abb_controller import AbbController
from underautomation.abb.rws.data.controller_restart_mode import ControllerRestartMode
from underautomation.abb.rws.data.network_configuration_method import NetworkConfigurationMethod

robot = AbbController()
robot.connect("192.168.0.1")

##
# IP configuration of every network interface of the controller
interfaces = robot.rws.controller.get_network_interfaces()

for item in interfaces:
    print(f"{item.port} {item.logical_name} : {item.address} / {item.mask}")
    print(f"   gateway {item.gateway}, DHCP {item.dhcp_enabled}")
##

##
# Fixed address on the LAN adapter
robot.rws.controller.set_network_configuration(NetworkConfigurationMethod.FixIp,
                                               "192.168.0.10",
                                               "255.255.255.0",
                                               "192.168.0.254")

# Or let a DHCP server give the address
robot.rws.controller.set_network_configuration(NetworkConfigurationMethod.Dhcp)

# The new configuration is used after the next restart
robot.rws.controller.restart(ControllerRestartMode.Restart)
##

robot.disconnect()
```

`SetNetworkConfiguration` changes the address of the LAN adapter. **This call can cut you off from the robot.** The controller keeps its current address until the next restart, then answers on the new one. If you set a wrong address or a wrong mask, the only way back is the FlexPendant. The connected user needs the UAS grant to write the controller properties.

| `NetworkConfigurationMethod` | Meaning                                                                  |
| ---------------------------- | ------------------------------------------------------------------------ |
| `FixIp`                      | Fixed address. `address` and `mask` are required, `gateway` is optional. |
| `Dhcp`                       | The address is given by a DHCP server                                    |
| `NoIp`                       | The interface gets no address                                            |

Both methods are refused by a virtual controller.

**Methods of ControllerService**
```csharp
// Gets the IP configuration of all network interfaces of the controller (synchronous) Not applicable to a virtual controller.
NetworkInterfaceItem[] GetNetworkInterfaces();

// Sets the IP configuration of the LAN adapter of the controller (synchronous) The controller must be restarted for the change to take effect. Requires the UAS grant UAS_CONTROLLER_PROPERTIES_WRITE. Not supported by a virtual controller.
void SetNetworkConfiguration(NetworkConfigurationMethod method, string address = null, string mask = null, string gateway = null);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

**Members of Rws.Data.NetworkInterfaceItem**
```csharp
public class NetworkInterfaceItem {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.NetworkInterfaceItem" data-throw-if-not-resolved="false"></xref> class
    public NetworkInterfaceItem()

    // IP address of the interface
    public string Address { get; set; }

    // DHCP status of the interface, if reported by the controller
    public bool? DhcpEnabled { get; set; }

    // Default gateway of the interface, if applicable
    public string Gateway { get; set; }

    // Logical name of the interface, for example "WAN", "LAN1" or "SERVICE"
    public string LogicalName { get; set; }

    // Subnet mask of the interface
    public string Mask { get; set; }

    // Network the interface belongs to ("Public", "Private", "Ability", "Drive").
    // 
    // <p>Only available when connected with version 2.</p>
    public string Network { get; set; }

    // Physical port of the interface, for example "X6" or "X23"
    public string Port { get; set; }

    // Primary DNS server of the interface.
    // 
    // <p>Only available when connected with version 2.</p>
    public string PrimaryDns { get; set; }

    // Secondary DNS server of the interface.
    // 
    // <p>Only available when connected with version 2.</p>
    public string SecondaryDns { get; set; }

    // Returns a string representation of this network interface
    public override string ToString()
}
```

**Members of Rws.Data.NetworkConfigurationMethod**
```csharp
public enum NetworkConfigurationMethod {
    // IP address obtained from a DHCP server
    Dhcp = 1

    // Fixed IP address, the address, mask and gateway have to be provided
    FixIp = 0

    // No IP address configured on the adapter
    NoIp = 2
}
```

## Options and installed systems

`HasOption` returns `true` or `false` instead of throwing when the option is missing. The option name is case sensitive, `SAFEMOVEPRO` and not `SafeMovePro`.

**C# : ControllerOptions**
```csharp
using UnderAutomation.ABB;

public class ControllerOptions
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // Is an option installed? The name is case sensitive.
        bool hasSafeMove = robot.Rws.Controller.HasOption("SAFEMOVEPRO");
        Console.WriteLine($"SafeMove Pro : {hasSafeMove}");

        // Systems installed on the controller
        string[] systems = robot.Rws.Controller.GetInstalledSystems();
        Console.WriteLine($"Installed systems : {string.Join(", ", systems)}");
        /**/

        /**/
        // Value of a controller environment variable
        string temp = robot.Rws.Controller.GetEnvironmentVariable("$TEMP");
        Console.WriteLine($"$TEMP is {temp}");

        // Would this RobotWare version run on this hardware?
        bool compatible = robot.Rws.Controller.IsRobotWareVersionCompatible("6.03.0101");
        Console.WriteLine($"Compatible : {compatible}");
        /**/

        robot.Disconnect();
    }
}
```

**Python : ControllerOptions**
```python
from underautomation.abb.abb_controller import AbbController

robot = AbbController()
robot.connect("192.168.0.1")

##
# Is an option installed? The name is case sensitive.
has_safe_move = robot.rws.controller.has_option("SAFEMOVEPRO")
print(f"SafeMove Pro : {has_safe_move}")

# Systems installed on the controller
systems = robot.rws.controller.get_installed_systems()
print("Installed systems : " + ", ".join(systems))
##

##
# Value of a controller environment variable
temp = robot.rws.controller.get_environment_variable("$TEMP")
print(f"$TEMP is {temp}")

# Would this RobotWare version run on this hardware?
compatible = robot.rws.controller.is_robot_ware_version_compatible("6.03.0101")
print(f"Compatible : {compatible}")
##

robot.disconnect()
```

`GetInstalledSystems` returns the names of the systems installed on the controller, and `IsRobotWareVersionCompatible` says whether a given RobotWare version would run on this hardware. Both need a real controller.

`SetLanguage` changes the language the controller writes its messages in, with a code such as `en`, `de` or `sv`. The language must be installed, otherwise the controller answers 400. The same setting is also reachable from the [control panel service](/abb/documentation/rws-panel).

The RobotWare version and the full list of installed options and products are read from the [system service](/abb/documentation/rws-system).

**Methods of ControllerService**
```csharp
// Gets the names of the systems installed on the controller (synchronous)
string[] GetInstalledSystems();

// Verifies whether an option is present on the controller (synchronous) The option name is case sensitive, for example "SAFEMOVEPRO".
bool HasOption(string option);

// Checks whether a RobotWare version is compatible with the controller hardware (synchronous) Supported only on a real controller.
bool IsRobotWareVersionCompatible(string robotWareVersion);

// Sets the language of the controller (synchronous)
void SetLanguage(string language);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

## Restart

**`Restart` stops the robot.** A running RAPID program is interrupted, the motors go off and the controller reboots. Depending on the mode, the RAPID programs or the system settings can also be lost. Do not call it on a production cell without knowing what the mode does.

**C# : ControllerRestart**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class ControllerRestart
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // Warm restart of the controller
        robot.Rws.Controller.Restart(ControllerRestartMode.Restart);

        // The controller closes the connection while it reboots
        robot.Disconnect();
        /**/

        /**/
        // Try to reconnect until the controller answers again
        while (true)
        {
            try
            {
                robot.Connect("192.168.0.1");
                break;
            }
            catch (Exception)
            {
                Thread.Sleep(5000);
            }
        }

        Console.WriteLine(robot.Rws.Panel.GetControllerState());
        /**/

        robot.Disconnect();
    }
}
```

**Python : ControllerRestart**
```python
import time

from underautomation.abb.abb_controller import AbbController
from underautomation.abb.rws.data.controller_restart_mode import ControllerRestartMode

robot = AbbController()
robot.connect("192.168.0.1")

##
# Warm restart of the controller
robot.rws.controller.restart(ControllerRestartMode.Restart)

# The controller closes the connection while it reboots
robot.disconnect()
##

##
# Try to reconnect until the controller answers again
while True:
    try:
        robot.connect("192.168.0.1")
        break
    except Exception:
        time.sleep(5)

print(robot.rws.panel.get_controller_state())
##

robot.disconnect()
```

| `ControllerRestartMode` | What the controller does                                                              |
| ----------------------- | ------------------------------------------------------------------------------------- |
| `Restart`               | Warm restart. The system and the RAPID programs are kept.                             |
| `Shutdown`              | The controller stops and stays off. Someone has to power it on again.                 |
| `IStart`                | The system restarts with its default settings                                         |
| `PStart`                | The system restarts and the RAPID programs are removed                                |
| `BStart`                | The system restarts from the state stored at the last shutdown                        |
| `XStart`                | The controller restarts to the boot application, where another system can be selected |

The request returns as soon as the controller accepts it. The connection is then lost, and every following request fails until the controller is up again. Call `Disconnect`, wait, and connect again.

On an OmniCore, the restart needs the mastership on all domains. The SDK takes it for you, this is what the `useImplicitMastership` argument does. Set it to `false` when you already hold the [mastership](/abb/documentation/rws-mastership). An IRC5 needs no mastership here and ignores the argument.

The [control panel service](/abb/documentation/rws-panel) also has a `Restart` method, with the same modes.

**Methods of ControllerService**
```csharp
// Restarts or shuts down the controller (synchronous)
void Restart(ControllerRestartMode mode, bool useImplicitMastership = true);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

**Members of Rws.Data.ControllerRestartMode**
```csharp
public enum ControllerRestartMode {
    // The controller will be restarted. The last automatically saved system state will be loaded.
    // Should be used to recover from a system crash.
    BStart = 5

    // The controller will be restarted. The current system parameter settings and RAPID programs will be discarded,
    // and the original system installation settings will be used.
    IStart = 3

    // The controller will be restarted. The current RAPID programs and data will be discarded, but not the system parameter settings.
    PStart = 4

    // The controller will be restarted. The state is saved and any changed system parameter settings will be activated after the restart.
    Restart = 0

    // The main computer will be shut down. Should be used if the controller UPS is broken.
    Shutdown = 1

    // The controller will be restarted and the Boot Application will be started. The current system is saved and deactivated
    // (the controller is non-functional, for advanced maintenance only).
    XStart = 2
}
```

## Backup and restore

A backup is a folder written by the controller on its own file system. Creating one is asynchronous: `CreateBackup` returns as soon as the controller accepts the request, and you follow the progress with `GetBackupState`.

**C# : BackupCreate**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class BackupCreate
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // The controller creates the backup in the background, this call returns immediately
        robot.Rws.Controller.CreateBackup("$temp/mybackup");

        // Poll the state until the controller is done
        BackupState state = robot.Rws.Controller.GetBackupState();

        while (state == BackupState.BackupInProgress)
        {
            Thread.Sleep(1000);
            state = robot.Rws.Controller.GetBackupState();
        }

        if (state != BackupState.BackupReady)
        {
            Console.WriteLine($"The backup failed : {state}");
            return;
        }
        /**/

        /**/
        // What the backup contains
        BackupSystemInfo backup = robot.Rws.Controller.GetBackupInfo("$temp/mybackup");
        Console.WriteLine($"{backup.SystemName}, RobotWare {backup.RobotWareVersion}");
        Console.WriteLine($"{backup.OptionCount} option(s) : {string.Join(", ", backup.Options)}");
        /**/

        robot.Disconnect();
    }
}
```

**Python : BackupCreate**
```python
import time

from underautomation.abb.abb_controller import AbbController
from underautomation.abb.rws.data.backup_state import BackupState

robot = AbbController()
robot.connect("192.168.0.1")

##
# The controller creates the backup in the background, this call returns immediately
robot.rws.controller.create_backup("$temp/mybackup")

# Poll the state until the controller is done
state = robot.rws.controller.get_backup_state()

while state == BackupState.BackupInProgress:
    time.sleep(1)
    state = robot.rws.controller.get_backup_state()

if state != BackupState.BackupReady:
    print(f"The backup failed : {state}")
    raise SystemExit(1)
##

##
# What the backup contains
backup = robot.rws.controller.get_backup_info("$temp/mybackup")
print(f"{backup.system_name}, RobotWare {backup.robot_ware_version}")
print("Options : " + ", ".join(backup.options))
##

robot.disconnect()
```

The destination path must be on the controller file system. Environment variables are allowed, `$temp/mybackup` or `~temp/mybackup` both work. The folder must not exist yet, and it cannot be created under `$HOME`. Creating a backup can stop the RAPID execution, so do not do it in the middle of a production cycle. The connected user needs the backup grant.

| `BackupState`                             | Meaning                              |
| ----------------------------------------- | ------------------------------------ |
| `BackupInProgress`                        | The controller is writing the backup |
| `BackupReady`                             | The last backup finished correctly   |
| `ErrorDuringBackup`                       | The last backup failed               |
| `None`, `InitState`, `Invalid`, `Unknown` | No usable backup state is reported   |

`GetBackupInfo` reads the content of a backup folder without restoring it: system name, RobotWare version and the options the backed up system was built with.

To copy the backup on your PC, download the files with the [file system service](/abb/documentation/rws-files). A complete example is given in [Backup & restore a controller](/abb/documentation/backup-restore-controller).

### Restore

**`RestoreBackup` replaces the current system and restarts the controller.** The RAPID programs, the configuration and, when asked, the safety settings of the running system are overwritten. Check the backup first with `CheckRestore`, which reports the mismatches without touching anything.

**C# : BackupRestore**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class BackupRestore
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // Check the backup before restoring it
        CheckRestoreResult check = robot.Rws.Controller.CheckRestore("$temp/mybackup");

        if (!check.IsAccepted)
        {
            Console.WriteLine($"The backup cannot be restored : {check.Status} {check.Path}");
            return;
        }
        /**/

        /**/
        // The controller restarts as soon as the restore is accepted
        robot.Rws.Controller.RestoreBackup("$temp/mybackup");

        // A backup taken on another controller has a different system id.
        // Ignore the mismatch to restore it anyway, and keep the backup folder.
        robot.Rws.Controller.RestoreBackup("$temp/mybackup",
                                           BackupRestoreIgnore.SystemId,
                                           false);

        // Restore only the RAPID modules, not the configuration
        robot.Rws.Controller.RestoreBackup("$temp/mybackup",
                                           BackupRestoreIgnore.All,
                                           true,
                                           true,
                                           true,
                                           BackupRestoreInclude.Modules);
        /**/

        robot.Disconnect();
    }
}
```

**Python : BackupRestore**
```python
from underautomation.abb.abb_controller import AbbController
from underautomation.abb.rws.data.backup_restore_ignore import BackupRestoreIgnore
from underautomation.abb.rws.data.backup_restore_include import BackupRestoreInclude

robot = AbbController()
robot.connect("192.168.0.1")

##
# Check the backup before restoring it
check = robot.rws.controller.check_restore("$temp/mybackup")

if not check.is_accepted:
    print(f"The backup cannot be restored : {check.status} {check.path}")
    raise SystemExit(1)
##

##
# The controller restarts as soon as the restore is accepted
robot.rws.controller.restore_backup("$temp/mybackup")

# A backup taken on another controller has a different system id.
# Ignore the mismatch to restore it anyway, and keep the backup folder.
robot.rws.controller.restore_backup("$temp/mybackup",
                                    BackupRestoreIgnore.SystemId,
                                    False)

# Restore only the RAPID modules, not the configuration
robot.rws.controller.restore_backup("$temp/mybackup",
                                    BackupRestoreIgnore.All,
                                    True,
                                    True,
                                    True,
                                    BackupRestoreInclude.Modules)
##

robot.disconnect()
```

| `CheckRestoreStatus`         | Meaning                                                  |
| ---------------------------- | -------------------------------------------------------- |
| `Accepted`                   | The backup can be restored as it is                      |
| `RestoreMismatchSystemId`    | The backup comes from another controller                 |
| `RestoreMismatchTemplateId`  | The backup was made from another system template         |
| `DirectoryNotComplete`       | The backup folder misses files, `Path` names one of them |
| `ConfigurationDataIncorrect` | A configuration file of the backup cannot be read        |

`BackupRestoreIgnore` says which mismatches are accepted anyway: `None`, `SystemId`, `TemplateId` or `All`. `BackupRestoreInclude` limits what is restored: `All`, `Cfg` for the configuration only, or `Modules` for the RAPID modules only.

`includeControllerSettings` is used by RobotWare 6. RobotWare 7 does not restore the controller settings and ignores the flag.

`GetBackupResources` returns the names of the backup sub resources the controller exposes. It is mainly useful to know what this particular controller supports.

**Methods of ControllerService**
```csharp
// Checks a backup for mismatches and other problems before restoring it (synchronous)
CheckRestoreResult CheckRestore(string backupPath, BackupRestoreIgnore ignore = BackupRestoreIgnore.None, bool includeControllerSettings = true, bool includeSafetySettings = true, BackupRestoreInclude include = BackupRestoreInclude.All);

// Creates a backup of the current system on the controller file system (synchronous) The backup is created asynchronously by the controller: this method returns as soon as the request is accepted. Poll ControllerService.GetBackupState to know when the backup is finished. Requires the UAS grant UAS_BACKUP. Creating a backup may affect RAPID execution and can cause system stops.
void CreateBackup(string backupPath, bool archive = false);

// Gets information about a backup stored on the controller file system (synchronous)
BackupSystemInfo GetBackupInfo(string backupPath);

// Gets the names of the backup sub resources exposed by the controller (synchronous)
string[] GetBackupResources();

// Gets the state of the backup operation of the controller (synchronous) Used to follow a backup started with String%2cSystem.Boolean).
BackupState GetBackupState();

// Restores a backup stored on the controller file system (synchronous) When the backup can be restored, the controller restarts. Requires the UAS grant to restore a backup. Use Data.BackupRestoreInclude) first to detect mismatches.
void RestoreBackup(string backupPath, BackupRestoreIgnore ignore = BackupRestoreIgnore.None, bool deleteDirectory = true, bool includeControllerSettings = true, bool includeSafetySettings = true, BackupRestoreInclude include = BackupRestoreInclude.All);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

**Members of Rws.Data.BackupSystemInfo**
```csharp
public class BackupSystemInfo {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.BackupSystemInfo" data-throw-if-not-resolved="false"></xref> class
    public BackupSystemInfo()

    // Number of options installed on the backed up system
    public int OptionCount { get; }

    // Options installed on the backed up system
    public string[] Options { get; set; }

    // RobotControl version of the backed up system.
    // 
    // <p>Only available when connected with version 2.</p>
    public string RobotControlVersion { get; set; }

    // RobotOS version of the backed up system.
    // 
    // <p>Only available when connected with version 2.</p>
    public string RobotOsVersion { get; set; }

    // RobotWare version of the backed up system.
    // 
    // <p>Only available when connected with version 1.</p>
    public string RobotWareVersion { get; set; }

    // Name of the backed up system
    public string SystemName { get; set; }

    // Returns a string representation of this backup information
    public override string ToString()
}
```

**Members of Rws.Data.BackupState**
```csharp
public enum BackupState {
    // A backup operation is running
    BackupInProgress = 3

    // The backup operation finished successfully
    BackupReady = 4

    // The backup operation failed
    ErrorDuringBackup = 5

    // A backup operation has been initialized
    InitState = 2

    // The backup state is invalid
    Invalid = 6

    // No backup operation
    None = 1

    // The backup state could not be determined
    Unknown = 0
}
```

**Members of Rws.Data.CheckRestoreResult**
```csharp
public class CheckRestoreResult {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.CheckRestoreResult" data-throw-if-not-resolved="false"></xref> class
    public CheckRestoreResult()

    // Indicates whether the backup can be restored
    public bool IsAccepted { get; }

    // File missing or corrupted in the backup, if reported by the controller
    public string Path { get; set; }

    // Status of the check
    public CheckRestoreStatus Status { get; set; }

    // Returns a string representation of this check result
    public override string ToString()
}
```

**Members of Rws.Data.CheckRestoreStatus**
```csharp
public enum CheckRestoreStatus {
    // The backup is accepted and can be restored
    Accepted = 1

    // Error in the configuration data of the backup
    ConfigurationDataIncorrect = 5

    // The backup directory is not complete
    DirectoryNotComplete = 4

    // The backup was not created from the current system, there might be differences in active options and selected languages
    RestoreMismatchSystemId = 2

    // The current system and the backed up system may be generated from different key ids, possibly with different robot types
    RestoreMismatchTemplateId = 3

    // The status could not be determined
    Unknown = 0
}
```

**Members of Rws.Data.BackupRestoreIgnore**
```csharp
public enum BackupRestoreIgnore {
    // All mismatches are ignored
    All = 1

    // No mismatch is ignored
    None = 0

    // A mismatch between the system id of the backup and the system id of the current system is ignored
    SystemId = 2

    // A mismatch between the template id of the backup and the template id of the current system is ignored
    TemplateId = 3
}
```

**Members of Rws.Data.BackupRestoreInclude**
```csharp
public enum BackupRestoreInclude {
    // Restore configuration files and RAPID modules
    All = 0

    // Restore configuration files only
    Cfg = 1

    // Restore RAPID modules only
    Modules = 2
}
```

## Safety

These methods talk to the safety controller. They need the Safety Module option (SafeMove) on the controller and the safety grants on the user account. Without them the controller answers 403, and the SDK throws an `RwsException` that says which of the two is probably missing.

**C# : SafetyStatus**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class SafetyStatus
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // Current safety mode of the controller
        SafetyModeStatus mode = robot.Rws.Controller.GetSafetyMode();
        Console.WriteLine($"Safety mode : {mode.Mode}, user data {mode.UserData}");

        // Versions and checksum of the loaded safety configuration
        SafetyConfiguration configuration = robot.Rws.Controller.GetSafetyConfiguration();
        Console.WriteLine($"{configuration.Name} created on {configuration.CreationDate} by {configuration.CreatedBy}");
        Console.WriteLine($"Checksum : {configuration.Checksum}");
        /**/

        /**/
        // What the safety controller reports about the last violation
        SafetyViolationInfo violation = robot.Rws.Controller.GetSafetyViolationInfo();
        Console.WriteLine($"{violation.ViolationNumber} violation(s), type {violation.ViolationType}");

        // Cyclic brake check of the drive number 1
        CyclicBrakeCheckStatus brakeCheck = robot.Rws.Controller.GetCyclicBrakeCheckStatus(1);
        Console.WriteLine($"Brake check : {brakeCheck.Status}, last result {brakeCheck.LastBrakeCheckStatus}");
        /**/

        robot.Disconnect();
    }
}
```

**Python : SafetyStatus**
```python
from underautomation.abb.abb_controller import AbbController

robot = AbbController()
robot.connect("192.168.0.1")

##
# Current safety mode of the controller
mode = robot.rws.controller.get_safety_mode()
print(f"Safety mode : {mode.mode}, user data {mode.user_data}")

# Versions and checksum of the loaded safety configuration
configuration = robot.rws.controller.get_safety_configuration()
print(f"{configuration.name} created on {configuration.creation_date} by {configuration.created_by}")
print(f"Checksum : {configuration.checksum}")
##

##
# What the safety controller reports about the last violation
violation = robot.rws.controller.get_safety_violation_info()
print(f"{violation.violation_number} violations, type {violation.violation_type}")

# Cyclic brake check of the drive number 1
brake_check = robot.rws.controller.get_cyclic_brake_check_status(1)
print(f"Brake check : {brake_check.status}, last result {brake_check.last_brake_check_status}")
##

robot.disconnect()
```

`GetSafetyMode` returns the current mode and the user data that goes with it. `SetSafetyMode` accepts `Active`, `Commissioning` and `Service`. The other values, `ModeError` and `Unknown`, are reported by the controller and cannot be requested. The controller must be in manual mode.

Loading a safety configuration is a two step operation. `GetSafetyLoadOperationStatus` says whether the controller accepts it right now, then `LoadSafetyConfiguration` reads a file that already exists on the controller file system.

**C# : SafetyConfigure**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class SafetyConfigure
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("192.168.0.1");

        /**/
        // A safety configuration can only be loaded in some controller states
        SafetyLoadOperationStatus status = robot.Rws.Controller.GetSafetyLoadOperationStatus();

        if (status == SafetyLoadOperationStatus.Ok)
        {
            // The file is already on the controller file system
            robot.Rws.Controller.LoadSafetyConfiguration("$home/safety.xml");
        }
        else
        {
            Console.WriteLine($"A safety configuration cannot be loaded now : {status}");
        }
        /**/

        /**/
        // The controller must be in manual mode to change the safety mode
        robot.Rws.Controller.SetSafetyMode(SafetyMode.Commissioning);

        // Removes the validation information of the current safety configuration
        robot.Rws.Controller.InvalidateSafetyConfiguration();
        /**/

        robot.Disconnect();
    }
}
```

**Python : SafetyConfigure**
```python
from underautomation.abb.abb_controller import AbbController
from underautomation.abb.rws.data.safety_load_operation_status import SafetyLoadOperationStatus
from underautomation.abb.rws.data.safety_mode import SafetyMode

robot = AbbController()
robot.connect("192.168.0.1")

##
# A safety configuration can only be loaded in some controller states
status = robot.rws.controller.get_safety_load_operation_status()

if status == SafetyLoadOperationStatus.Ok:
    # The file is already on the controller file system
    robot.rws.controller.load_safety_configuration("$home/safety.xml")
else:
    print(f"A safety configuration cannot be loaded now : {status}")
##

##
# The controller must be in manual mode to change the safety mode
robot.rws.controller.set_safety_mode(SafetyMode.Commissioning)

# Removes the validation information of the current safety configuration
robot.rws.controller.invalidate_safety_configuration()
##

robot.disconnect()
```

| `SafetyLoadOperationStatus`  | Why loading is refused                 |
| ---------------------------- | -------------------------------------- |
| `Ok`                         | A configuration can be loaded          |
| `OptionNotPresent`           | The safety option is not installed     |
| `NotInManualMode`            | The controller is not in manual mode   |
| `NotInMotorsOff`             | The motors are on                      |
| `CurrentConfigurationLocked` | The configuration in use is locked     |
| `UserGrantMissing`           | The connected user has no safety grant |

`InvalidateSafetyConfiguration` removes the validation information of the configuration file. **After that the safety configuration has to be validated again before the robot can run.**

`GetCyclicBrakeCheckStatus` takes the drive number of a mechanical unit and returns when the next brake check is due and how the last one ended. `GetSafetyViolationInfo` gives the details of the last violation seen by the safety controller.

**Methods of ControllerService**
```csharp
// Gets the cyclic brake check status of a mechanical unit (synchronous)
CyclicBrakeCheckStatus GetCyclicBrakeCheckStatus(int driveNumber);

// Gets the safety supervision configuration of the controller (synchronous)
SafetyConfiguration GetSafetyConfiguration();

// Checks whether a new safety configuration is allowed to be loaded (synchronous) The user must have the safety services privileges.
SafetyLoadOperationStatus GetSafetyLoadOperationStatus();

// Gets the safety mode of the controller (synchronous)
SafetyModeStatus GetSafetyMode();

// Gets the names of the safety sub resources exposed by the controller (synchronous)
string[] GetSafetyResources();

// Gets the safety violation details reported by the safety controller (synchronous) The user must have the safety services privileges.
SafetyViolationInfo GetSafetyViolationInfo();

// Removes the validation information from the safety configuration file (synchronous) Requires the UAS grant UAS_SAFETY_SERVICES.
void InvalidateSafetyConfiguration();

// Loads a safety configuration file into the controller (synchronous) The configuration file must already exist on the controller file system. Use ControllerService.GetSafetyLoadOperationStatus to check whether loading is currently allowed.
void LoadSafetyConfiguration(string filePath);

// Sets the safety mode of the controller (synchronous) The controller must be in manual mode.
void SetSafetyMode(SafetyMode mode);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

**Members of Rws.Data.SafetyModeStatus**
```csharp
public class SafetyModeStatus {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.SafetyModeStatus" data-throw-if-not-resolved="false"></xref> class
    public SafetyModeStatus()

    // Current safety mode
    public SafetyMode Mode { get; set; }

    // Returns a string representation of this safety mode status
    public override string ToString()

    // User data associated with the safety mode, if reported by the controller
    public int? UserData { get; set; }
}
```

**Members of Rws.Data.SafetyMode**
```csharp
public enum SafetyMode {
    // The safety configuration is active and supervised
    Active = 1

    // Commissioning mode, used while configuring the safety controller
    Commissioning = 2

    // The safety controller reports a mode error
    ModeError = 4

    // Service mode
    Service = 3

    // The safety mode could not be determined
    Unknown = 0
}
```

**Members of Rws.Data.SafetyConfiguration**
```csharp
public class SafetyConfiguration {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.SafetyConfiguration" data-throw-if-not-resolved="false"></xref> class
    public SafetyConfiguration()

    // Checksum of the configuration, as base64 encoded data
    public string Checksum { get; set; }

    // Status of the configuration, for example "SCORCH_CONFIG_LOADED".
    // 
    // <p>Only available when connected with version 2.</p>
    public string ConfigurationStatus { get; set; }

    // Author of the configuration
    public string CreatedBy { get; set; }

    // Creation date of the configuration, if available
    public DateTime? CreationDate { get; set; }

    // Configuration file major version
    public int? FileMajorVersion { get; set; }

    // Configuration file minor version
    public int? FileMinorVersion { get; set; }

    // Configuration file revision
    public int? FileRevision { get; set; }

    // Name of the configuration
    public string Name { get; set; }

    // Safety software major version
    public int? SoftwareMajorVersion { get; set; }

    // Safety software minor version
    public int? SoftwareMinorVersion { get; set; }

    // Safety software revision
    public int? SoftwareRevision { get; set; }

    // Returns a string representation of this safety configuration
    public override string ToString()
}
```

**Members of Rws.Data.SafetyLoadOperationStatus**
```csharp
public enum SafetyLoadOperationStatus {
    // The current safety configuration is locked (SCORCH_ERR_CURRENT_CONFIG_LOCKED)
    CurrentConfigurationLocked = 5

    // The controller is not in manual mode (SCORCH_ERR_NOT_IN_MANUAL_MODE)
    NotInManualMode = 3

    // The motors are not switched off (SCORCH_ERR_NOT_IN_MOTORS_OFF)
    NotInMotorsOff = 4

    // Loading a new safety configuration is allowed
    Ok = 1

    // The safety option is not present on the controller (SCORCH_ERR_OPTION_NOT_PRESENT)
    OptionNotPresent = 2

    // The status could not be determined
    Unknown = 0

    // The user does not have the required grant (SCORCH_ERR_USER_GRANT_IS_MISSING)
    UserGrantMissing = 6
}
```

**Members of Rws.Data.SafetyViolationInfo**
```csharp
public class SafetyViolationInfo {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.SafetyViolationInfo" data-throw-if-not-resolved="false"></xref> class
    public SafetyViolationInfo()

    // Axis range supervision active status
    public int? AxisRangeActiveStatus { get; set; }

    // Axis range violation status
    public int? AxisRangeViolationStatus { get; set; }

    // Index of the drive module involved in the violation
    public int? DriveModuleIndex { get; set; }

    // Instance id of the last violation
    public int? LastViolationInstanceId { get; set; }

    // Returns a string representation of this safety violation information
    public override string ToString()

    // Id of the tool involved in the violation
    public int? ToolId { get; set; }

    // Tool position supervision active status
    public int? ToolPositionActiveStatus { get; set; }

    // Tool position violation status
    public int? ToolPositionViolationStatus { get; set; }

    // Tool speed supervision active status
    public int? ToolSpeedActiveStatus { get; set; }

    // Tool speed violation status
    public int? ToolSpeedViolationStatus { get; set; }

    // Indicates whether the robot is unsynchronized
    public int? Unsynchronized { get; set; }

    // Upper arm violation status
    public int? UpperArmViolationStatus { get; set; }

    // Violating safety supervision value
    public int? ViolatingSsv { get; set; }

    // Number of violations
    public int? ViolationNumber { get; set; }

    // Type of the current violation
    public SafetyViolationType ViolationType { get; set; }
}
```

**Members of Rws.Data.SafetyViolationType**
```csharp
public enum SafetyViolationType {
    // Emergency stop triggered (empstop)
    EmergencyStop = 12

    // The safety controller reports an invalid violation
    Invalid = 14

    // No violation
    None = 1

    // Operational Safety Range (osr)
    OperationalSafetyRange = 7

    // Internal error (other)
    Other = 13

    // Reduced Axis Speed in manual mode (red_axis_speed)
    ReducedAxisSpeed = 10

    // Reduced Tool Speed in manual mode (red_tool_speed)
    ReducedToolSpeed = 9

    // Safe Axis Range (sar)
    SafeAxisRange = 3

    // Safe Axis Speed (sas)
    SafeAxisSpeed = 5

    // Safe Standstill (sst)
    SafeStandstill = 8

    // Safe Tool Speed (sts)
    SafeToolSpeed = 4

    // Safe Tool Zone (stz)
    SafeToolZone = 2

    // Tool Orientation Monitoring (tom)
    ToolOrientationMonitoring = 6

    // The violation type could not be determined
    Unknown = 0

    // Reduced Axis Speed due to unsynchronized robot (unsync_speed_lim)
    UnsynchronizedSpeedLimit = 11
}
```

**Members of Rws.Data.CyclicBrakeCheckStatus**
```csharp
public class CyclicBrakeCheckStatus {
    // Initializes a new instance of the <xref href="UnderAutomation.ABB.Rws.Data.CyclicBrakeCheckStatus" data-throw-if-not-resolved="false"></xref> class
    public CyclicBrakeCheckStatus()

    // Drive number of the mechanical unit this status belongs to
    public int DriveNumber { get; set; }

    // Result of the last brake check
    public CyclicBrakeCheckTestStatus LastBrakeCheckStatus { get; set; }

    // Remaining time before the next brake check is required, if reported by the controller
    public long? NextBrakeCheckTime { get; set; }

    // Current cyclic brake check state
    public CyclicBrakeCheckState Status { get; set; }

    // Returns a string representation of this cyclic brake check status
    public override string ToString()
}
```

**Members of Rws.Data.CyclicBrakeCheckState**
```csharp
public enum CyclicBrakeCheckState {
    // No brake check is needed (CBC_STATUS_OK)
    Ok = 1

    // A brake check will soon be required (CBC_STATUS_PREWARNING)
    PreWarning = 2

    // A brake check is required (CBC_STATUS_REQUIRE_CBC)
    Required = 3

    // The state could not be determined
    Unknown = 0
}
```

**Members of Rws.Data.CyclicBrakeCheckTestStatus**
```csharp
public enum CyclicBrakeCheckTestStatus {
    // The last brake check failed (CBC_TEST_ERROR)
    Error = 3

    // The last brake check succeeded (CBC_TEST_OK)
    Ok = 1

    // No brake check has been performed yet (CBC_TEST_UNDEFINED)
    Undefined = 4

    // The test status could not be determined
    Unknown = 0

    // The last brake check ended with a warning (CBC_TEST_WARNING)
    Warning = 2
}
```

## Virtual time

A RobotStudio virtual controller does not run in real time. It runs a simulation clock, the virtual time, that you can slow down, speed up or advance step by step. These methods make sense only on a virtual controller, a real one has no such clock.

**C# : ControllerVirtualTime**
```csharp
using UnderAutomation.ABB;
using UnderAutomation.ABB.Rws.Data;

public class ControllerVirtualTime
{
    static void Main()
    {
        AbbController robot = new AbbController();
        robot.Connect("127.0.0.1");

        /**/
        // Milliseconds elapsed since the virtual controller started
        long virtualTime = robot.Rws.Controller.GetVirtualTime();
        Console.WriteLine($"Virtual time : {virtualTime} ms");

        // 100 is about real time, -1 runs the simulation as fast as possible
        robot.Rws.Controller.SetVirtualTimeSpeed(100);
        Console.WriteLine($"Speed : {robot.Rws.Controller.GetVirtualTimeSpeed()} %");

        // Duration of one step, 10 ms minimum
        robot.Rws.Controller.SetVirtualTimeSlice(50);
        Console.WriteLine($"Time slice : {robot.Rws.Controller.GetVirtualTimeSlice()} ms");
        /**/

        /**/
        // Run the virtual time one step at a time
        robot.Rws.Controller.SetVirtualTimeState(VirtualTimeState.RunSlice);
        robot.Rws.Controller.RunVirtualTime();

        VirtualTimeState state = robot.Rws.Controller.GetVirtualTimeState();
        Console.WriteLine($"State : {state}");

        // Let the simulation run freely again
        robot.Rws.Controller.SetVirtualTimeState(VirtualTimeState.FreeRun);
        /**/

        robot.Disconnect();
    }
}
```

**Python : ControllerVirtualTime**
```python
from underautomation.abb.abb_controller import AbbController
from underautomation.abb.rws.data.virtual_time_state import VirtualTimeState

robot = AbbController()
robot.connect("127.0.0.1")

##
# Milliseconds elapsed since the virtual controller started
virtual_time = robot.rws.controller.get_virtual_time()
print(f"Virtual time : {virtual_time} ms")

# 100 is about real time, -1 runs the simulation as fast as possible
robot.rws.controller.set_virtual_time_speed(100)
print(f"Speed : {robot.rws.controller.get_virtual_time_speed()} %")

# Duration of one step, 10 ms minimum
robot.rws.controller.set_virtual_time_slice(50)
print(f"Time slice : {robot.rws.controller.get_virtual_time_slice()} ms")
##

##
# Run the virtual time one step at a time
robot.rws.controller.set_virtual_time_state(VirtualTimeState.RunSlice)
robot.rws.controller.run_virtual_time()

state = robot.rws.controller.get_virtual_time_state()
print(f"State : {state}")

# Let the simulation run freely again
robot.rws.controller.set_virtual_time_state(VirtualTimeState.FreeRun)
##

robot.disconnect()
```

`GetVirtualTime` returns the milliseconds elapsed since the virtual controller started. `GetVirtualTimeSpeed` and `SetVirtualTimeSpeed` work in percent of the real time: `100` is about real time, `-1` runs the simulation as fast as the PC can. The time slice is the duration of one step, 10 ms minimum.

| `VirtualTimeState` | Meaning                                                                       |
| ------------------ | ----------------------------------------------------------------------------- |
| `Stop`             | The virtual time does not advance                                             |
| `FreeRun`          | The virtual time runs continuously                                            |
| `RunSlice`         | Each call to `RunVirtualTime` advances the clock by one time slice            |
| `NextEvent`        | Each call to `RunVirtualTime` advances the clock to the next controller event |
| `Unknown`          | The controller reported a value the SDK does not know                         |

`Unknown` is only returned by `GetVirtualTimeState`, it cannot be set. `RunVirtualTime` executes the virtual time according to the current state, so it is used with `RunSlice` and `NextEvent`.

Running a simulation faster than real time makes tests shorter, but the robot then reacts faster than your application. Read [Test with a RobotStudio virtual controller](/abb/documentation/virtual-controller) before using it in automated tests.

**Methods of ControllerService**
```csharp
// Gets the current value of the virtual time, in milliseconds (synchronous) The virtual time is zeroed when the virtual controller starts. Supported only on a virtual controller.
long GetVirtualTime();

// Gets the names of the virtual time sub resources exposed by the controller (synchronous) Supported only on a virtual controller.
string[] GetVirtualTimeResources();

// Gets the time slice of the virtual controller, in milliseconds (synchronous) Supported only on a virtual controller.
int GetVirtualTimeSlice();

// Gets the speed of the virtual time, in percent relative to real time (synchronous) -1 means full speed. Supported only on a virtual controller.
int GetVirtualTimeSpeed();

// Gets the state of the virtual time server (synchronous) Supported only on a virtual controller.
VirtualTimeState GetVirtualTimeState();

// Executes the virtual time according to the current state of the virtual time server (synchronous) Supported only on a virtual controller.
void RunVirtualTime();

// Sets the time slice of the virtual controller, in milliseconds (synchronous) The minimum value is 10 ms, lower values are replaced by the controller with the default value of 10 ms. Supported only on a virtual controller.
void SetVirtualTimeSlice(int milliseconds);

// Sets the speed of the virtual time, in percent relative to real time (synchronous) 100 makes the virtual time run approximately at real time speed, -1 runs it as fast as possible. Supported only on a virtual controller.
void SetVirtualTimeSpeed(int speed);

// Sets the state of the virtual time server (synchronous) Supported only on a virtual controller.
void SetVirtualTimeState(VirtualTimeState state);
```

Every method also exists in an asynchronous version, with the same name followed by `Async` and an optional `CancellationToken`.

**Members of Rws.Data.VirtualTimeState**
```csharp
public enum VirtualTimeState {
    // Virtual time runs freely (VTFREERUN)
    FreeRun = 2

    // Virtual time runs until the next event (VTNEXTEVENT)
    NextEvent = 4

    // Virtual time runs one time slice at a time (VTRUNSLICE)
    RunSlice = 3

    // Virtual time is stopped (VTSTOP)
    Stop = 1

    // The state could not be determined
    Unknown = 0
}
```

## Try it in the demo application

Everything on this page can be tried without writing code, in the **Controller info (RWS)** page of the [demo application](/abb/documentation/demo-app).

![Controller info (RWS) page of the ABB SDK demo application](/abb/WinformsScreenshots/Rws.jpg)

Its C# source is [RwsControllerControl.cs](https://github.com/underautomation/ABB.NET/blob/main/UnderAutomation.ABB.Showcase.Forms/Components/RwsControllerControl.cs).