Nested sequences and Plant Groups

As a software developer, I like to keep things DRY (Don’t Repeat Yourself). As such, I’ve set out to create a large amount of atomic sequences (that I’ve called Actions) that can be composed together in parent sequences (that I’ve called Routines), and using the directory feature (thanks for this!) to somewhat keep my sanity.

I’ve just tested a seeding and watering sequence that runs on a Plant Group consisting of three plants, and I’ve bumped into an issue where it does not behave as desired (although while building out the sequences it dawned on me that it could behave the way I just observed).

Sequences

Action: Collect seed from seed bin

This sequence consists of:

  • Execute sequence: “Home safely”.
  • Safe Z Move to seed bin with an offset appropriate for the mounted needle.
  • Turn vacuum peripheral ON.

Action: Deposit seed at 5 mm below soil height

  • Safe Z Move to externally defined location, with an offset taking into account soil height, length of the needle and the desired depth of the deposit.
  • Turn vacuum peripheral OFF.
  • Mark As Status: Planted the externally defined location variable.

Subroutine: Sow 1x at 5 mm from seed bin

The 1x refers to the fact I desire one trip to the seed bin. Some seeds, like watermelon, are so big the needle will only catch one of them at a time, yet it’s a good idea to sow 4-5 seeds and thin later. Watermelon would use a Sow 5x at 12 mm from seed bin sequence.

This subroutine consist of:

  • Execute sequence: “Collect seed from seed bin”.
  • Execute sequence: “Deposit seed at 5 mm below soil height”. This sequence requires a variable definition, which is set to “externally defined”, passing it up further the chain.

Routine: Sow 1x at 5 mm for each plant from seed bin

  • Execute sequence: “Get Seeder”. This mounts the Seeder toolhead.
  • Execute sequence: “Sow 1x at 5 mm from seed bin”. This sequence requires a variable definition, which is set to “externally defined”, passing it up further the chain.
  • Execute sequence: “Return Seeder”. This unmounts the Seeder toolhead.

Plant Groups

Radishes A

A plant group with 0 automatic filters and 3 manually selected radish plants.

Regimens

Radish

The Radish test regimen has a Day 1 event set to “Sow 1x at 5 mm for each plant from seed bin”. This sequence requires a variable definition, which is set to “externally defined”, passing it up further the chain.

Farm Events

Radish Regimen

The Farm Events list contains an event that triggers the “Radish” regimen for today. This regimen requires a variable definition, which is set to “Radishes A”, passing the list of 3 radishes down the chain all the way to the “Sow 1x at 5 mm for each plant from seed bin” sequence.

Expected behaviour

  • Execute sequence: “Get Seeder”.
  • Execute sequence: “Sow 1x at 5 mm from seed bin” for Radishes A plant #1.
  • Execute sequence: “Sow 1x at 5 mm from seed bin” for Radishes A plant #2.
  • Execute sequence: “Sow 1x at 5 mm from seed bin” for Radishes A plant #3.
  • Execute sequence: “Return Seeder”.

Observed behaviour

  • Execute sequence: “Get Seeder”.
  • Execute sequence: “Sow 1x at 5 mm from seed bin” for Radishes A plant #1.
  • Execute sequence: “Return Seeder”.
  • Execute sequence: “Get Seeder”.
  • Execute sequence: “Sow 1x at 5 mm from seed bin” for Radishes A plant #2.
  • Execute sequence: “Return Seeder”.
  • Execute sequence: “Get Seeder”.
  • Execute sequence: “Sow 1x at 5 mm from seed bin” for Radishes A plant #3.
  • Execute sequence: “Return Seeder”.

How can I improve my nested sequences, regimens and farm events to achieve the expected behaviour without giving up the modularity of my sequences?

1 Like

I think one way to make it work is to pass in the “Radishes A” group earlier. For example, in the “Sow 1x at 5 mm for each plant radish” sequence. That stops the variable from being passed on to the parent, which normally makes the entire sequence loop-able.

But then I will need to make plant-specific sequences and that just doesn’t make sense to me. In my opinion, sequences should be almost completely plant-agnostic. They are simply a collection of commands to do things that every plant requires. Regimens (again in my opinion) are where things become plant-specific. A sequence is “water lightly” and “water heavily”. Regimens pick and choose the correct sequence at the correct Day to make it care for a type of plant specifically.

But even regimens are agnostic to which exact plants in your bed it targets. Farm Events take care of that. I don’t want to create a “Radishes A” regimen and an identical “Radishes B” regimen that targets another group. Both groups require the exact same plant care that a regimen provides. Both groups just need the care of a single, group-agnostic Radish regimen. The Farm Event ties the regimen to the group.

But as far as I can see (I really hope I’m overlooking something), the software won’t allow me to set up the dependencies like that. It seems to make things work correctly, I need to bake the groups into the sequences and simply call them from the Farm Events.

I don’t want to do that. Is there a way around this?

OK @mdingena we have done some investigation on our end. All four of us discussed your issue at our Friday planning meeting and although we think we understand the problem, no one was 100% certain, as the setup criteria seems a bit complex / hard to follow.

We think (and we might be wrong on this) that there is a misunderstanding about how variables get passed.

When you pass a point to child, the Celeryscript runtime will always pass the point to the child.

Pseudocode:

var point = point(123);
mySequence(point);

When you pass a point group to a child, the Celeryscript runtime will always transform the call to an iteration one level deep and no deeper than that.

So if you start with this:

var myGroup = [point(123), point(456)];
mySequence(myGroup);

The celeryscript runtime will, behind the scenes, tranform it to this:

mySequence(point(123));
mySequence(point(456));

The transformation stops there. It will not perform a deep transformation. Point groups cannot be passed around more than once since they are transformed one time. It is best to think of point groups similar to a spreadsheet range rather than an array in a traditional programming language.

EDIT I: If you’ve worked with functional languages that support macros, you can think of pointGroup passing more like a macro and less like a map iterations. PointGroup passing happens at “compile time” rather than run time.

2 Likes

Side note: The inability to pass point groups around is for extremely technical reasons and we do want to offer this ability eventually. It was a compromise we made so that the point groups feature could get in the hands of customers quickly rather than us needing to delay the release until a 100% perfect solution is available.

1 Like

If point groups can’t be “passed through”, then it’s impossible to pass a group to a sequence that does:

  • One-off command
  • Looped command
  • One-off command

In other words, it’s impossible to water many plants with just a single “mount watering nozzle” and “unmount water nozzle” action bookending both sides of the loop.

I don’t understand how everyone else uses their FarmBot. Does everyone else just hardcode the plant groups into their sequence? Then you can never reuse a sequence… you’ll have a “water my radishes” sequence and a “water my strawberries” sequence and a “water my basil” sequence. You’re just repeating yourself over and over and if you want to change the watering amount, now you’ll have to edit all those sequences rather than just one watering sequence that you can reuse.

Honestly, I can’t wait for this feature to exist.

@mdingena

You add a wrapper sequence or apply the groups at a lower level. There’s light repetition, but not like the pre-FBOS 8 days.

Here’s a typical workflow:

  • You create groups that match certain criteria. Eg: crop type, plant stage, etc…
  • You add plants to that group by ensuring they match the criteria (eg: don’t manually add them).
  • You create a single sequence that does one thing. Example: “Water a plant”, “photograph a plant”.
  • You create a wrapper sequence that applies the “one thing” to many plants. Eg: “Photograph all plants”, “Water all radishes”.
  • You schedule the wrapper sequence in a farm event.

In the case above, you would ideally have only one sequence. You instead create two: an isolated action (water, photograph, etc…) plus a “wrapper” that applies that isolated action to a group. It’s inconvenient for advanced use cases, but it’s by no means a show stopper- in this case the ideal scenario would be a single sequence, but we instead needed to create two.

As you change a plant’s properties, it will move in and out of different groups. As its membership in the group changes, the sequences that will act upon each individual plant will change also (the group membership is calculated immediately before the sequence runs).

This workflow is incompatible with Genesis devices because we need tool mounting and unmounting sequences that the Express doesn’t need. We have no place to put the mounting sequences as a one-off action. You must either leave them out (so you water with no nozzle) or you add them, but then the nozzle is mounted and unmounted for every plant. Both of these are no good to me.

The only way currently is to schedule a mounting sequence in Farm Events, then later schedule another Farm Event that does the watering, and last you schedule an unmounting sequence an estimated time after the watering. I really hope that is not the end state of sequences because this is not a good way of dealing with the problem.

Watering sequences should be runnable on their own. In my opinion, that includes preparing the UTM for the task. I do not want to decouple tool mounting and actions, they are inherently related. In my opinion we must be able to colocate these.

I am a Genesis owner also. I have a “lab” setup for an Express bot, but my real bot in the Garden is a Genesis model and I am able to perform all operations on it just fine with wrapper sequences.

I would not recommend trying to time your farm events in this manner. I don’t think it will work (though I haven’t tried), and you won’t find that recommendation anywhere in the docs. The best way to get around the limitation is to EXECUTE the mount/unmount sequence in the wrapper sequence before / after iteration runs:

Example:

“WATER ALL” Wrapper Sequence:

  1. Mount watering nozzle
  2. Execute the “WATER A PLANT” sequence and pass in a group XYZ.
  3. Unmount watering nozzle.

“PHOTOGRAPH ALL” Wrapper Sequence:

  1. Execute the “PHOTOGRAPH PLANT” sequence and pass in group XYZ.

“WATER AND PHOTOGRAPH ALL” Wrapper Sequence:

  1. Execute “WATER ALL”
  2. Execute “PHOTOGRAPH ALL”

It is not. Like a lot of things on the roadmap, it takes time and resources.

1 Like

This is the issue. You cannot make a generic “water plants lightly” sequence because you must hardcode the group into the sequence.

You cannot have:

  • Water plants lightly
  • Water plants heavily

You must instead make

  • Water radishes lightly
  • Water cherry tomatoes lightly
  • Water strawberries lightly
  • Water basil lightly
  • Water bok choy lightly
  • Water chives lightly
  • Water watermelon heavily
  • Water cucumber heavily
  • Water zucchini heavily

As soon as you make it a generic sequence by passing in an external variable instead of a hardcoded plant group, a Genesis will start mounting and unmounting for every plant.

Your software documentation (v13) hints at the fact we should be able to do this.

This sequence can only be used to water the specific Spinach plant chosen. Instead of making watering sequences for every plant in your garden, make this sequence a generic Water plant sequence that can be used on any plant by using a location variable.

Source. Bold emphasis mine.

But this won’t work. Well, it does, but Genesis will mount and unmount the toolhead for every plant.

1 Like

Didn’t see any more replies to the post. RickCarlino is there a reply to mdingena’s last comment?

I am having a problem understanding how to create a wrapper function like you state. I am hitting the same issue in which my bot wants to mount the tool after each planting sequence. Can you include screenshots for how you did this?

Sure thing @Jarvisdvs.

As a note to anyone who finds this post via search, we offer a tutorial on how to perform seeding here.

Since we already have documentation on how to mount tools, plant seeds etc. I will focus instead on the main question of how to build wrapper sequences (see screenshots above).

I am happy to go into deeper detail if you are still unsure of how to proceed. Please let me know- we’re here to help.

It’s been a while since this discussion, and being that it was a very lengthy one, I apologize if I am forgetting details. I am happy to clarify if needed and apologies ahead of time if I am misunderstanding your question.

Marc’s main point that it is less-than-ideal still stands, and we by no means want this to be the long-term solution. The current solution is still to do the wrapper. The reasons for this are very specific to some FBOS internals and will take a bit of effort on our end to address (more than our two-person dev team can support at the moment, unfortunately), but in the meantime, it is still possible to work around this limitation via wrappers:

image

1 Like

Hi @RickCarlino thanks a lot for the detailed answers and for your patience to understand your customers problems.
I would like to ask if in the last 2 years there has been any improvement regarding this?
I think that creating wrappers for every plant group is not very practical, specially for the XL version where we can have lots of different plants.

What I am looking for is for a sequence where first I check the soil moisture at one point and then the sequence decide if to water the Externally defined plant group or not. However that is not currently possible because then the Soul Moisture measurement is repeated for each plant in the group.

What I want to do:

Variables:

  • Measure Point - Externally defined
  • Plant group - Externally defined

Steps:

  • Mount Soil Moisture tool
  • Go to “Measure Point”
  • Measure Soil Moisture
  • Unmount Soil Moisture tool
  • If Moisture lower than XX
    • Mount Water Tool
    • Call Water plant sequence on “Plant group”
    • Unmount Water Tool

However as now, this is not possible because when I call this sequence it will repeat the Soil Moisture measurement for each plant in the Plant Group.

Do you have a suggestion on how to accomplish testing the Soil Moisture once in an area and then decide if watering the plants in a plant group or not?

Thank!!
Greetings from Germany! :slight_smile:

Edit: Just to add. We currently have 36 plant groups with different watering needs. It would not be practical to have 36 wrapper sequences. I hope there is a more practical way of accomplishing this.

1 Like

@juangrados Unfortunately this still isn’t possible with basic sequences and variables because a group cannot be passed through a sequence into a subsequence without triggering the parent to run for every group member.

However, if you are willing to play around with some custom Lua code in your sequences, here is a workaround idea you can try. I’ve set up my sequence variables as follows:

And then I added one Lua command to the sequence with the following code:

-- Mount Soil Sensor Tool
soil_sensor_tool = variable("Soil Sensor Tool")
mount_tool(soil_sensor_tool)

-- Move to Measure Point
measure_point = variable("Measure Point")
move{
  x = measure_point.x
  y = measure_point.y,
  z = measure_point.z,
  safe_z = true
}

-- Measure Soil Moisture
soil_sensor_pin = 59
soil_moisture = read_pin(soil_sensor_pin, "analog")

-- Unmount Soil Moisture tool
dismount_tool()

-- Check if Moisture is below threshold
moisture_threshold = 500
if soil_moisture < moisture_threshold then
  -- Mount Water Tool
  water_tool = variable("Water Tool")
  mount_tool(water_tool)

  -- Iterate over plants in group
  for i,member in ipairs(group(variable("Group ID")) do
      -- Get plant object
      plant = api({
          method = "get",
          url = "/api/points/" .. member
      })

      -- Move to plant and water
      move_absolute(plant.x, plant.y, 0)
      water(plant)
  end

  -- Unmount Water Tool
  dismount_tool()
end

Instead of passing in the plant group directly, we’re passing in the plant group’s ID, which will not trigger the sequence to run in its entirety for every group member. Instead, the iteration is determined by the Lua code and only happens when needed.

To find a group’s ID, navigate to it in the app and look for the last part of the URL (the numbers), for example: my.farm.bot/app/designer/groups/1690

While this is definitely a workaround, it will allow you to not need 36 wrapper sequences :sweat_smile: Hope this helps!

2 Likes