Linear Gantry

In this first tutorial we’ll be creating a simulation of a linear gantry, which will transfer boxes between two conveyors. After completing this tutorial, you should be able to create basic simulations of simple industrial machines. Start by downloading the CAD geometry for the model.

Solution

The completed model can be downloaded.

Conveyor Controller

The conveyor controller component switches off the conveyor’s motor when the sensor is blocked, and switches it back on once the sensor is cleared. The conveyor controller component therefore needs to know about both the sensor and the conveyor’s motor. There are two ways of obtaining these dependencies.

The first method is to find these components by searching on the entities to which they are attached. For example, to find the sensor component on the “Beam” entity.

const beamEntity = this.world.find("Beam");
const sensor = beamEntity!.findComponent(SensorComponent);

This method is fine when finding components on the same entity as your scripted component. However, if there are multiple entities named “Beam” then find() will return only the first one found.

The second method is to inject the dependencies into the component. This is the method used in this tutorial. It involves creating public properties, which can then be configured in the editor. Additionally, we use handles for the dependencies. Handles automatically monitor the lifetime of the objects that they reference, so the value of the handle is automatically set to null if the object they reference is deleted.

The complete source code for the component is provided below.

import { type Entity, Component, MotorComponent, SensorComponent, Handle } from "prototwin";

export class ConveyorController extends Component {
    public sensor: Handle<SensorComponent>;
    public motor: Handle<MotorComponent>;

    constructor(entity: Entity) {
        super(entity);
        this.sensor = this.handle(SensorComponent);
        this.motor = this.handle(MotorComponent);
    }

    public override initialize() {
        this.subscribe(this.sensor.value!.io.state, (state: boolean) => this.motor.value!.state = !state);
    }
}

Gantry Controller

The gantry controller component performs the cycle of operations required to transfer the boxes from one conveyor to the other. Much like the conveyor controller, it has a number of dependencies for which we create public properties. We once again make use of overriding the initialize() function, which is executed at the start of the simulation.

The built-in Sequence type is used to manage the transitions from one operation to the next. Each operation is added to the sequence in the order that they should be executed. Operations are just functions, which may optionally return a Future<T> object. A future is a promise to return a value at some time in the future. The Wait.value() function returns a future that is resolved when the value of the first argument is equal to the value of the second argument. So the first operation uses this function to wait for the sensor to be blocked.

As an aside, if you’re familiar with JavaScript then this will look very similar to the Promise<T> type. The key difference is that JavaScript promises are asynchronous, whilst futures are synchronous. ProtoTwin uses multiple threads which require synchronization at certain points. Asynchronous functions present a problem, since they may be executed at arbitrary times. Whilst it’s possible to make asynchronous functions safe, this is an advanced topic for another time.

The complete source code for the component is provided below.

import { type Entity, Component, MotorComponent, SensorComponent, SuctionGripperComponent, Sequence, Wait, Handle } from "prototwin";

export class GantryController extends Component {
    public sensor: Handle<SensorComponent>;
    public horizontal: Handle<MotorComponent>;
    public vertical: Handle<MotorComponent>;
    public gripper: Handle<SuctionGripperComponent>;

    constructor(entity: Entity) {
        super(entity);
        this.sensor = this.handle(SensorComponent);
        this.horizontal = this.handle(MotorComponent);
        this.vertical = this.handle(MotorComponent);
        this.gripper = this.handle(SuctionGripperComponent);
    }

    public override initialize() {
        const sequence = new Sequence(true);
        sequence.add(() => Wait.value(this.sensor.value!.io.state, true)) // Blocked
                .add(() => this.horizontal.value!.moveTo(0.26)) // Left
                .add(() => this.vertical.value!.moveTo(0.995)) // Down
                .add(() => this.gripper.value!.state = true) // Grip
                .add(() => this.vertical.value!.moveTo(0.5)) // Up
                .add(() => this.horizontal.value!.moveTo(1.74)) // Right
                .add(() => this.vertical.value!.moveTo(0.9)) // Down
                .add(() => this.gripper.value!.state = false) // Release
                .add(() => this.vertical.value!.moveTo(0.5)) // Up
                .start();
    }
}