User Script Case Study

Here's a rather interesting case study that shows how a user script can be utilized as a bridge between a position tracking sensor and the scroll position of a scroll block. The project involves a 5 meter long wall with a printed "wallpaper". In front of this wall hangs a vertically oriented display. This display runs on a track, allowing it to slide along the wall. The content of the display scrolls to shows the section of the wall it's placed in front of, allowing a visitor to interact with the content at that position.

To interact with another part of the wall, the visitor can push the screen to the desired position. Hence, the screen acts as an interactive window or looking glass for the static image behind it. The position tracker reads a barcode along the track, sending the position readout to Blocks. Blocks translates this to a scroll position, applying the result to the Display Spot connected to the display. By scrolling the content in the opposite direction of the display's movement, the image appears stationary in relation to the wall.

The barcode reader is a rather small device, mounted behind one of the brackets used to attach the display to the track. There are many types of position sensors operating in various ways. This one – a Leuze ODSL 8 – reads the position with millimeter accuracy from a barcode strip affixed to the track.

The sensor uses a laser to read the barcode strip, streaming the resulting data over a serial RS-232 connection, which in its turn connects to Blocks through a MOXA nPort serial-to-ethernet adapter. The MOXA nPort is added to Blocks under Manage as a Network TCP device.

To save some time in getting the complete solution working, no driver was written for this device. Instead, the code that received data from the sensor was rolled into the user script. Another option could have been to write a driver that published a property corresponding to the most recent position readout. That would have simplified the user script at the expense of writing both a driver and a user script.

Here's the complete script used to read the position data, scale it as appropriate and apply the result to the scroll position of the display spot.

/* Blocks user script reading data from a Leuze ODSL 8 barcode
   distance sensor. The device has an RS232 comms port, which
   is connected to the network through a MOXA nPort interface.
 */

// Import referenced system objects
import {Script, ScriptEnv} from "system_lib/Script";
import {NetworkTCP, Network} from "system/Network";
import {Spot, DisplaySpot} from "system/Spot";

/** A user script must have a class with the same name as its 
    containing file (minus the .ts suffix).
*/
export class LeuzeScript extends Script {
  networkPort: NetworkTCP;  // Network TCP connection to device
  lastPosition: number;    // Last position received from device
  timeout: CancelablePromise<void>; // Used to re-init stalled comms

  /** A user script must have a constructor that takes a single
      ScriptEnv parameter, which is passed to super.
  */
  public constructor(env : ScriptEnv) {
    super(env);
    // Get the network TCP connection used to talk to the device.
    this.networkPort = <NetworkTCP>Network['Leuze over Moxa'];

    this.networkPort.subscribe('textReceived', (sender, message) => 
      this.dataReceived(message.text)
    );

    if (this.networkPort.connected) // Handle connected BEFORE script starts
      this.doWhenConnected();

    // Handle connection state changes AFTER script started
    this.networkPort.subscribe('connect', (sender, message) => {
      // console.log("Connection state", message.type);
      if (message.type === 'Connection' && this.networkPort.connected)
        this.doWhenConnected();
    });
    // console.log("LeuzeScript instantiated");
  }

  /** What needs to be one when we connect (or re-connect) to the reader device.
  */
  doWhenConnected() {
    this.initComms();
    this.resetTimeout();
  }

  /** Tell the barcode reader to please start sending data. These command
      strings are according to the device's documentation.
  */
  initComms() {
    if (this.networkPort.connected) {
      this.networkPort.sendText('\x02M+', '\r\n');
      this.networkPort.sendText('\x02MMT0100', '\r\n');
    }
  }

  /** Look for silence from the device, and re-init communication 
      if that happens. Call when data is received form the device
      to reset and restart this timer.
  */
  resetTimeout() {
    if (this.timeout) // Had a pending timer
      this.timeout.cancel();  // Kill it
    this.timeout = wait(2000);  // Wait at most this many mS
    this.timeout.then(() => {  // Re-init comms if ever times out
      this.timeout = undefined;
      // console.log("Re-inited comms");
      this.initComms();
      this.resetTimeout();  // Restart timer
    });
  }

  /** New data received from device. Attempt to parse out number and scroll
      to that position.
  */
  dataReceived(posData: string) {
    // console.log(posData);
    // Skip leading STX character, then parse remainder as a number 
    var position = parseInt(posData.substr(1));
    /*  Device sends other data occasionally, causing the parsing to fail.
      So we need to verify that parseInt indeed returned a Number.
    */
    const posType = typeof position;
    if (posType === 'number' && this.lastPosition !== position) {
      // Was indeed a number, and different from last position received
      this.lastPosition = position;
      // console.log("New position", position);
      // Normalize maximum position readout to 1, for scroller use
      position = position / 5000;
      
      // Lookup the Spot by name and scroll it to that position
      const spot = <DisplaySpot>Spot['43inch Portrait'];
      spot.scrollTo(position);
    }
    this.resetTimeout(); // Got data, so reset silence timeout
  }
}

The script has plenty of comments explaining what's going on, but here are a few additional highlights.

  • The script must be saved in a file named LeuzeScript.ts in the script/user directory of the Blocks root.
  • It compiles to a file named LeuzeScript.js, in that same directory. This is handled automatically by the editor used to create and edit scripts.
  • The script doesn't expose any externally visible properties or functions, as all it's intended to do is handled internally.
  • The class name must match the filename (minus the .ts/.js file extension).
  • Several console.log calls were added during the development to inspect the behavior along the way. Those were commented out once the script was working properly.
  • The script must have a constructor that takes a single ScriptEnv parameter and calls super with this parameter.
  • It uses the imported Network system object to obtain a reference to the network device named 'Leuze over Moxa', which has been added with that name under Manage, Network TCP/UDP in Blocks, with its IP address and port number set to the ones assigned to the MOXA interface.
  • It subscribes to the textReceived event in order to read the position information from the sensor.
  • When data is received, the payload in message.text is passed to the dataReceived function. This is done using an arrow function, also known as a lambda function.
  • The sensor needs to be told to start streaming data. This is done by the initComms function.
  • If the sensor is disconnected or lose power, it will revert back to its silent state, and must be told to start streaming data again. This is managed by the resetTimeout function, which is called once when first connected, and then whenever data is received. If no data is received for 2 seconds, it will call initComms to re-initialize the sensor.
  • The connection between Blocks and the MOXA nPort interface may be established before the script is started or after. Both these cases are accounted for in the constructor, which first checks if already connected. If so, it calls doWhenConnected immediately. It also subscribes to the 'connect' event, and calls doWhenConnected when this fires in a way that indicates that the port just connected.

Transforming the sensor data to a scroll position

The code that binds the data received from the sensor to the scroll position of the spot is found in the dataReceived function. It accepts the raw sensor data, which is a string that begins with an ASCII STX character, followed by a number of digits indicating the scroll position. The STX character is of no interest, so it is skipped over. The remainder is parsed to a number. Occasionally, the sensor may send other data rather than a numeric position. Hence, we need to verify that the type of the resulting data is indeed a number. We also compare the numeric value to the previous value received, and ignore any repetitions.

Finally, the readout from the sensor is scaled to a normalized value (0…1), the Display Spot is obtained from the Spot system object, and scrolled to desired position.

Credit where credit is due

The idea described above was devised and implemented by Sierk Janszen, Wilfred de Zoete and Jean-Paul Coenraad at Rapenburg Plaza. Some details were altered and adapted for this write-up.