Announcement of a Python client library & some questions

Hi, we are new kids on the FarmBot block. We are part of Xebia, a software consultancy, and we have built and are setting up a FarmBot in our main office.

Since we are software developers we felt we were constrained too much by the current sequence editor (even though it’s very nice for non-developers :slight_smile: ). We’ve been looking for ways to use the power of programming languages and are creating several different ways to do this. One of them is a client library similar to farmbot-js for Python (but only the subset needed for sequence programming). It connects to the MQTT broker to do its thing (the Python examples at https://github.com/FarmBot-Labs/FarmBot-Python-Examples) were very helpful.

The code can be found on GitHub: https://github.com/xebia/farmbot-py/

Of course there’s always more to improve, but it’s ready for someone who would like to have something that successfully provides a Python API. I’ve created a synchronous programming experience by waiting for the “rpc_ok” messages with the correct label (as was stated as an issue in Asking for farmbot --> client help using python & mqtt).

Having said that, we have the following issues which I’ve summarized in code.

from farmbot.bot import FarmBot, Axis
from farmbot.connection import FarmBotConnection
from farmbot.config import FarmBotConfiguration, ToolBay

cfg = FarmBotConfiguration('./config.json')   
bot = FarmBot(cfg, FarmBotConnection(cfg))
try:
    # Issue 1
    # The corpus says that kind 'home' is the message to use, but in practice it's 'find_home'.
    # Corpus: https://github.com/FarmBot/farmbot-js/blob/master/src/farmbot.ts Line 155-163

    # Issue 2
    # The 'home' message ignores the axis parameter and always goes to (0, 0, 0)

    # bot.go_home(Axis.z)

    # Issue 3
    # A 'read_pin' message responds with an
    # Unknown syncable: Elixir.Farmbot.Asset.SensorReading error message

    # {'kind': 'rpc_error', 'comment': None, 'body': [{'kind': 'explanation', 'comment': None, 'body': [],
    #       'args': {'message': 'Unknown syncable: Elixir.Farmbot.Asset.SensorReading'}}],
    #       'args': {'label': 'f47ebec0-fe15-42b6-9424-a4f78c79fbcb'}}

    bot.verify_tool()
finally:
    bot.stop()

Who would be the best place / and or person to contact on these issues? Is this forum the place?

4 Likes

Cool!
Yup, I think this is the right place…
You just need a flag for the gurus…
@RickCarlino
@Gabriel
@connor
@roryaronson

@sbeaumont I’m glad the corpus and reference implementations (FarmbotJS) were a good starting point. I will forward the three issues above to @connor for review.

Our developer support is still a work in progress. I appreciate your help in identifying “surprising” behavior in the platform. Real world feedback helps me improve developer documentation and adjust our APIs to conform to how 3rd party developers are actually using the platform. Thanks!

1 Like

This seems like a bug in the firmware. I don’t think we actually use this rpc anywhere, but i just checked
and farmbot os supports this. It sent the correct command, but spun all three motors anyway. We will investigate this.

I think this one might be be a combination of errors on both sides. (the python client & farmbot_os)
read_pin now has a side effect of creating a sensor reading, which is an asset that FarmBot does not sync.
I suspect your 'label’s are being crossed at some point. This is the response to an auto_sync rpc that was not sent by your python library, but the Farmbot API. That said i’ve noted this as a bug and am fixing it.

1 Like

Indeed, you’re right. It was a combo of errors. The read_pin just returns a neat ‘rpc_ok’. But then I’m stumped how to actually read the pin. How does the return value come back?

In fact, it’s not even about reading a pin in general. What I specifically want is detect which tool is currently connected to the universal tool mount so the scripts can take that into account. But I now see no way to do that.

I’ve also analyzed the status tree, but it doesn’t have that much “live” data, the bulk of it is relatively stable configuration stuff. So far the location and pin 8 seem to be the only things?

On a side note, I’m pretty happy with the developer experience of the code so far. This is what a simple watering script could be. (The calibration is not really needed normally, and the verify_tool is something to make the scripts more robust - and not working atm. Not really nice to pick up a tool if another one is mounted :-)).

from farmbot.bot import FarmBot, Axis
from farmbot.connection import FarmBotConnection
from farmbot.config import FarmBotConfiguration, ToolBay

cfg = FarmBotConfiguration('./config.json')
bot = FarmBot(cfg, FarmBotConnection(cfg))
try:
    bot.calibrate(Axis.all)
    if not bot.verify_tool():
        bot.pick_up_tool(ToolBay.Watering_Nozzle)
    bot.water((1140, 110), 3)
    bot.water((820, 520), 3)

finally:
    bot.stop()

bot.water parameters are just the x,y since I’ve just set the height to max height (z=0) since that works well enough and looks cool :-). The second parameter is the number of seconds to release water.

3 Likes

This has been a source of confusion previously, and @RickCarlino and I have been brainstorming better ways of doing this sort of thing. Basically right now we have to implement this really clunky workflow:

send read_pin rpc, send read_status rpc, and observe the state change of pins on the status tree.
There is currently no real documentation of which rpcs do what and what side effects it has on the system. Rick and I are working on better solutions currently, but nothing to report yet.

Okay, understood.

However, I only see one pin in my status tree. It’s the JSON coming out of the /status channel, right? When reading the status tree I watch for changes after filtering out the wifi strength and the raw encoders, which jitter a bit and always change.

My status tree only shows pin 8, which never changes.

  "pins": {
    "8": {
      "value": 0,
      "mode": 0
    }
  },

These two items “jitter” for me, making it hard to detect a real status change:

  "informational_settings": {
    "wifi_level": -66,
    "raw_encoders": {
      "z": 3,
      "y": 18724,
      "x": 29519
    },

So therefore my listener code is changed to ensure it’s not as “chatty”.

def on_message(client, userdata, msg):
    global prev_status
    global ping_count
    json_message = json.loads(msg.payload)
    if (msg.topic.endswith('from_device') or msg.topic.endswith('from_clients')) and (json_message["args"]["label"] == "ping"):
        # Only print a ping every once in a while
        if ping_count >= REPORT_PING_ONCE_PER:
            print(str(msg.topic) + " " + str(json_message))
            ping_count = 0
        ping_count += 1
    elif str(msg.topic)[-6:] == 'status':
        # Only report the status if it changes
        stat = json.loads(msg.payload)
        # Fiddly values that change all the time without much effect.
        stat['location_data']['raw_encoders'] = ''
        stat['informational_settings']['wifi_level'] = 0
        if prev_status != stat:
            print(str(msg.topic) + " " + str(json_message))
            prev_status = stat
    else:
        print(str(msg.topic) + " " + str(json_message))
1 Like