Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(behaviors): Add behavior metadata information. #2231

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

petejohanson
Copy link
Contributor

  • For upcoming ZMK studio work, make a set of rich metadata available to provide a friendly name for a behavior, and allow super flexible descriptions of the parameters the behaviors take.

@petejohanson petejohanson added enhancement New feature or request behaviors studio ZMK Studio (runtime keymaps) labels Mar 27, 2024
@petejohanson petejohanson self-assigned this Mar 27, 2024
@petejohanson petejohanson requested a review from a team as a code owner March 27, 2024 22:23
@@ -9,6 +9,7 @@
/omit-if-no-ref/ to: to_layer {
compatible = "zmk,behavior-to-layer";
#binding-cells = <1>;
friendly-name = "Go To Layer";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't call this "Go To Layer" anywhere in the docs, just "To Layer". I think it'd be good to be consistent, do you prefer the former?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I was reading it.. I realized it was a pretty odd name. I'll update to match our current language.

@@ -9,6 +9,7 @@
/omit-if-no-ref/ tog: toggle_layer {
compatible = "zmk,behavior-toggle-layer";
#binding-cells = <1>;
friendly-name = "Layer Toggle";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also typically called "Toggle Layer." Having a proper noun phrase rather than verb+action might be better in general, but I'd again tend to consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll defer to consistency for now, but I do think this reads better than "Toggle Layer", IMHO.

.position = 0,
.value = BT_NXT_CMD,
.friendly_name = "Next Profile",
.type = BEHAVIOR_PARAMETER_VALUE_METADATA_TYPE_VALUE,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think it would be easier to read these if the type came before the value/range/etc. Maybe this would be a good order?

  1. Position
  2. Name
  3. Type
  4. Value/range/etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Definitely reads better, thanks!

#define ZMK_BEHAVIOR_REF_INITIALIZER(_dev) \
{ .device = _dev, }

#define ZMK_BEHAVIOR_REF_DEFINE(name, ...) \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the compiler complain about unused macro parameters? If not, you could replace the variable args with an explicit parameter list and then just pass in everything but ignore the unneeded parameters whenever metadata is disabled. As this is currently written, it's quite difficult to understand what parameters this takes in either case.

Also, an idea for reducing the number of macros that need alternate implementations:

struct zmk_behavior_metadata {
    // Making this still defined but empty when metadata is disabled so we can write
    // ".metadata = ..." and have it still compile when metadata is disabled.
#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
    const char *name;
    // any additional fields needed later go here...
#endif
};

#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
#define ZMK_BEHAVIOR_METADATA_INITIALIZER(name) \
    { .name = name }
#else
#define ZMK_BEHAVIOR_METADATA_INITIALIZER(name) {}
#endif    

struct zmk_behavior_ref {
    const struct device *device;
    struct zmk_behavior_metadata metadata;
};

#define ZMK_BEHAVIOR_REF_INITIALIZER(dev, name) \
    { .device = dev, .metadata = ZMK_BEHAVIOR_METADATA_INITIALIZER(name) }

(Test for that idea: https://godbolt.org/z/GzaeT9dYh)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I dig it. Took a slightly different approach though and just passed the node_id into the macro, in case more gets added later, the ZMK_BEHAVIOR_METADATA_INITIALIZER can handle that "internally".

Thoughts on the updated take?

@petejohanson petejohanson force-pushed the core/add-behavior-metadata branch 2 times, most recently from e968c3a to 0119f0b Compare March 29, 2024 21:34
@petejohanson petejohanson mentioned this pull request Apr 5, 2024
3 tasks
@petejohanson petejohanson force-pushed the core/add-behavior-metadata branch 2 times, most recently from fda849e to a9d724f Compare April 10, 2024 20:54
uint16_t max;
} range;

enum behavior_parameter_standard_domain standard;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With "standard" being an adjective here, would naming this domain make more sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, since this is the union field that matches the type field, I'd like the naming to match, which explains my choice here. Thoughts?

BEHAVIOR_PARAMETER_STANDARD_DOMAIN_NULL = 0,
BEHAVIOR_PARAMETER_STANDARD_DOMAIN_HID_USAGE = 1,
BEHAVIOR_PARAMETER_STANDARD_DOMAIN_LAYER_INDEX = 2,
BEHAVIOR_PARAMETER_STANDARD_DOMAIN_HSV = 3,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
BEHAVIOR_PARAMETER_STANDARD_DOMAIN_HSV = 3,
BEHAVIOR_PARAMETER_STANDARD_DOMAIN_COLOR_HSV = 3,

might make it clearer what HSV means. (If it isn't supposed to be a color, then this really needs to be clarified.)

Also, RGB seems like a generally more common format for sending color data. I guess we could add a *_COLOR_RGB value later, but then we'd have to deal with two different color formats everywhere.

One alternative would be to change the underglow code to store the value as RGB, then convert to HSV when loading. If the devicetree format needs to match, we could also change RGB_COLOR_HSB_VAL() to convert from HSV to RGB.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had originally used "HSV" as the "native" format to make it easier to do increase/decrease brightness, IIRC. I don't have a strong opinion here, tbh. Maybe @Nicell has soe thoughts though.

Copy link
Collaborator

@joelspadin joelspadin Jun 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main concern is that you can only represent a small fraction of the possible RGB colors using integer HSV values. Assuming I correctly wrote the Python script I used to test this, rgb_underglow.c's hsb_to_rgb() function can represent 2,189,687 out of 16,777,216 possible colors (about 13%).

struct {
uint16_t min;
uint16_t max;
} range;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we are assuming that

  1. Nothing with a range of values will ever go above 65535.
  2. Values cannot be negative.

I'm not sure what types of values would use this, so I'm not sure if the first one is a safe assumption.

Given that you can write a negative number as a parameter in devicetree, then read it by casting to int32_t in driver code, is limiting values to non-negative safe? Should we maybe have separate types for signed and unsigned ranges?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.... some good questions. I suppose for this, I was having a hard time imagining a range needing to be larger than this, or negative, but since we're trying to have this not have to change any time soon.... worth being a bit more flexible here.

Do we think { int32_t min; uint32_t max; } range would work? Do we really think we need a range that's only negative numbers? or with a minimum above 2,147,483,647 ever?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixing signed and unsigned math in C is generally a great way to have a bad time. I'd be in favor of just using signed values for both min and max. I can't think of any real-world range of values that wouldn't fit into a signed 32-bit integer.

The first thing I could think of that might need large numbers is millisecond durations. With 16-bit, that could be a problem since you'd be limited to ~32 s signed or ~65 s unsigned. With signed 32-bit, you get ~25 days, which I can't imagine being insufficient for anything (if that's the kind of time scale your behavior is working on, you should probably be using seconds instead of milliseconds).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, totally fair. I'll update for signed 32-bit values. It'll increase the size a small bit, but not enough I think to warrant limiting things.

struct {
enum behavior_parameter_standard_domain param1;
enum behavior_parameter_standard_domain param2;
} standard;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding this correctly, this is effectively just a shortcut for

{
  .type = BEHAVIOR_PARAMETER_METADATA_CUSTOM,
  .custom = {
    .sets_len = 1,
    .sets = {{
      .values_len = 2,
      .values = {{
        .position = 0,
        .type = BEHAVIOR_PARAMETER_VALUE_METADATA_TYPE_STANDARD,
        .standard = <param1>,
      }, {
        .position = 1,
        .type = BEHAVIOR_PARAMETER_VALUE_METADATA_TYPE_STANDARD,
        .standard = <param2>,
      }},
    }},
  },
}

right? If so, that simplifies defining the data, but it adds a special case to the implementation that has to read this data. You also still have to use the more complex form for something like a MIDI note behavior that uses param1 = range[0, 127] (note) and param2 = range[0, 127] (velocity), since there's no way to represent a range type here.

Is there any way we could avoid the redundant ways to represent the same data? Maybe have some predefined constants and/or macros to let you build a metadata set that only has one item without all the boilerplate? Alternatively, could we support all possible value types in this inline form of writing the parameter data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... this is a very valid question..... After adding the necessary complexity for custom params.... do we really need both ways of doing this? I think I'm almost convinced we don't. Let me sleep on this, but if I can't come up with a compelling reason, I will refactor to "one single way of doing this" with some naming tweaks to make this saner.

BEHAVIOR_PARAMETER_VALUE_METADATA_TYPE_VALUE = 0,
BEHAVIOR_PARAMETER_VALUE_METADATA_TYPE_RANGE = 1,
BEHAVIOR_PARAMETER_VALUE_METADATA_TYPE_STANDARD = 2,
} type;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be a bit of overlap between what this and enum behavior_parameter_standard_domain conceptually represent. If we can get rid of the special case in behavior_parameter_metadata as suggested below, maybe "domain" and "type" could be merged together?

* For upcoming ZMK studio work, make a set of rich metadata available
  to provide a friendly name for a behavior, and allow super flexible
  descriptions of the parameters the behaviors take.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
behaviors enhancement New feature or request studio ZMK Studio (runtime keymaps)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants