Linear Gantry

In this 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 file for the model.

Solution

The completed model can be downloaded.

Conveyor Controller

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

Search

The first way 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 called Beam then find() will return only the first entity found.

Handles

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 that they reference is deleted.

The complete source code for the conveyor controller component is provided below:

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

export class ConveyorController extends Component {
    public sensor: Handle<SensorComponent> = this.handle(SensorComponent);
    public motor: Handle<MotorComponent> = this.handle(MotorComponent);

    constructor(entity: Entity) {
        super(entity);
    }

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

Gantry Controller

The gantry controller component performs a cyclic sequence 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 handle properties. Once again we make use of overriding the initialize() function which gets executed at the start of the simulation.

The built-in Sequence class is used to manage the transition from one operation in the sequence to the next operation. 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 until the sensor is blocked before executing the next operation in the sequence.

As an aside, if you’re familiar with JavaScript then this will look very similar to the Promise<T> type. The key difference is in 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. It is possible to make asynchronous functions safe, though this is an advanced topic for another time.

The complete source code for the gantry controller component is provided below:

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

export class GantryController extends Component {
    public sensor: Handle<SensorComponent> = this.handle(SensorComponent);
    public horizontal: Handle<MotorComponent> = this.handle(MotorComponent);
    public vertical: Handle<MotorComponent> = this.handle(MotorComponent);
    public gripper: Handle<SuctionGripperComponent> = this.handle(SuctionGripperComponent);

    constructor(entity: Entity) {
        super(entity);
    }

    public override initialize() {
        const sequence = new Sequence(true);
        sequence.add(() => Wait.value(this.sensor.value!.io.state, true)) // Wait until the sensor is blocked.
        sequence.add(() => this.horizontal.value!.moveTo(0.26)) // Move left.
        sequence.add(() => this.vertical.value!.moveTo(0.995)) // Move down.
        sequence.add(() => this.gripper.value!.state = true) // Grip the object by turning the vacuum pump on.
        sequence.add(() => this.vertical.value!.moveTo(0.5)) // Move up.
        sequence.add(() => this.horizontal.value!.moveTo(1.74)) // Move right.
        sequence.add(() => this.vertical.value!.moveTo(0.9)) // Move down.
        sequence.add(() => this.gripper.value!.state = false) // Release the object by turning the vacuum pump off.
        sequence.add(() => this.vertical.value!.moveTo(0.5)) // Move up.
        sequence.start();
    }
}