Writing Control Applications and Other Script Tasks ######################################################### In addition to measurement scripts and transmission formatting scripts, |short_port_name| 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 <|link_linkcomm_for_win|>`_ will suffice. The manual has crucial information about how scripts work. |link_satlink_manual| Please take the time to read the chapter on Python scripts. Without that knowledge, it is going to be difficult to work with |short_port_name| 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. |short_port_name| 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 |link_satlink_manual|. ------------ This code will trigger the digital line DOUT2 for 2 seconds .. code-block:: python 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 .. code-block:: python 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: * :doc:`library/sl3` 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: .. code-block:: python @TASK def trigger_task(): trigger_sampler_digital() * Note that the @TASK decorator is required for functions that will interface into the |short_port_name| setup. ------------ Here is the completed Python script: :download:`sampler_daily.py<./setup_script/sampler_daily.py>` ------------ 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: * :download:`sampler_daily_setup.txt<./setup_script/sampler_daily_setup.txt>` 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. .. code-block:: python # 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: :download:`sampler_daily_lvl2.py<./setup_script/sampler_daily_lvl2.py>` ------------ 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() .. code-block:: python 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: :download:`sampler_daily_lvl3.py<./setup_script/sampler_daily_lvl3.py>` ------------ |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: :download:`sampler_daily_lvl3_setup.txt<./setup_script/sampler_daily_lvl3_setup.txt>` ------------ The examples below go even further, providing applications that trigger samplers on more complex conditions. * :doc:`examples/auto_sampler_volume` * :doc:`examples/auto_sampler_eight_triggers` .. |LinkCommSL3Connect| image:: ./media/image30.png :width: 5.00000in :height: 3.70000in .. |LinkCommImport| image:: ./media/image31.png :width: 5.50000in :height: 4.00000in .. |LinkCommOpenScript| image:: ./media/image49.png :width: 6.00000in :height: 4.50000in .. |LinkCommDailyTask| image:: ./media/image50.png :width: 6.00000in :height: 4.50000in .. |LinkCommSendSetup| image:: ./media/image55.png :width: 5.00000in :height: 3.50000in .. |LinkCommScriptStatus| image:: ./media/image51.png :width: 5.80000in :height: 4.20000in .. |LinkCommRunScript| image:: ./media/image52.png :width: 6.00000in :height: 4.83000in .. |LinkCommStage| image:: ./media/image53.png :width: 5.00000in :height: 5.7000in .. |LinkCommScript3| image:: ./media/image54.png :width: 5.00000in :height: 3.6000in