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.

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.