Telemetry.update() behavior inconsistent between Blocks and OnBotJava (OBJ)

Apologize in advance for this rabbit hole.

I’m trying to learn how to create myBlocks using OnBotJava (OBJ), for subsequent use in Blockly or OBJ opmodes.

In that effort, I have an OpMode that does some telemetry.addData and I have some telemetry.addData within the myBlock.

Initially, I had a telemetry.update() at the end of the runOpMode loop.
That turned out to be “confusing” and I found that the telemetry.addData() that were at the runOpMode level were being displayed, but those nested within the myBlock were not.
So, I put telemetry.update() in the myBlock and that displayed the myBlock ones, but cleared the runOpMode() ones.

OK, so that is all confusing, but the two update()s clobbering each other is more or less expected. What isn’t expected (by me) is that an update() in the runOpMode gets the addData() from within runOpMode and an update() within the myBlock gets the addData() from within the myBlock and with no update() no addData is displayed. (This last disproves the idea that a myBlock has a “built-in” update() which is clearing the telemetry buffer or something).

I find that this behavior is different when running a Blockly OpMode than running an OnBotJava OpMode, even though the OBJ one is a direct copy/paste from the java display of the Blockly OpMOde.

When running the Blockly version, I get all the addData().
When running the OBJ version, I get EITHER the addData() from the RunOpMode method OR the addData() from the myBlock, but never both, depending on whether the telemetry.update() is in RunOpMode or within the myBlock.

Is there a way to attach files? hmmm. I rather dread pasting in the pictures.

This is an excellent question for @lizlooney

Here is the Blockly picture:

and here is the OBJ myBlock:
package org.firstinspires.ftc.teamcode;

import org.firstinspires.ftc.robotcore.external.BlocksOpModeCompanion;
import com.qualcomm.robotcore.util.ElapsedTime;
import org.firstinspires.ftc.robotcore.external.ExportToBlocks;

public class SampleMyBlocks extends BlocksOpModeCompanion {
static int myVariable = 10;
private static ElapsedTime myStopwatch = new ElapsedTime();
static boolean wasAPressed = false;
static boolean wasYPressed = false;

@ExportToBlocks (
    comment = "put inside a repeat loop. Press X to reset.",
    tooltip = "Stopwatch on gamepad button X."
)
public static void stopwatchX() {
    telemetry.addData("Stopwatch timer", "%.2f", myStopwatch.time());
    telemetry.addData("To reset stopwatch", "press X");
    telemetry.addData("counterNested", myVariable);

    if (gamepad1.x) {
        myStopwatch.reset();
    }
    if (gamepad1.y) {
        // increment myVariable once each time Y is pressed
        telemetry.addData("wasYPressed",wasYPressed);
        if (!wasYPressed) {
            incrementMyVariable();
            telemetry.update();
        }
        wasYPressed = true;
    } else {
        wasYPressed = false;
    }
    if (gamepad1.a) {
        telemetry.addData("wasAPressed",wasAPressed);
        if (!wasAPressed) {
            telemetry.update();
        }
        wasAPressed = true;
    } else {
        wasAPressed = false;
    }
    if (gamepad1.b) {
        telemetry.update();
    }

}

public static void incrementMyVariable() {
    myVariable++;
}

}

and here is the OBJ equivalent of the Blockly:

package org.firstinspires.ftc.teamcode;

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;

@TeleOp(name = “Test_myBlocks_v01 (OBJ version)”)
public class OBJTest_myBlocks_v01 extends LinearOpMode {

/**

  • This function is executed when this OpMode is selected from the Driver Station.
    */
    @Override
    public void runOpMode() {
    int count;
// Put initialization blocks here.
count = 0;
waitForStart();
if (opModeIsActive()) {
  // Put run blocks here.
  while (opModeIsActive()) {
    // Put loop blocks here.
    count = count + 1;
    telemetry.addData("Count", count);
    // put inside a repeat loop. Press X to reset.
    SampleMyBlocks.stopwatchX();
    if (gamepad1.left_bumper) {
      telemetry.update();
    }
  }
}

}
}

There should not be any differences between a Blocks OpMode that calls the SampleMyBlocks.stopwatchX MyBlock and an OBJ OpMode that calls SampleMyBlocks.stopwatchX method.

It doesn’t matter where the call to telemetry.update() is placed (in blocks vs in the MyBlock code), it matters when it is called. When telemetry.update() is called, it sends (to the driver station) all the telemetry data that has been collected since the last time it was called.

Does that make sense?

Thanks. Yes, that makes sense. It just doesn’t match observations.

In the example/test program I’ve been working with, I’ve put all the telemetry.update() calls inside some conditional so they only happen when requested (by pushing a gamepad button), and in addition to the previous observation that the update() in runOpMode in OBJ only displays the addData from runOpMode, and the update() in myBlock only displays the addData from myBlock, it feels like the update() needs to have happened at the time the telemetry data gets sent. i.e. if there are further addData() before the next Send, those data don’t go.
(It is a little hard to have confidence in this observation. That aspect seems the same in both Blockly and OBJ).

If this timing on telemetry.update() really is important to getting the collected data Sent, then perhaps the reason it is different between the Blockly opmode and the OBJ opmode is related to timing quirks of the invocation of myBlock from Blockly vs. from OBJ, though I really don’t see a reason there would be any difference (i.e. I assume the Robot Controller “runs” the Java code in both cases, and is not even aware if it came from a Blocks program or an OBJ program).

Management summary: I think there is something about addData wiping the collected Items before the end of the TransmissionInterval. perhaps crazy talk.
I’m going to slow down the TransmissionInterval and see if that illuminates anything for me.

thanks.

Ok. this reply is just about telemetry.update() from within an OnBotJava OpMode.

with telemetry.setMsTransmissionInterval(2000) it is clear that no “update” occurs on the Driver Station (DS) if telemetry.update() is at the bottom of runOpMode unless the call to telemetry.update() happens at the expiration of the TransmissionInterval. If it is, the the Items that have been collected within runOpMode are sent and displayed on the DS.
telemetry.update() calls that happen in the “middle” of the TransmissionInterval do NOT result in an update on the DS (not even an update queued up, that gets delivered at the end of the TransmissionInterval).
Data collected via transmission.addData() within the myBlock are not included in the update and not displayed on the DS.

If the telemetry.update() occurs within the myBlock, then all the data collected within the myblock (but not the values collected in runOpMode()) are immediately displayed on the DS, without waiting for the 2000 ms TransmissionInterval to end.

Management summary: do the OBJ OpMode and the myBlock share the same telemetry objects, queues, timers, etc?

[update:]
Also find that telemetry.setMsTransmissionInterval(4000) from within myBlock sets the TransmissionInterval for telemetry.update()s within the myBlock, but does not affect those at the outer runOpMode level. i.e. the OBJ OpMode runOpMode() has one TransmissionInterval and the myBlock has a different TransmissionInterval.

(and still finding that the update() needs to happen at the TransmissionInterval and calling it in the middle of the TransmissionInterval does not result in a DS display update (ever).
It doesn’t seem to be as simple as not liking more addData() after the update()).

[another update:]
telemetry.setAutoClear(false) causes the DS to NOT display the telemetry data collected via addData() and impelled via update(). Interestingly, setAutoClear on the telemetry object visible in runOpMode is independent from setAutoClear on the telemetry object visible in a myBlock.
(this is similar to observation in previous post update saying that setMsTransmissionInterval()s seemed to be independent).

Management summary: is there only one “telemetry” object, or does a myBlock have a different instance of one than other OBJ classes?

Oooh. That might be the problem. I’m not sure how a BlocksOpModeCompanion subclass gets its telemetry object.

I think you can make your stopwatchX method take a Telemetry parameter and blocks will automatically pass in the telemetry object that belongs to the opmode. (There won’t even be a socket on the MyBlock for it, since it is passed automatically.)

Try that and let me know if it fixes the problem.

Hmm. I think we are barking up the right tree, and what you said about “There won’t even be a socket on the MyBlock…” is true.

now, if I run the OBJ version of the OpMode from a power-up, I get an exception
Attempt to read from field ‘boolean com.qualcomm.robotcore.hardware.Gamepad.x’ on a null object reference.

Then if I run the Blocks version of the OpMode, that works OK, including getting all the telemetry I expect (except that I still don’t think an update() is getting queued as expected).

After running the Blocks version, the OBJ version also works fine.

So, somehow the OBJ version doesn’t get gamepad1 initialized, but since it is “public static Gamepad gamepad1”, after the Block Opmode properly initializes it, subsequent runs of the OBJ opmode find gamepad1 and work OK (because “static”).

gamepad1 and telemetry (etc) are public in the BlocksOpModeCompanion, and are not part of its Construction (afaict), so what fills those in? Is that different on Blocks vs. OBJ?

BlocksOpModeCompanion also has
public static OpMode opMode;
public static LinearOpMode linearOpMode;
and opMode extends OpModeInternal, which has a “telemetry” object.

Management summary:

  1. In OBJ, why would adding a telemetry parameter to a myBlock method cause initialization of gamepad1 to get skipped for the myBlock?
  2. Is the initialization of a MyBlock after Construction the same when called from Blocks than when called from OnBotJava (OBJ)?

Bonus question, mostly unrelated:
3. If a myBlock is implemented in AndroidStudio, it is available in Blocks, but seems not visible in OBJ. Is there a way to make it visible?

I figured out why you get odd behavior when you run the OBJ OpMode is a direct copy/paste from the java display of the Blockly OpMode.

  • Both the Blockly OpMode and the OBJ OpMode (created from copy/paste from Blocks) call the method SampleMyBlocks.stopwatchX.
  • SampleMyBlocks.stopwatchX uses telemetry.
  • SampleMyBlocks extends BlocksOpModeCompanion.
  • BlocksOpModeCompanion has a static field named telemetry. You can see the source for BlocksOpModeCompanion here.
  • When you run a Blocks OpMode, the actual OpMode that is run is an instance of a class named BlocksOpMode, which is part of the FTC SDK.
  • The BlocksOpMode.runOpMode method sets all the BlocksOpModeCompanion fields. You can see that code here.
  • But, when you run a Java OpMode, (written in OBJ or Android Studio), the fields in BlocksOpModeCompanion are not set. The value of BlocksOpModeCompanion.telemetry remains whatever it already was.
    • If you had previously run a Blocks OpMode, it would be the telemetry that was used during that previous OpMode’s run. That old telemetry object is not the current telemetry object and does not work correctly.
    • If you hadn’t previously run a Blocks OpMode, it would be null and running the OnBotJava OpMode that uses the MyBlocks will crash with a NullPointerException.

If you want to write Java code that is used as a MyBlock and also used from Java OpModes, your class should not extend BlocksOpModeCompanion.

Instead, you can put the @ExportClassToBlocks annotation on your class. That will signal to the Blocks editor that it should look for methods in the class that have the @ExportToBlocks annotation. Then your method should take a Telemetry parameter. From Blocks (as I mentioned before), this will be filled in for you. From a Java OpMode, you can pass telemetry when you call the method.

Like this…

The MyBlocks class:

package org.firstinspires.ftc.teamcode;

import org.firstinspires.ftc.robotcore.external.ExportClassToBlocks;
import org.firstinspires.ftc.robotcore.external.ExportToBlocks;
import org.firstinspires.ftc.robotcore.external.Telemetry;

@ExportClassToBlocks
public class SampleMyBlocks  {
  @ExportToBlocks (
    comment = "put inside a repeat loop. Press X to reset.",
    tooltip = "Stopwatch on gamepad button X."
)
public static void stopwatchX(Telemetry telemetry) {
    telemetry.addData("stopwatchX", 123);
  }
}

The Blocks OpMode:
MyBlocksTelemetry

The Java OpMode:

package org.firstinspires.ftc.teamcode;

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;

@TeleOp(name = "OBJOpMode")
public class OBJOpMode extends LinearOpMode {

  @Override
  public void runOpMode() {
    while (opModeInInit()) {
      telemetry.addData("OBJ", "abc");
      SampleMyBlocks.stopwatchX(telemetry);
      telemetry.update();
    }
  }
}

now, if I run the OBJ version of the OpMode from a power-up, I get an exception
Attempt to read from field ‘boolean com.qualcomm.robotcore.hardware.Gamepad.x’ on a null object reference.

What I wrote above about telemetry in BlocksOpModeCompanion also applies to gamepad1 and gamepad2. That’s why you get an exception when you run the OBJ after a power-up.

The Blocks environment is smart enough to know how to pass the telemetry object that belongs to the Blocks OpMode when it calls a MyBlock method that has a telemetry parameter. It’s not that the initialization is skipped. It’s that the Blocks environment already knows how to pass the telemetry parameter when it calls the method.

It appears that when you compile in OBJ, it doesn’t know about the TeamCode classes and methods that you added in Android Studio. I can confirm that I also get an OBJ compiler error for this. I’ll ask the rest of the FTC Tech Team to see if they are already aware of this limitation.

If I correctly understand your explanation of my question:

Then my Null Pointer exception on gamepad1 had nothing to do with adding a Telemetry parameter to my myBlock, it was just luck, and “cold” running any OBJ OpMode created by copy/paste from Blockly’s “show java” which uses a myBlock (aka BlocksOpModeCompanion) will crash with a null pointer exception on references to gamepad or telemetry, etc.

And this is exactly the case.

It turned out that I had never run an OnBotJava opmode without previously running a Blockly flavor of the OpMode until I added the telemetry parameter to see if that fixed the telemetry surprises. I correlated adding the Telemetry parameter on my BlocksOpModeCompanion method with the Null Pointer Exception on references to gamepad1, but it had nothing to do with that, it coincidentally was the first time I had run the OBJ opmode from a cold start of the Robot Controller app.

I think it turns out that if your OBJ OpMode does the initialization that is done for Blockly OpMode:
BlocksOpModeCompanion.opMode = this;
BlocksOpModeCompanion.linearOpMode = this;
BlocksOpModeCompanion.hardwareMap = hardwareMap;
BlocksOpModeCompanion.telemetry = telemetry;
BlocksOpModeCompanion.gamepad1 = gamepad1;
BlocksOpModeCompanion.gamepad2 = gamepad2;
Then the myBlock will work when run from OBJ or Blockly.

I’m not a Java authority, but I gather that, since these member variables are static within the BlocksOpModeCompanion class, setting them once at the top of your OpMode’s runOpMode() will make them available to all of the MyBlocks used by your OpMode.
(and in fact, available to all the MyBlocks used by other OBJ OpModes until a reboot of RC).

Unrelated to this topic of discussion, but interestingly coincidental with a RC version 10.2 change, our team had never seen the problem with DcMotor RUN_TO_POSITION before setting what position to run to with DcMotor.SetTargetPosition() until our first competition this year. Our bug lay dormant because we always tested with a teleop OpMode first, which had them in the right order, so subsequently running an autonomous OpMode with them in the wrong order didn’t fail because the target position had been previously set in the teleop OpMode (albeit to the wrong value). At competition, the auto OpMode was run from a cold start, and crash. This was another example of one OpMode accidentally relying on data from a different OpMode being left around.
I saw another of the same crashes in competition last week.

Management summary:

  1. OnBotJava does not initialize BlocksOpModeCompanion members such as telemetry and gamepad1. You can do it in your OBJ runOpMode() (but that is probably a maintenance burden.
    i.e. bad style for the user of a class to know its internals).
  2. a Blocks OpMode doesn’t simply run like any other OpMode, it is run by a special opmode, BlocksOpMode (which extends LinearOpMode). BlocksOpMode sets up the environment needed to run the Blocks in your Blockly OpMode (including telemetry and gamepad).
  3. Sometimes it is important to remember that your “program” doesn’t “start” with the Init or Play button on the Driver Station. It “starts” when the RobotController app boots up, usually on powerup of the ControlHub or (phone) USB connection to ExpansionHub. Some data persists across invocations of an OpMode. Recommend testing all your OpModes from a cold start.

Thanks for your help on this. A deeper rabbit hole than I expected, but glad to have some light shed on it.

–david

1 Like