UnexpectedTypeException: Expected a Toybox.Lang.Number or WatchUi.BitmapResource

One of my users reported this error, but unfortunately I don’t have a stack trace to go on.
I’m hoping to tap into the swarm intelligence here - does anyone know of an SDK function that accepts a parameter which can be either a Number or a BitmapResource?
I’ve checked my own code and don’t see any instances of this.

  • TL;DR:

    - the-ninth originally had code like this:

    var b = new Bitmap({ :bitmap => someBufferedBitmap });

    - this fails in the simulator with "Could not find symbol 'getHeight'" (and "Could not find symbol 'getWidth'", if the previous error is somehow resolved)

    - the-ninth tried to work around this by subclassing BufferedBitmap and adding getHeight / getWidth methods in the subclass. (Based on the error mesaages and on the fact the newer versions of BufferedBitmap have those functions, but older versions do not.)

    - this worked in the sim (again, if you really think about it, this points to a serious problem in the sim, because no sane API would work this way). There's obviously more to creating a Bitmap from a BufferedBitmap than simply having getWidth() and getHeight() available on the latter - the Bitmap initializer (or setBitmap) has to know exactly what to do to create a Bitmap from a BufferedBitmap, meaning it has to have BufferedBitmap-specific code. But why would it have BufferedBitmap-specific code when a normal BufferedBitmap is clearly not supported "out of the box"? Btw, a BufferedBitmap is *not* a subclass of Bitmap.

    EDIT: ok it's possible that nothing more is required than getWidth / getHeight

    To be clear: although it may seem like a tautology / circular reasoning: the fact that BufferedBitmap doesn't work out of the box suggests BufferedBitmap support doesn't exist. I am saying there is no reason to think that Bitmap.initialize / Bitmap.setBitmap would support a custom subclass of BufferedBitmap when it doesn't support the BufferedBitmap base class in the first place. (Other than the broken behaviour in the sim.) Unless ofc you assume that the lack of BufferedBitmap out of the box is simply due to a hilarious oversight where Garmin "forgot" to implement getWidth / getHeight on BufferedBitmap, but actually implemented other BufferedBitmap-related functionality that would work if only we devs added those methods manually in a subclass

    Ofc a big problem here is that the correct behaviour is only exhibited by a real device.

    - this ofc does not work on a real device

    - the device throws an exception from Bitmap.setBitmap (which is called by Bitmap.initialize): "UnexpectedTypeException: Expected a Toybox.Lang.Number or WatchUi.BitmapResource"

    The most reasonable explanation, based on looking at old API docs, is to take the device at its word when it tries to tell us that its version of Bitmap.initialize / setBitmap only accepts resource IDs (Number) and BitmapResource.

    This relates back to this question in the OP:

    does anyone know of an SDK function that accepts a parameter which can be either a Number or a BitmapResource?

    Yes, now we do. It's most likely an old version of Bitmap.setBitmap(), which is called by Bitmap.initialize().

    Honestly the entire discussion about the workaround of subclassing BufferedBitmap is a red herring [*], and the fact that it works in the sim again points to a huge deficiency in the sim (and documentation). It's not (as much of) a problem on a real device and it's not a limitation of the Monkey C language. In fact the Monkey C language actually does allow the workaround.

    [*] except for the fact that had the workaround never been attempted, this topic would not exist at all

  • Yes, I see your point. Honestly, I took the complaint about the missing getWidth and getHeight at face value and added them to see if it would help. To be fair, I don’t think a Bitmap necessarily needs more than that. I’m not sure how Garmin handles it, but in my own BufferedBitmapDrawable, width and height are enough to calculate coordinates - Dc.drawBitmap does the rest.

    I agree the real issue lies with the simulator.

    I regularly test in the simulator across different API levels and would’ve noticed this immediately. But in this case, the report came much later, and the link to BufferedBitmap wasn’t immediately obvious. The user had already been using the app extensively, but hadn’t triggered the specific functionality that uses the bitmap, which initially made the cause unclear.

    It would be interesting to dig into what exactly is going wrong in the simulator.

  • To be fair, I don’t think a Bitmap necessarily needs more than that. I’m not sure how Garmin handles it, but in my own BufferedBitmapDrawable, width and height are enough to calculate coordinates - Dc.drawBitmap does the rest.

    Ok, but from the pov of the dev who wrote the *old* Bitmap.initialize / Bitmap.setBitmap (on a real device), why would it accept a BufferedBitmap when it knows those methods aren't available?

    Again, BufferedBitmap isn't a subclass of Bitmap. And a Bitmap itself can't be used to initialize another Bitmap anyway, even on newer devices (you need a BitmapReference to handle that use case).

    It would be interesting to dig into what exactly is going wrong in the simulator.

    Sorry if I didn't make it clear, but I think it's super obvious that the simulator simply doesn't simulate the *old*, limited functionality of Bitmap.initialize / setBitmap which:

    - would not work with a BufferedBitmap at all

    - explicitly had a check to make sure the input to setBitmap is a Number / Symbol or BitmapResource (this again goes back to the exception you are seeing on a real device)

    - might even ignore the :bitmap option altogether on a really old device like FR935

    It seems that the simulator incorrectly simulates the new functionality (or at least the subset of it which supports BufferedBitmap).

    To summarize: the docs are written as if all the Bitmap.initialize() functionality exists regardless of device / CIQ version, and apparently the simulator is written that way too (or at least as far as the BufferedBitmap stuff goes).

    To be clear, I think this is what the implementation looks like:

    In this example, we can see that the new implementation works properly with BufferedBitmap provided that it's really being called on a new device (simulated or real).

    The problem arises when the new implementation is simulated on an old device, which doesn't have the newer version of BufferedBitmap.

    If you try to work around this problem by just subclassing BufferedBitmap, it will work in the sim because it has the newer implementation. It won't work on the device, because the device doesn't have the newer implementation.

    To be clear I don't think the runtime type exceptions are "magic", I think there's code in the API functions which uses instanceof (like I guessed in the example above).

    Because of the runtime type guards, it isn't really possible in principle to expect to be able to pass in a completely unrelated class to the old Bitmap.initialize / setBitmap and expect it to work. (I mean "unrelated" as in "unrelated to the classes which are supported")

    If not for the runtime type guards, then it might be possible in principle for your workaround to succeed on fenix6pro (given that the :bitmap option is supported, and passing in a BitmapResource works). (But there have to some kind of runtime type guards on setBitmap, since clearly a resource ID has to be treated differently than a BitmapResource, for example. So what would really have to happen is that the code for handling BitmapResource would have to be the "default case" if no other type (such as Number / Symbol / ResourceID)  is matched; that way BufferedBitmap could also be handled the same way as BitmapResource. But ofc this would prevent the API from detecting if some other class was incorrectly passed in, like a FontResource or something.)

    But what about even older devices (e.g. possibly fr935), which only accept a resource ID for Bitmap.initialize / Bitmap.setBitmap. In this case, both of those functions will ultimately load a resource, so it would be probably expecting too much to think that you could trick those functions into accepting a BitmapResource or BufferedBitmap.

  • Otoh, if BitmapResource and BufferedBitmap inherited the same ancestor class (they don't), and Bitmap.initialize() was written to accept that ancestor class), then I would expect the 2 types to be interchangeable for Bitmap.initialize().

    But ofc that doesn't reflect reality. If it did, we wouldn't have this problem in the first place.

    This kind of ties back to what flocsy said about interfaces, except I don't think that really applies since I don't think interfaces can be used to perform runtime type checks, as I believe interface is a compile-time-only Monkey Types feature.

    But if interfaces could be used to do type checking at runtime, and the API used interfaces in such a way, then the problem wouldn't exist. Ofc that doesn't reflect reality.

    Furthermore, we could also imagine a kind of limited interface-like runtime type check which just uses has checks to see if certain methods and members exist (without also checking the class type via instanceof). Ofc this would be dangerous since has checks alone can't validate member types. Iow if the implementation just checked whether getWidth and getHeight exist, it wouldn't be able to check that they return the right types. I guess you could also write has checks for the returned values too, but at some point it seems like this is a lot of code to replace a simple instanceof check. And ofc this doesn't reflect reality either.

    But yeah, in a hypothetical world where Garmin decided to change the entire API so argument / option types are specified as interfaces and not classes, and also decided to change the implementation so type checks are done via a ton of has checks to replace each instanceof check, the flexibility we're looking for would be possible. (i.e. just pass in anything which has getWidth / getHeight to Bitmap.initialize and expect it to work properly, assuming that the same type can also be passed to dc.drawBitmap.) But even if Garmin did all of that (they wouldn't), they wouldn't be able to change all the old devices to work that way.

    I don't think Garmin would want interfaces to be used in this manner anyway, because that would open the door to the dev passing in unrelated classes which conform to the interface but don't actually have the requisite functionality.

    e.g. let's say Garmin wrote an interface that matches both BitmapResource and BufferedBitmap, and they used runtime type checking (has checks) to validate that an object passed into Bitmap.setBitmap conforms to the interface.

    What's to stop the dev from writing some unrelated class which conforms to the interface but can't be drawn by dc.drawBitmap()?

    Now dc.drawBitmap() also has to use an interface for its bitmap option. But a dev could still write a class that superficially conforms to the interface yet doesn't have ability to be drawn by dc.drawBitmap. I guess if the interface was documented properly, it would be the devs fault if this happened.

    But I think you see my point: the hypothetical use of interfaces instead of classes for API types would lead to a lot of additional complexity for both the devs and Garmin.

  • TL;DR the reason it doesn't work on a real (fenix6pro) device is that the real device has a runtime type check to make sure that nothing other than Number or BitmapResource is passed into Bitmap.setBitmap. It's literally in the topic title.

    The reason it works on a simulated fenix6pro (after the workaround of adding getHeight / getWidth) is because the simulator apparently uses the newer implementation - intended for newer devices - even on old devices such as fenix6pro. In this case, BufferedBitmap is probably explicitly allowed (via instanceof check) and handled within the code, except the code isn't expecting an old BufferedBitmap (without getHeight / getWidth), since it's a new implementation meant for newer devices.

  • In other words it's not realistic (on a real device) for Bitmap.initialize / setBitmap to support BufferedBitmap but to also receive a BufferedBitmap which doesn't have the requisite getHeight / getWidth methods.

    On a real device either:

    - (old devices) BufferedBitmap is not supported [*] and if you try to pass it in, you get the very type exception mentioned in the topic title. [*] according to old API docs, only resourceId is supported for the oldest devices. For slightly newer devices, BitmapResource is also supported

    - (new devices) BufferedBitmap is explicitly supported and if you try to pass it in, it works

    The key here is that the API implementation performs run time type checks (which is why the API throws InvalidTypeExceptions.)

  • You're absolutely right. I only meant that knowing the width and height would be sufficient to draw a BufferedBitmap. However, with CIQ < 4.0.0, any code attempting to access that information simply won’t compile—no matter how it's written. That’s something I hadn’t fully considered at the time, and I take responsibility for that.

    As for the simulator, yes - it seems clear it's running a newer version of the code. But how is that actually implemented? What's the relationship between the simulator's implementation and that of the real device? Are they maintained separately by different teams trying to replicate behavior, or is the simulator directly running the same codebase that's deployed to the devices?

  • I only meant that knowing the width and height would be sufficient to draw a BufferedBitmap

    Yeah, you're right too. That's probably exactly why getWidth and getHeight were added in CIQ 4 - so BufferedBitmap could work with Bitmap.initialize(). All of this circumstantial evidence does make it understandable that you might think it could work if you added those functions yourself.

    Then again, you gotta wonder why the existing BufferedBitmap.getDc().getHeight() and getWidth() functions weren't sufficient. Unless I'm missing something, it seems that BufferedBitmap.getHeight / getWidth would return the same values as the other functions.

    And ofc, even before CIQ 4, it was possible to literally draw a BufferedBitmap by passing it into dc.drawBitmap().

    Anyway, nothing above is meant to be a lecture or criticism. I'm just trying to work out why I think this behaviour "makes sense" (even though we don't like it, and it clearly points to multiple failures in the doc and the sim.)

    As for the simulator, yes - it seems clear it's running a newer version of the code. But how is that actually implemented? What's the relationship between the simulator's implementation and that of the real device? Are they maintained separately by different teams trying to replicate behavior, or is the simulator directly running the same codebase that's deployed to the devices?

    Yeah based on the fact that we often see different behaviour in the sim vs. the real device, I'd say that the sim doesn't run all the real code. Maybe some of the Monkey C API code is the same, but the native code is probably different. As others have said, the sim is not an emulator.

    We also see bugs that exist in the sim and not a real device, and vice versa. iirc, on super old devices you could crash/reboot the watch with an array out of bounds access, but in the simulator it was just a normal uncatchable error.

    There's also many things that are not simulated at all, like most of the built-in watch UI.

    Otoh, the structure of the device files and folders in the simulator is similar (not identical) to that of a real watch.

  • However, with CIQ < 4.0.0, any code attempting to access that information simply won’t compile—no matter how it's written. That’s something I hadn’t fully considered at the time, and I take responsibility for that.

    I think if the sim did its job and simulated the behaviour of old devices properly, none of this would've happened.