JMT Prop Addons is a collection of optional ProffieOS extensions. The included wrapper layers
new behavior on top of the saber_fett263_buttons prop without modifying that prop or any
core ProffieOS source. The original files stay untouched. The goal is to add features to an already
excellent prop while preserving everything that makes it work.
Each feature is opt-in via a #define. Charge detection, chassis detection,
gesture-based preset switching while the chassis is removed, blade ID-based blade detect (OS8+), and a favorites
system can be enabled independently or combined as needed. Nothing activates unless you explicitly
enable it in a config.
Easiest path: use JMT Studio's Add JMT Features button on any installed ProffieOS version. The wrapper, helper headers, and config entries are placed for you and stay in sync with future updates.
Add this include inside your config's CONFIG_PROP block. The wrapper internally defines
saber_fett263_buttons as its base, so this line replaces your existing prop
include. Only one prop can be active in a build, so leaving another #include "props/..." in
place will cause a compile error.
#ifdef CONFIG_PROP
#include "../props/jmt_fett263_wrapper.h"
#endif
From there, opt into any feature below by adding its #define to your config. All defines and tunables
are listed inside each feature.
If any of your blade styles use ChargeFullPropF or PixelRelay (see
Style Add-ons), add the matching include inside your config's
CONFIG_PRESETS block. These must be processed after the prop include so the shared
symbols are available. Add only the includes for the add-ons your styles actually use.
#ifdef CONFIG_PRESETS
#include "../functions/charge_full_prop.h"
#include "../styles/pixel_relay.h"
#endif
When you add a blade style that references ChargeFullPropF or PixelRelay through
JMT Studio's Style Library, Studio writes the style entry inside an #ifdef guard keyed to the
matching add-on header. A missing include silently skips the wrapped entries instead of breaking your build.
The includes above are what activate them. This also means a Style Library shared across users on older
ProffieOS versions or older add-on releases will not error out; entries that depend on a not-yet-installed
add-on simply do not compile in until the matching include is present.
JMT Studio's Add JMT Features button places these files for you in the right directories. It does
not edit your config, so the #include lines above still need to be added by hand. If you are
installing manually from GitHub, make sure each file lives in the directory shown.
props/jmt_fett263_wrapper.h The wrapper itself. This is the file your config includes. common/charge_state.h Holds the shared g_charging and g_charge_full state declarations the wrapper relies on. functions/charge_full_prop.h Optional. Only needed if a blade style references ChargeFullPropF to react to charge-complete. styles/pixel_relay.h Optional. Only needed if a blade style uses PixelRelay or PixelRelayL. Adds chassis-presence detection to the prop. State changes play chassisin.wav or chassisout.wav and cancel any pending favorite-reset hold action (if enabled). While the chassis is out and the saber is off, swing-to-ignite is suppressed so the saber will not light up just from being handled. Twist on/off remains available if enabled, or may be naturally suppressed by blade-detect behavior depending on the install.
Two mutually exclusive ways to detect the chassis. Both produce identical downstream behavior: same sounds, same gesture gating, same wake handling.
1. Dedicated detect pin. The original method. Uses a free GPIO pin on the board, wired to a contact that closes when the chassis is inserted.
INPUT_PULLUP. Chassis detected when the pin is shorted to GND.#define CHASSIS_DETECT_PIN_HIGH. Requires an external pulldown so the pin sits LOW when no chassis is present.2. BladeID resistance (no detect pin). Reuses the existing BladeID line and saves a physical wire. Detection comes from the BladeID resistance reading instead of a dedicated pin.
#define JMT_CHASSIS_DETECT_RANGE <min>,<max>
When the raw BladeID reading falls inside the configured range, the wrapper treats the chassis as OUT. Requires ENABLE_POWER_FOR_ID, BLADE_ID_SCAN_MILLIS, and BLADE_ID_TIMES. Latency is one BladeID scan cycle (scan-driven, not edge-triggered).
The range must not overlap any BladeConfig ohm entry. How it relates to NO_BLADE_ID_RANGE depends on how blade presence is detected on this build:
JMT_BLADE_DETECT): NO_BLADE_ID_RANGE is the standard OS macro that classifies "no blade present" readings. The chassis-detect range typically sits as a subset inside it, carving out the chassis-OUT portion. The wrapper handles the OS's no-blade remapping internally.BLADE_DETECT_PIN): blade presence comes from the pin, so NO_BLADE_ID_RANGE is not required for this feature. The chassis-detect range just needs to sit above the highest in-chassis BladeID reading you will see.Chassis IN has two valid BladeID readings (the blade's own resistance with a blade in, or the chassis-only resistance with no blade in), which sit far apart. Detecting OUT is simpler because open-circuit gives one expected reading zone. Anything outside the configured range is treated as chassis IN by absence, which is also the safer fallback if a reading drifts unexpectedly.
Measure your specific saber's chassis-out (open-circuit), chassis-in-no-blade (chassis resistance only), and the BladeID reading of each blade you plan to use. Do not guess. Open-circuit is the most variable reading; pad generously on the high end.
JMT_CHASSIS_DETECT_RANGE defines the band the wrapper treats as chassis OUT (literal min, max):
If you are also configuring NO_BLADE_ID_RANGE (the typical BladeID-based blade-detect build):
JMT_CHASSIS_DETECT_RANGE. A reading higher than this max falls outside the no-blade band, the OS treats it as a mystery blade, and the chassis state can flip erratically.NO_BLADE_ID_RANGE and JMT_CHASSIS_DETECT_RANGE)#define NO_BLADE_ID_RANGE 20000, 400000
#define JMT_CHASSIS_DETECT_RANGE 40000, 400000
Your specific values will depend on your hardware. JMT_CHASSIS_DETECT_RANGE marks the chassis-OUT band, not chassis-IN: anything below it counts as chassis present. In this build chassis-in-no-blade is around 22848, so NO_BLADE_ID_RANGE starts at 20000 to include it, and JMT_CHASSIS_DETECT_RANGE starts at 40000 to leave clean buffer above that before chassis-OUT begins. The shared upper bound (400000) absorbs open-circuit drift.
JMT_CHASSIS_WAKE: wake motion detection on chassis state change. Helpful if the board is in a deep idle state.Place chassisin.wav and chassisout.wav in your saber's common/ folder on the SD card. You can use any compatible WAVs, or preview and grab the defaults below.
Pin-based detection includes a fixed 30ms software debounce. BladeID-based detection is scan-driven with latency equal to BLADE_ID_SCAN_MILLIS. No tuning is normally required for the pin method; the BladeID method needs the range tuning above.
Required for all charge-related features below. When the charger is detected the prop plays chargebegin.wav, cancels any pending favorite reset, and prevents normal MOTION_TIMEOUT / IDLE_OFF_TIME behavior (if enabled) while charging so charge styles remain active and visible.
A silent boot-time sync prevents chargebegin.wav from playing if charge detect is already active at startup.
Two global flags are tracked and declared extern in common/charge_state.h:
g_charging: true while the charger is detected. Available to prop and config code.g_charge_full: true once the battery has been at full for the configured dwell time. Blade styles can react to this via the bundled ChargeFullPropF helper (see Style Helpers).Place chargebegin.wav in your saber's common/ folder on the SD card. The wrapper does not install this for you. You can use any compatible WAV, or preview and grab the default below.
Active LOW input using INPUT_PULLUP with fixed 30ms debounce.
Monitors battery level and determines when charging is considered complete. Uses two thresholds and a dwell timer to avoid rapid full/not-full toggling near 100%. Drives the g_charge_full global, which blade styles can consume via the included ChargeFullPropF style helper (see Style Helpers).
| Macro | Default | Meaning |
|---|---|---|
CHARGE_FULL_ENTER | 32700 | Level required before full-charge dwell timing begins |
CHARGE_FULL_EXIT | 32000 | Drop-out threshold. Must be below ENTER. |
CHARGE_FULL_DWELL_MS | 30000 | How long ENTER must be held before declaring full |
When charging is no longer detected, the full state and dwell timer are cleared immediately.
Plays chargecomplete.wav once when g_charge_full first becomes true (see Charge Full Tracking for the timing). Resets when charging is no longer detected, allowing the next charge cycle to announce again.
Place chargecomplete.wav in your saber's common/ folder on the SD card. You can use any compatible WAV, or preview and grab the default below.
Requires: CHARGE_DETECT_PIN.
While charging is active, this feature suppresses nearly all button events. The single exception is BUTTON_POWER, which announces battery level instead of igniting the saber (rate limited to once every 300ms).
If BLADE_DETECT_PIN is defined, blade-detect events are also ignored during charging so blade-detect events cannot trigger behaviors while charging.
Requires: CHARGE_DETECT_PIN.
Recommendation: also enable FETT263_SAVE_GESTURE_OFF so prior gesture state is fully preserved across a charge cycle. The build will succeed without it, but a warning is emitted.
Treats the last preset in your preset array as a dedicated charge preset. When charging is detected, this feature switches to that preset and triggers EFFECT_BOOT. When no longer charging, the board reboots.
Normal preset switching does not re-run ProffieOS startup behavior such as font scanning, motion initialization, and boot effects. Rebooting guarantees a clean post-charge state and avoids subtle inconsistencies in motion and audio.
Charge-preset entry and unplug-reboot both play a boot sound. ProffieOS uses the font's boot.wav if present, falling back to font.wav otherwise. To give every font a dedicated boot sound, place a generic boot.wav in your saber's common/ folder. Fonts without their own will use it instead of falling back to font.wav.
If a user manually navigates and lands on the charge preset while not charging, this feature detects scroll direction and skips past it. The charge preset cannot be selected as a normal preset.
Requires: CHARGE_DETECT_PIN.
Designed primarily for removable chassis setups handled outside the hilt. While the chassis is out and the saber is not ignited, rolling it forward or backward through a large rotation changes the preset.
With the chassis held roughly horizontal (within ±7° of level), the feature captures a baseline roll and watches for large rotations. A positive roll delta beyond 170° advances to the next preset; a negative roll delta beyond 170° selects the previous preset. The baseline resets after each trigger so successive flips continue scrolling through presets.
The threshold is intentionally set slightly below 180° so a full flip gesture remains reliable without requiring an exact angle boundary.
Active only when: saber not ignited and chassis removed.
Requires: CHASSIS_DETECT_PIN.
Mutually exclusive with: JMT_FLICK_PRESETS.
Designed primarily for removable chassis setups where presets must be changed without button access, but full roll gestures feel awkward or unnatural due to hilt shape, board orientation, or install geometry.
Unlike traditional roll gestures, this feature establishes a known neutral arm pose first. Optional pitch/roll offsets can calibrate that pose for curved hilts, angled chassis installs, or rotated board mounting, so preset navigation feels natural without changing the rest of ProffieOS motion behavior.
From the arm pose, twist right and return to center to advance presets. Twist left and return to center to move backward. The preset change is not committed until the chassis returns to the arm pose, which reduces accidental activations during handling.
Arm pose: pitch near 0°, roll near ±180°. Tolerance ±18° pitch, ±25° roll.
Twist pose: tolerance ±22° pitch, ±22° roll. Eligibility window opens 700ms after entering arm pose.
Commit: return to arm pose within 500ms after the twist pose.
Direction: roll near -90° then return selects the next preset. Roll near +90° then return selects the previous preset.
JMT_PITCH_OFFSET and JMT_ROLL_OFFSET calibrate the flick gesture matcher after ORIENTATION_ROTATION has already been applied. They do not change the IMU orientation globally and do not affect other ProffieOS motion behavior.
Use these offsets when a rotated board or angled chassis makes your natural arm pose read somewhere other than pitch 0°, roll ±180°. The offsets shift only the flick preset target poses so your physical neutral position matches the gesture matcher.
| Macro | Notes |
|---|---|
JMT_PITCH_OFFSET |
Degrees added to measured pitch for flick-preset matching. Requires ORIENTATION_ROTATION. |
JMT_ROLL_OFFSET |
Degrees added to measured roll for flick-preset matching. Requires ORIENTATION_ROTATION. |
Think of these as calibration offsets for this gesture only, not a replacement for ORIENTATION_ROTATION.
Active only when: saber not ignited and chassis removed.
Requires: CHASSIS_DETECT_PIN.
Mutually exclusive with: JMT_ROLL_PRESETS.
Uses the Blade ID scan result instead of a hardware detect pin. The prop considers the blade removed when the detected Blade ID resolves to NO_BLADE.
On insertion:
On removal:
If FETT263_SAVE_GESTURE_OFF is defined, prior gesture state is saved on removal and restored on insertion. Otherwise gestures are force-enabled on insertion.
Blade-detect processing is skipped while charging when CHARGE_DETECT_PIN is defined and charging is active.
ENABLE_POWER_FOR_IDBLADE_ID_SCAN_MILLIS > 0BLADE_ID_TIMES > 0If your BladeConfig array marks "no blade present" with a custom ohm value below the standard NO_BLADE constant (1e9), and you are on ProffieOS 7.x, add this define so the wrapper plays bladein.wav and bladeout.wav on blade swaps instead of font.wav:
#define JMT_NO_BLADE_VALUE <your no-blade ohm value>
On ProffieOS 8.0 or later, use the native NO_BLADE_ID_RANGE instead. If your BladeConfig already uses NO_BLADE as its no-blade ohm value (the default), this define is not needed on any version.
Mutually exclusive with: BLADE_DETECT_PIN.
If BLADE_DETECT_PIN is already present in your config, this feature adds two small behavior improvements:
Nothing additional to configure. No new macros required.
Mutually exclusive with: JMT_BLADE_DETECT.
A tilt-driven favorites system that lets a user mark presets and cycle through just those, without scrolling the full preset list. Triggered by AUX + EVENT_FIRST_HELD_LONG while the saber is off. The action depends on the saber's orientation when the trigger fires.
| Orientation | Action |
|---|---|
| Pointing straight down (within ±5° of -90°) | Arm favorites reset confirmation |
| Tilted up (beyond +5°) | Jump to next favorite |
| Tilted down (beyond -5°, not the reset zone) | Jump to previous favorite |
| Roughly level (within ±5°) | Toggle current preset as favorite |
mconfirm.wav and mdefault.wav.mdefault.wav.mcancel.wav.BUTTON_POWER press confirms (clears favorites, writes empty file).BUTTON_AUX press cancels.Stored as favorites.ini on the SD card, with favorites.bak written as a best-effort backup after every successful save. The loader falls back to the backup if the primary fails and rewrites the primary if needed. Malformed lines are dropped and the file is rewritten cleanly.
| Macro | Default | Meaning |
|---|---|---|
JMT_MAX_PRESET_FAVORITES | 10 | Maximum favorite slots |
JMT_DISABLE_FAVORITES | off | Manually disable favorites entirely |
Single-button builds (NUM_BUTTONS == 1) automatically disable favorites since the system relies on AUX.
Optional pieces that extend what a blade style can do. Each one ships in the add-ons repo and is enabled by an optional include in your config (see Setup). Once enabled, any blade style can use it directly.
0 when not full, 32768 when full
Mix<>, AlphaL<>, masks, transitions, selectors
DisplayBatteryChargeFullPropFBy the time ChargeFullPropF returns 32768, the wrapper has already validated four conditions:
Thresholds and timing live in the Charge Full Tracking feature. Style code does not need its own smoothing.
Mix<ChargeFullPropF, NOT_FULL_STYLE, FULL_STYLE>
When charge is not complete, the first branch shows. Once g_charge_full becomes true, the style instantly switches to the second branch.
Amber while charging, green when full. The simplest possible use.
StylePtr<
Mix<ChargeFullPropF,
Rgb<255, 50, 0>, // shown when charging (function = 0)
Rgb<0, 255, 50> // shown when full (function = 32768)
>
>()
Amber base with a green pulse layered on top when full. AlphaL<> uses ChargeFullPropF as the alpha source for the pulse layer: invisible while charging, fully visible when full.
StylePtr<Layers<
// Base: amber while charging
RotateColorsX<Variation, Rgb<255, 80, 0>>,
// When g_charge_full == true, fade in a green pulse overlay
AlphaL<
Pulsing<Rgb<0,255,0>, Black, 1200>,
ChargeFullPropF
>
>>()
A complete charging preset. Boot-wipe animation, then a live red-to-green battery meter while charging. Switches to a calm pulsing green when charge is confirmed complete.
StylePtr<
Mix<
ChargeFullPropF,
// NOT FULL: battery bar with boot wipe
Layers<
Mix<
PulsingF<Int<5000>>,
Mix<
SmoothStep<DisplayBattery, Int<-1>>,
Black,
Mix<DisplayBattery, Red, Green>
>,
Mix<
SmoothStep<DisplayBattery, Int<-1>>,
Black,
Mix<DisplayBattery, Red, Green>
>
>,
// BOOT: hold black during wipe setup
TransitionEffectL<
TrConcat<TrInstant, Black, TrDelay<5000>>,
EFFECT_BOOT
>,
// BOOT: wipe battery bar upward
TransitionEffectL<
TrConcat<
TrWipe<5000>,
Mix<
SmoothStep<DisplayBattery, Int<-1>>,
Black,
Mix<DisplayBattery, Red, Green>
>,
TrInstant
>,
EFFECT_BOOT
>
>,
// FULL: pulsing green
Pulsing<
Green,
Mix<Int<18000>, Green, Black>,
4000
>
>
>()
If you also enable JMT_CHARGE_STYLE_PRESET, the typical setup is: the last preset in your array is your charging visual, and that preset's style uses ChargeFullPropF to switch to a "fully charged" appearance. The saber jumps to that preset automatically when plugged in, then visually announces when it is done charging without any user interaction.
StylePtr<PixelRelay<...>>(). Off pixels render solid black.
Layers<base, PixelRelayL<...>>. Off pixels stay transparent.
PixelRelay flashes COLOR_A COUNT_A times, then COLOR_B COUNT_B times, then repeats. Both colors share a single flash heartbeat (FLASH_PERIOD_MS) so the off color is simply dark while it is not its turn. Designed for the Luke ROTJ cave-scene arrow accent (the green-then-red alarm strobes in the Death Star II), but works as a general-purpose alternating flasher on any 1- or 2-pixel accent.
PixelRelay: top-level form, pre-wrapped as Layers<Black, PixelRelayL<...>>. Drops directly into StylePtr<>. Off pixels render solid black.PixelRelayL: layer form, returns transparent for off pixels. Compose it inside Layers<some_base_color, PixelRelayL<...>> when you want a base color to show through.| Parameter | Default | Meaning |
|---|---|---|
COLOR_A | required | First color (any COLOR template) |
COUNT_A | required | Number of flashes for COLOR_A before handing off (must be > 0) |
COLOR_B | required | Second color |
COUNT_B | required | Number of flashes for COLOR_B before handing back (must be > 0) |
FLASH_PERIOD_MS | 333 | Full on+off duration of a single flash, in ms. Pass 0 to use the default. |
SAME_PIXEL | 0 | 0 for two-pixel mode, 1 for one-pixel mode |
START_INDEX | 0 | Starting pixel index for the relay. Must be 0 or positive. |
SAME_PIXEL = 0): COLOR_A flashes at START_INDEX, COLOR_B flashes at START_INDEX + 1. Useful for two-LED accents.START_INDEX set to skip earlier pixels on the same strip. Useful when the accent strip has other LEDs before the relay pair.SAME_PIXEL = 1): both colors flash on the same pixel at START_INDEX, alternating in time. Useful for single-LED accents or parallel-wired pairs.Green flashes 9 times, then red flashes 5 times, on the first two pixels of the accent strip. Both colors use RgbArg<BASE_COLOR_ARG, ...> so they are recolorable from the preset editor.
StylePtr<PixelRelay<
RgbArg<BASE_COLOR_ARG, Rgb<0,255,0>>, 9,
RgbArg<BASE_COLOR_ARG, Rgb<255,0,0>>, 5
>>()
Leaves pixel 0 free for a different accent.
StylePtr<PixelRelay<
RgbArg<BASE_COLOR_ARG, Rgb<0,255,0>>, 9,
RgbArg<BASE_COLOR_ARG, Rgb<255,0,0>>, 5,
0, // FLASH_PERIOD_MS = 0 uses the default 333 ms
0, // SAME_PIXEL = 0 (two-pixel mode)
1 // START_INDEX = 1 (relay lives on pixels 1 and 2)
>>()
Both colors share a single pixel, alternating in time.
StylePtr<PixelRelay<
RgbArg<BASE_COLOR_ARG, Rgb<0,255,0>>, 9,
RgbArg<BASE_COLOR_ARG, Rgb<255,0,0>>, 5,
0, // default flash period
1 // SAME_PIXEL = 1 enables one-pixel mode
>>()
These combinations will fail to compile. The error message points at the conflict directly.
JMT_ROLL_PRESETS and JMT_FLICK_PRESETS defined: Choose one gesture systemJMT_PITCH_OFFSET or JMT_ROLL_OFFSET without JMT_FLICK_PRESETS: Offsets only apply to the flick gestureJMT_PITCH_OFFSET or JMT_ROLL_OFFSET without ORIENTATION_ROTATION: Required for offset mathBLADE_DETECT_PIN and JMT_BLADE_DETECT defined: Pick one blade detection methodCHASSIS_DETECT_PIN and JMT_CHASSIS_DETECT_RANGE defined: Pick one chassis detection methodJMT_CHARGE_LOCKOUT without CHARGE_DETECT_PIN: Charge features need a detect pinJMT_CHARGE_STYLE_PRESET without CHARGE_DETECT_PIN: Charge features need a detect pinJMT_CHARGE_COMPLETE_ANNOUNCE without CHARGE_DETECT_PIN: Charge features need a detect pinJMT_BLADE_DETECT without ENABLE_POWER_FOR_ID: Required infrastructureJMT_BLADE_DETECT without BLADE_ID_SCAN_MILLIS: Required infrastructureJMT_BLADE_DETECT without BLADE_ID_TIMES: Required infrastructureJMT_CHASSIS_DETECT_RANGE without ENABLE_POWER_FOR_ID: Required infrastructure (shares BladeID scan)JMT_CHASSIS_DETECT_RANGE without BLADE_ID_SCAN_MILLIS: Required infrastructure (shares BladeID scan)JMT_CHASSIS_DETECT_RANGE without BLADE_ID_TIMES: Required infrastructure (shares BladeID scan)JMT_NO_BLADE_VALUE without JMT_BLADE_DETECT: Only applies when JMT blade detect is in useBLADE_ID_SCAN_MILLIS ≤ 0 with JMT_BLADE_DETECT: Must be greater than 0BLADE_ID_TIMES ≤ 0 with JMT_BLADE_DETECT: Must be greater than 0CHARGE_FULL_EXIT ≥ CHARGE_FULL_ENTER: Hysteresis requiredCHARGE_FULL_DWELL_MS ≤ 0: Must be greater than 0Recommended to address before flashing, but not blocking.
JMT_CHARGE_LOCKOUT without FETT263_SAVE_GESTURE_OFF: Prior gesture state will not be fully preserved across a charge cycleNo action required. Listed here for transparency.
NUM_BUTTONS == 1 automatically enables JMT_DISABLE_FAVORITES. The favorites system relies on AUX, so it cannot run on a single-button build.JMT_PITCH_OFFSET and JMT_ROLL_OFFSET default to 0 when JMT_FLICK_PRESETS is enabled.