OpenFlexure json return variable

After running main.py I get an error:

[pawel@phantom ~]$ python3 main.py
Current position: {"x": 0, "y": 0, "z": 0}
Homing
Traceback (most recent call last):
  File "main.py", line 64, in <module>
    values = [a for a in values if a >= 0 ]
  File "main.py", line 64, in <listcomp>
    values = [a for a in values if a >= 0 ]
TypeError: '>=' not supported between instances of 'str' and 'int'
[pawel@phantom ~]$ 

I assume that problem lies in plugin stage.py code. It looks like function doesn’t return correct value. The files main.py and stage.py are here.

After putting command print(values) I get: 'generator' object is not subscriptable.

The main.py part consists of

def mm(self, position, measurements, absolute=True):
    """Move the stage to a given position.
    WARNING! If you specify zeros, the stage might move a long way, as
    the default is absolute moves.  Position should be a dictionary
    with keys called "x", "y", and "z", although we will (for now) also
    accept an iterable of three numbers.
    """
    try:
        pos = {k: int(position[k]) for k in ["x", "y", "z"]}
    except:
        pos = {k: int(position[i]) for i, k in enumerate(["x", "y", "z"][:len(position)])}
    pos['absolute'] = absolute
    pos['measurements'] = measurements
    response = self.post_json("/extensions/com.openflexure.stage-mapping/actions/stage/move-measure/MoveMeasureAPI", pos)
    return response


microscope = ofm_client.find_first_microscope()
microscope = ofm_client.MicroscopeClient("192.168.0.226")
microscope.moveMeasure = mm


# homing routine
pos = microscope.position
print("Current position: " + json.dumps(pos))
print("Homing")
pos['x'] = 0
pos['y'] = 0
pos['z'] = 0
#microscope.move(pos)

zStep = 10
zRange = 1500
maxValue = -1
maxPosition = -1
for z in range(-micronToStep(zRange/2), micronToStep(zRange/2) - micronToStep(zStep), micronToStep(zStep)):
    pos = microscope.position
    pos['x'] = 0
    pos['y'] = 0
    pos['z'] = z
    values = microscope.moveMeasure(microscope, pos, 5)
    values = [a for a in values if a >= 0 ]
    values = np.asarray(values)
    value = np.median(values)

and the stage.py contain function MoveMeasureAPI:

i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
ads.gain = 16
ads.mode = ADS.Mode.SINGLE

chan = AnalogIn(ads, ADS.P0)

class MoveMeasureAPI(ActionView):
    args = {
        "absolute": fields.Boolean(
            missing=False, example=False, description="Move to an absolute position"
        ),
        "x": fields.Int(missing=0, example=100),
        "y": fields.Int(missing=0, example=100),
        "z": fields.Int(missing=0, example=20),
        "measurements": fields.Int(missing=0, example=100),
    }

    def post(self, args):
        """
        Move the microscope stage in x, y, z
        """
        microscope = find_component("org.openflexure.microscope")
        # Handle absolute positioning (calculate a relative move from current position and target)
        if (args.get("absolute")) and (microscope.stage):  # Only if stage exists
            target_position: List[int] = axes_to_array(args, ["x", "y", "z"])
            logging.debug("TARGET: %s", (target_position))
            position: Tuple[int, int, int] = (
                target_position[i] - microscope.stage.position[i] for i in range(3)
            )

            logging.debug("DELTA: %s", (position))

        else:
            # Get coordinates from payload
            position = axes_to_array(args, ["x", "y", "z"], [0, 0, 0])

        logging.debug(position)

        for x in range(position[0],):
            # Move if stage exists
            if microscope.stage:
                # Explicitally acquire lock with 1s timeout
                with microscope.stage.lock(timeout=1):
                    microscope.stage.move_rel(position)
        else:
            logging.warning("Unable to move. No stage found.")

        v=[]
        for i in range(0, args['measurements']):
            v.push(chan.value)

        return v

Does the microscope actually move on the values = microscope.moveMeasure(microscope, pos, 5) line?

Microscope doesn’t move on the values = microscope.moveMeasure(microscope, pos, 5). I checked journal on microscope server and here it is:

pi@microscope:~ $ ofm journal
-- Logs begin at Wed 2022-07-20 01:54:39 CEST. --
lip 20 01:54:55 microscope python[494]: INFO:root:Waiting for frames
lip 20 01:54:57 microscope python[494]: INFO:root:Creating locks
lip 20 01:54:57 microscope python[494]: INFO:root:Loading /var/openflexure/settings/microscope_settings.json...
lip 20 01:54:57 microscope python[494]: INFO:root:0 capture files found on disk
lip 20 01:54:57 microscope python[494]: INFO:root:0 capture files successfully reloaded
lip 20 01:54:57 microscope python[494]: INFO:root:Creating app
lip 20 01:54:57 microscope python[494]: Auto-scanning ports
lip 20 01:55:00 microscope python[494]: /var/openflexure/application/openflexure-microscope-server/.venv/lib/python3.7/site-packages/apispec/ext/marshmallow/common.py:131: UserWarning: Multiple schemas resolved to the name MoveStageAPIInput. The name has been modified. Either manually add each of the schemas with a different name or provide a custom schema_name_resolver.
lip 20 01:55:00 microscope python[494]:   UserWarning,
lip 20 01:55:00 microscope python[494]: INFO:root:Starting OpenFlexure Microscope Server...
lip 20 01:58:52 microscope python[494]: ERROR:root:Traceback (most recent call last):
lip 20 01:58:52 microscope python[494]:   File "/var/openflexure/application/openflexure-microscope-server/.venv/lib/python3.7/site-packages/labthings/actions/thread.py", line 255, in wrapped
lip 20 01:58:52 microscope python[494]:     self._return_value = f(*args, **kwargs)
lip 20 01:58:52 microscope python[494]:   File "/var/openflexure/application/openflexure-microscope-server/.venv/lib/python3.7/site-packages/flask/ctx.py", line 158, in wrapper
lip 20 01:58:52 microscope python[494]:     return f(*args, **kwargs)
lip 20 01:58:52 microscope python[494]:   File "/var/openflexure/application/openflexure-microscope-server/.venv/lib/python3.7/site-packages/webargs/core.py", line 450, in wrapper
lip 20 01:58:52 microscope python[494]:     return func(*args, **kwargs)
lip 20 01:58:52 microscope python[494]:   File "/var/openflexure/extensions/microscope_extensions/stage.py", line 168, in post
lip 20 01:58:52 microscope python[494]:     for x in range(position[0], ):
lip 20 01:58:52 microscope python[494]: TypeError: 'generator' object is not subscriptable

That is strange - I suspect the first error (the things you expected to be numbers actually being strings) is probably one you can figure out by printing values. I assume you are expecting values to be a list of numbers? I suspect instead it is a dictionary (almost all of the actions return dictionaries, even ones where the function just returns some text), and the list of numbers you’re after should be one of the keys, most probably output.

You are, however, probably not getting a successful result, so what you’re iterating on is most likely an error message from the server, not the result you hoped for…

Additionally, mm returns the response to your initial POST request. You might want to use microscope.poll_task to wait until the task is complete - that will ensure that the result is present. That should also give some more helpful error messages if the server code fails.

The error on the server side comes from this line. I am not sure what you’re intending to do here but I’ve tried to guess.

The first thing you can fix easily is that position is a generator rather than a tuple. This may be copy-pasted from code I wrote in the server, which was correct in earlier Python versions but doesn’t work in 3.7. You can trivially fix it by changing line 97 to have tuple( instead of just ( at the end. That will evaluate the generator and create a tuple instead. That said, I can’t remember if I use tuples or lists elsewhere.

Second, I’m not sure what the for loop that causes the problem is actually achieving - my guess is your intention is to:

  • move to a specific xyz position (absolute or relative)
  • move only in z (to x, y, z2) while measuring
  • return the list of measurements

I don’t think you need the for loop on 168 at all, as you only need two moves.

Additionally, if I’ve guessed your intent correctly, there’s a logic error with the second move: if it’s meant to be only in z, then the second (relative) move should always be [0, 0, args["z2"] - args["z"]]. The code as you have it would end up moving twice as far in x and y as intended, and doubling the initial z move as well.

I hope that’s helpful!

Thank you very much for your explanation.

I didn’t find any example about usage microscope.poll_task. Where should I place it?

Also you suggested

but I’m already using half of range in my code, i. e. main.py line 103. Is that what you mean?

I’ve read through again and I think the bit I was looking at was MeasureZAPI which perhaps you’re not using yet. I couldn’t completely follow what you were doing in main.py - happy to try to work through it if you add some comments or talk me through your intent, but for now I’ll try to help sort the confusing web API stuff, and leave you to figure out the logic once that works!

poll_task is not really documented, which is very much my bad. I really need to improve the docs for the microscope client! Firstly, I sent you in the wrong direction - the function is defined at module level, not as a method of the client object. So you need to do ofm_client.poll_task(r) (because you imported it with import openflexure_microscope_client as ofm_client). the r in that question is the return value from your POST request. That will mean it keeps checking the action until it has completed (or failed), then returns the result (which should be the return value of MoveMeasureAPI.post(), though it may be a slightly different format as it gets serialised to JSON and turned back into Python objects again.

That means that mm should look like:

import openflexure_microscope_client as ofm_client

def mm(self, position, measurements, absolute=True):
    """Move the stage to a given position while measuring photodiode signal.
    WARNING! If you specify zeros, the stage might move a long way, as
    the default is absolute moves.  Position should be a dictionary
    with keys called "x", "y", and "z", although we will (for now) also
    accept an iterable of three numbers.
    """
    try:
        pos = {k: int(position[k]) for k in ["x", "y", "z"]}
    except:
        pos = {k: int(position[i]) for i, k in enumerate(["x", "y", "z"][:len(position)])}
    pos['absolute'] = absolute
    pos['measurements'] = measurements
    response = self.post_json("/extensions/com.openflexure.stage-mapping/actions/stage/move-measure/MoveMeasureAPI", pos)
    return ofm_client.poll_task(response)

i.e. the last line should change.

I’m fairly sure that won’t fix everything, but hopefully it will help!

Also, as a hopefully-helpful tip, I make a lot of use of the logging module. If you put, at the top of main.py

import logging
logging.getLogger().setLevel(logging.INFO)

then anything logged at INFO level or higher will be printed. poll_task forwards log entries from the server, so if you use logging.info("message") or logging.warning("message") in stage.py, these should be printed to the console when you run main.py, which is often more convenient than looking through the server logs.