syntax = "proto3"; import "nanopb.proto"; package PB; /* * Message FROM the SmartKnob to the host */ message FromSmartKnob { uint32 protocol_version = 1 [(nanopb).int_size = IS_8]; oneof payload { Ack ack = 2; Log log = 3; SmartKnobState smartknob_state = 4; } } /* * Message TO the Smartknob from the host */ message ToSmartknob { uint32 protocol_version = 1 [(nanopb).int_size = IS_8]; uint32 nonce = 2; oneof payload { RequestState request_state = 3; SmartKnobConfig smartknob_config = 4; } } /** Lets the host know that a ToSmartknob message was received and should not be retried. */ message Ack { uint32 nonce = 1; } message Log { string msg = 1 [(nanopb).max_length = 255]; } message SmartKnobState { /** Current integer position of the knob. (Detent resolution is at integer positions) */ int32 current_position = 1; /** * Current fractional position. Typically will only range from (-snap_point, snap_point) * since further rotation will result in the integer position changing, but may exceed * those values if snap_point_bias is non-zero, or if the knob is at a bound. When the * knob is at a bound, this value can grow endlessly as the knob is rotated further past * the bound. * * When visualizing sub_position_unit, you will likely want to apply a rubber-band easing * function past the bounds; a sublinear relationship will help suggest that a bound has * been reached. */ float sub_position_unit = 2; /** * Current SmartKnobConfig in effect at the time of this State snapshot. * * Beware that this config contains position and sub_position_unit values, not to be * confused with the top level current_position and sub_position_unit values in this State * message. The position values in the embedded config message will almost never be useful * to you; you probably want to be reading the top level values from the State message. */ SmartKnobConfig config = 3; /** * Value that changes each time the knob is pressed. Does not change when a press is released. * * Why this press state a "nonce" rather than a simple boolean representing the current * "pressed" state? It makes the protocol more robust to dropped/lost State messages; if * the knob was pressed/released quickly and State messages happened to be dropped during * that time, the press would be completely lost. Using a nonce allows the host to recognize * that a press has taken place at some point even if the State was lost during the press * itself. Is this overkill? Probably, let's revisit in future protocol versions. */ uint32 press_nonce = 4 [(nanopb).int_size = IS_8]; } message SmartKnobConfig { /** * Set the integer position. * * Note: in order to make SmartKnobConfig apply idempotently, the current position * will only be set to this value when it changes compared to a previous config (and * NOT compared to the current state!). So by default, if you send a config position * of 5 and the current position is 3, the position may remain at 3 if the config * change to 5 was previously handled. If you need to force a position update, see * position_nonce. */ int32 position = 1; /** * Set the fractional position. Typical range: (-snap_point, snap_point). * * Actual range is technically unbounded, but in practice this value will be compared * against snap_point on the next control loop, so any value beyond the snap_point will * generally result in an integer position change (unless position is already at a * limit). * * Note: idempotency implications noted in the documentation for `position` apply here * as well */ float sub_position_unit = 2; /** * Position is normally only applied when it changes, but sometimes it's desirable * to reset the position to the same value, so a nonce change can be used to force * the position values to be applied as well. * * NOTE: Must be < 256 */ uint32 position_nonce = 3 [(nanopb).int_size = IS_8]; /** Minimum position allowed. */ int32 min_position = 4; /** * Maximum position allowed. * * If this is the same as min_position, there will only be one allowed position. * * If this is less than min_position, bounds will be disabled. */ int32 max_position = 5; /** The angular "width" of each position/detent, in radians. */ float position_width_radians = 6; /** * Strength of detents to apply. Typical range: [0, 1]. * * A value of 0 disables detents. * * Values greater than 1 are not recommended and may lead to unstable behavior. */ float detent_strength_unit = 7; /** * Strength of endstop torque to apply at min/max bounds. Typical range: [0, 1]. * * A value of 0 disables endstop torque, but does not make position unbounded, meaning * the knob will not try to return to the valid region. For unbounded rotation, use * min_position and max_position. * * Values greater than 1 are not recommended and may lead to unstable behavior. */ float endstop_strength_unit = 8; /** * Fractional (sub-position) threshold where the position will increment/decrement. * Typical range: (0.5, 1.5). * * This defines how hysteresis is applied to positions, which is why values > */ float snap_point = 9; /** * Arbitrary 50-byte string representing this "config". This can be used to identify major * config/mode changes. The value will be echoed back to the host via a future State's * embedded config field so the host can use this value to determine the mode that was * in effect at the time of the State snapshot instead of having to infer it from the * other config fields. */ string text = 10 [(nanopb).max_length = 50]; /** * For a "magnetic" detent mode - where not all positions should have detents - this * specifies which positions (up to 5) have detents enabled. The knob will feel like it * is "magnetically" attracted to those positions, and will rotate smoothy past all * other positions. * * If you want to have more than 5 magnetic detent positions, you will need to dynamically * update this list as the knob is rotated. A recommended approach is to always send the * _nearest_ 5 detent positions, and send a new Config message whenever the list of * positions nearest the current position (as reported via State messages) changes. * * This approach enables effectively unbounded detent positions while keeping Config * bounded in size, and is resilient against tightly-packed detents with fast rotation * since multiple detent positions can be sent in advance; a full round-trip Config-State * isn't needed between each detent in order to keep up. */ repeated int32 detent_positions = 11 [(nanopb).max_count = 5]; /** * Advanced feature for shifting the defined snap_point away from the center (position 0) * for implementing asymmetric detents. Typical value: 0 (symmetric detent force). * * This can be used to create detents that will hold the position when carefully released, * but can be easily disturbed to return "home" towards position 0. */ float snap_point_bias = 12; /** * Hue (0-255) for all 8 ring LEDs, if supported. Note: this will likely be replaced * with more configurability in a future protocol version. */ int32 led_hue = 13 [(nanopb).int_size = IS_16]; } message RequestState {} message PersistentConfiguration { uint32 version = 1; MotorCalibration motor = 2; StrainCalibration strain = 3; } message MotorCalibration { bool calibrated = 1; float zero_electrical_offset = 2; bool direction_cw = 3; uint32 pole_pairs = 4; } message StrainCalibration { int32 idle_value = 1; int32 press_delta = 2; }