Writing Control Applications and Other Script Tasks

In addition to measurement scripts and transmission formatting scripts, Satlink 3 and XLink 500 also support script tasks. Script tasks are not tied to a measurement or a transmission. Instead, they may be scheduled to run at specified time or triggered by an event.

You do not need to have the full Python development environment setup for this. LinkComm will suffice.

The manual has crucial information about how scripts work.
https://www.ott.com/download/operations-maintenance-manual-sutron-satlink-3-1/

Please take the time to read the chapter on Python scripts. Without that knowledge, it is going to be difficult to work with Satlink 3 and XLink 500 scripts.

Triggering an Auto Sampler Daily

An auto sampler is a device that collects a sample of water into a bottle. Samplers are used so that the quality of water may be later checked in a lab.

Our first example will trigger a sampler at 8 PM every day.

Samplers are controlled by the data logger. Satlink 3 and XLink 500 offer two main ways of triggering a sampler:

  • Grounding a 5V digital line (DOUT)
  • Taking the switched power line to 12V (SW12)

The appropriate method will depend on the sampler. Please see the manual for more details https://www.ott.com/download/operations-maintenance-manual-sutron-satlink-3-1/.


This code will trigger the digital line DOUT2 for 2 seconds

def trigger_sampler_digital():
    output_control('OUTPUT2', True)
    utime.sleep(2.0)
    output_control('OUTPUT2', False)

And this code will drive the Switched 12V SW2 line for 1.5 seconds

def trigger_sampler_12v():
    power_control('SW2', True)
    utime.sleep(1.5)
    power_control('SW2', False)

output_control, power_control, and other functions are all documented in:

For our example, let us use the digital line. Let us write the function that will be executed daily at 8PM in order to trigger the sampler:

@TASK
def trigger_task():
    trigger_sampler_digital()
  • Note that the @TASK decorator is required for functions that will interface into the Satlink 3 and XLink 500 setup.

Here is the completed Python script:


Now that we have the script, we need to integrate it into the setup using LinkComm.

  • Run LinkComm

LinkCommSL3Connect
  • Choose Station Type XLink 500
  • Click Work offline.

LinkCommOpenScript
  • Go to the Script tab of LinkComm and open the sampler_daily script file.

LinkCommDailyTask
  • On the left hand side, click S1
  • S1 is one of the available script tasks we may use. Any task would have been fine.
  • Setup the S1 Script Task to trigger daily at 8PM
  • Choose trigger_task as the Script Function
Completed setup file:

That completes this basic example. Next, we will expand upon this script.


Counting Bottles And Trigger Time

This example builds upon the daily auto sampler trigger. In addition to triggering the sampler at 8PM, these features are illustrated

  • We will track how many bottles are used and only trigger if emtpy bottles remain.
  • Status will be updated as the sampler is triggered, allowing us to view the number of remaining bottles in LinkComm.
  • We will write a log entry every time the sampler is triggered.
  • Additionally, we will track the time of the last sampler trigger and ensure we trigger only once a day.

This will illustrate the use of global variables. Please note that these variables are reset on power up.

# The sampler has a limited number of bottles.
bottles_capacity = 24

# We count how many bottles are in use.
bottles_used = 0

# Time sampler was triggered last.
time_last_sample = 0.0


def triggered_today():
    """Have we triggered the sampler today?"""
    if time_last_sample == 0.0:
        return False  # we never triggered
    else:
        # what is the day today?
        today = utime.localtime()[6]  # returns the weekday (value from 0 to 6)

        # and what day did we last trigger?
        last = utime.localtime(time_last_sample)[6]

        if today == last:
            return True  # yes we triggered today
        else:
            return False


def trigger_sampler_master():
    """Triggers the sampler immediately and logs it."""

    global bottles_used
    global time_last_sample

    # increment the number of bottles used
    bottles_used += 1

    # update the time of the last trigger
    time_last_sample = utime.time()

    # trigger sampler via the digital output line
    trigger_sampler_digital()

    # write a log entry
    reading = Reading(label="Triggered", time=time_scheduled(),
                      etype='E', value=bottles_used,
                      right_digits=0, quality='G')
    reading.write_log()


def trigger_sampler():
    """
    Call to attempt to trigger the sampler.
    Certain conditions may prevent the triggering.

    :return: True if sampler was triggered.
    """
    global bottles_capacity
    global bottles_used
    global time_last_sample

    trigger = True

    if bottles_used >= bottles_capacity:
        trigger = False  # out of bottles
    elif triggered_today():
        trigger = False  # already triggered today

    if trigger:
        trigger_sampler_master()  # Call routine that controls sampler.
        return True

    else:
        return False  # Sampler was NOT triggered.


@TASK
def trigger_task():
    """
    This function should be associated with a script task scheduled for a certain time every day.
    If the sampler has not been triggered today, this function will trigger it.
    """

    if trigger_sampler():
        # sampler was triggered

        # Write a log entry indicating why sampler was triggered.
        reading = Reading(label="DailyTrig", time=time_scheduled(),
                          etype='E', quality='G')
        reading.write_log()

    # add diagnostic info to the script status
    global bottles_capacity
    global bottles_used
    global time_last_sample

    print("Bottles used: {}".format(bottles_used))
    print("Bottle capacity: {}".format(bottles_capacity))
    if time_last_sample:
        print("Last trigger: {}".format(ascii_time(time_last_sample)))
        if triggered_today():
            print("   Which was today")
        else:
            print("   No trigger today")
    else:
        print("Not triggered since bootup")

Here is the completed Python script:


To integrate the script into the system, use LinkComm to load in the new script file. Follow the same steps as in the previous example, but choose the new script file this time.


How does this script report the status via LinkComm?

To use that feature, the setup and script will have to be sent to an actual XLink. Here are the steps to do so:

  • Run LinkComm

    LinkCommSL3Connect

  • Choose Station Type XLink 500

  • Click Connect


LinkCommImport
  • Go to the LinkComm menu and choose Import Setup
  • Choose the setup file sampler_daily_setup.txt and script file sampler_daily_lvl2.py

LinkCommSendSetup
  • Press the Changed button and choose Send Setup to Station

LinkCommScriptStatus
  • The script status will be displayed on the Script tab

LinkCommRunScript
  • If the script has not run, the status will indicate that
  • Click on S1, then click Run Script Now
  • The script status will show

Triggering the Sampler Based on Stage

Let us further build upon the example.

  • Our system measures water level (stage).
  • If the stage exceeds a certain threshold, we want to trigger the sampler.
  • Even if the stage does not exceed the threshold, we want to trigger the sampler at 8PM.

Let us put additional constraints on the sampler:

  • The sampler may only be triggered once per day.
  • If the stage is too low, we do not want to sample (no water to collect).

To make this happen we will need to

  • Add a stage measurement to the setup using LinkComm.
  • Connect the stage measurement to a script function that may trigger the sampler.
  • Add a script function that ensures the sampler does not trigger if the stage is low.

We can build upon the code we already have. Below are the new functions and modifications to trigger_sampler()

def stage_sufficient():
    """Returns true if the last measured stage is sufficient
    to trigger the sampler"""

    stage = measure("STAGE", READING_LAST).value  # what is the current stage?

    if stage > 2.0:
        return True  # yes there is enough water to sample
    else:
        return False  # water level too low to sample


@MEASUREMENT
def stage_sampler_check(stage):
    """ This function needs to be associated with the stage measurement.
    It will compare the current stage reading with the threshold.
    If the stage exceeds the threshold, it will try to trigger the sampler."""

    if stage > 5.5:
        if trigger_sampler():
            # sampler was triggered

    # makes sure the return the stage reading so the system can log it
    return stage


def trigger_sampler():
    """
    Call to attempt to trigger the sampler.
    Certain conditions may prevent the triggering.

    :return: True if sampler was triggered.
    """
    global bottles_capacity
    global bottles_used
    global time_last_sample

    trigger = True

    if bottles_used >= bottles_capacity:
        trigger = False  # out of bottles
    elif triggered_today():
        trigger = False  # already triggered today
    elif not stage_sufficient():
        trigger = False  # not enough water to sample

    if trigger:
        trigger_sampler_master()  # Call routine that controls sampler.
        return True

    else:
        return False  # Sampler was NOT triggered.

Here is the completed Python script:


LinkCommScript3
  • Load in the new script file.

Let us make the changes to the measurement setup.

LinkCommStage
  • Setup a measurement and call it STAGE. We reference the label STAGE in the script.
  • Tie the script function stage_sampler_check into the STAGE measurement.
  • Send the setup to XLink and you are all done!

Here is the completed Python script:


The examples below go even further, providing applications that trigger samplers on more complex conditions.