Skip to content
Open
4 changes: 2 additions & 2 deletions docs/docs/main/docs/configuration/input_device/joystick.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ let adc = saadc::SAADC::new(p.SAADC, Irqs, saadc_config,
],
);
saadc.calibrate().await;
let mut adc_dev = NrfAdc::new(adc, [AnalogEventType::Battery, AnalogEventType::Joystick(2)], 20 /* polling interval */, Some(350)/* light sleep interval */);
let mut adc_dev = NrfAdc::new(adc, [AnalogEventType::Battery, AnalogEventType::Joystick(2)], [0, 0], 20 /* polling interval */, Some(350)/* light sleep interval */);
let mut batt_proc = BatteryProcessor::new(1, 5);
let mut joy_proc = JoystickProcessor::new([[80, 0], [0, 80]], [29130, 29365], 6, &keymap);
let mut joy_proc = JoystickProcessor::new(0, [[80, 0], [0, 80]], [29130, 29365], 6, &keymap);
...
run_all!(matrix, adc_dev),
run_all! {
Expand Down
85 changes: 76 additions & 9 deletions docs/docs/main/docs/configuration/input_device/pmw33xx.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,84 @@ This should be added to the `central.rs`-File even if the sensor is on split per
:::

```rust
use rmk::input_device::pointing::{ PointingProcessor, PointingProcessorConfig };
use rmk::input_device::pointing::{
PointingProcessor, PointingProcessorConfig, PointingMode, ScrollConfig, SniperConfig
};

let pmw3360_proc_config = PointingProcessorConfig {
// invert_x: true, // invert axis if neccesary
// invert_y: true,
// swap_y: true,
..Default::default()
};
let pmw3360_proc_config = PointingProcessorConfig {
device_id: 0, // Match the id set on the PointingDevice (default 0)
// invert_x: true, // Invert X axis globally (all modes)
// invert_y: true, // Invert Y axis globally (all modes)
// swap_xy: true, // Swap X and Y axes globally
..Default::default()
};

let mut pmw3360_processor = PointingProcessor::new(&keymap, pmw3360_proc_config);
let mut pmw3360_processor = PointingProcessor::new(&keymap, pmw3360_proc_config);

run_all!(pmw3360_processor, /* other processors and devices */)
run_all!(pmw3360_processor, /* other processors and devices */)
```

## Per-Layer Pointing Modes

The `PointingProcessor` supports configuring different pointing behaviors for each layer. This is useful for:

- **Gaming**: Normal cursor on layer 0, sniper mode on layer 1
- **Productivity**: Cursor on layer 0, scroll mode on layer 1 for document navigation
- **CAD/Design**: Different precision levels for different tasks

### Available Modes

- **Cursor mode** (default): Normal mouse movement
- **Scroll mode**: Movement becomes scroll wheel/pan
- **Sniper mode**: Precision mode with reduced sensitivity

### Example Configuration

```rust
use rmk::input_device::pointing::{
PointingProcessor, PointingProcessorConfig, PointingMode, ScrollConfig, SniperConfig
};

let mut pointing_processor = PointingProcessor::new(&keymap, PointingProcessorConfig::default());

// Configure different modes for each layer
pointing_processor
.set_layer_mode(0, PointingMode::Cursor) // Layer 0: Normal cursor
Copy link
Copy Markdown
Contributor

@Schievel1 Schievel1 Feb 18, 2026

Choose a reason for hiding this comment

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

As I understand it everything should be possible to configure via keyboard.toml as well. Mostly because once you go configuring in Rust you can't use keyboard.toml anymore. So if users want to use those layer modes with their pointing processor they would be forced to convert their whole configuration to rust.

But see my comment in pointing.rs suggesting to only listen for events in PointingProcessor and let the users program controllers to emit them. This way we can avoid this whole configuration section.
Controllers must be written in Rust, but they can be combined with keyboard.toml configuration.

.set_layer_mode(1, PointingMode::Scroll(ScrollConfig {
divisor_x: 8, // Pan sensitivity (higher = slower)
divisor_y: 8, // Wheel sensitivity (higher = slower)
invert_x: false, // Set true to reverse horizontal pan direction
invert_y: false, // Set true to reverse scroll wheel direction
}))
.set_layer_mode(2, PointingMode::Sniper(SniperConfig {
divisor: 4, // Precision divisor (higher = slower)
invert_x: false, // Set true to reverse X movement in sniper mode
invert_y: false, // Set true to reverse Y movement in sniper mode
}));
```

### Mode Details

**Cursor Mode**
- Direct 1:1 mapping of sensor movement to cursor movement
- Best for general navigation and pointer control

**Scroll Mode**
- X-axis movement → horizontal pan
- Y-axis movement → vertical scroll wheel
- `divisor_x` / `divisor_y`: sensitivity per axis — higher = slower. **Set to `0` to disable that axis entirely** (e.g. `divisor_x: 0` disables panning)
- `invert_x`: reverses horizontal pan direction (independent of global `invert_x`)
- `invert_y`: reverses scroll wheel direction (independent of global `invert_y`)
- Recommended divisor values: 4–16 (default: 8)

**Sniper Mode**
- Reduces movement speed for precision aiming
- `divisor`: applies to both X and Y axes — higher = slower, more precise
- `invert_x` / `invert_y`: reverses movement per axis in sniper mode
- Recommended divisor values: 2–8 (default: 4)
- Useful for games, CAD, or detailed work

::: tip
Use momentary layer keys (`MO(n)`) in your keymap to temporarily activate different pointing modes. The motion accumulator automatically resets when switching layers to ensure smooth transitions.
:::

146 changes: 138 additions & 8 deletions docs/docs/main/docs/configuration/input_device/pmw3610.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ name = ...
name = ...
```

::: warning Multi-device ID assignment

`PointingEvent` carries a `device_id` so that each `PointingProcessor` can be paired
with a specific sensor. If you have sensors on both halves of a split keyboard,
assign distinct ids to avoid the central treating them as the same device:

```toml
# Central side sensor
[[split.central.input_device.pmw3610]]
id = 0

# Peripheral side sensor
[[split.peripheral.input_device.pmw3610]]
id = 1
```

The peripheral forwards events to the central with the `device_id` preserved.
The generated `PointingProcessorConfig` on the central will automatically use the
matching `device_id` for each sensor.

:::

::: warning Breaking change

Adding `device_id` to `PointingEvent` changes the serialized binary format used by the
split protocol. Both halves of a split keyboard **must** be flashed with the same
firmware version at the same time.

:::

## Rust configuration

Define a `PointingDevice` and add it to `run_all!` macro.
Expand Down Expand Up @@ -120,16 +150,116 @@ This should be added to the `central.rs`-File even if the sensor is on split per
:::

```rust
use rmk::input_device::pointing::{ PointingProcessor, PointingProcessorConfig };
use rmk::input_device::pointing::{
PointingProcessor, PointingProcessorConfig, PointingMode, ScrollConfig, SniperConfig
};

let pmw3360_proc_config = PointingProcessorConfig {
// invert_x: true, // invert axis if neccesary
// invert_y: true,
// swap_y: true,
..Default::default()
};
let pmw3610_proc_config = PointingProcessorConfig {
device_id: 0, // Match the id set on the PointingDevice (default 0)
// invert_x: true, // Invert X axis globally (all modes)
// invert_y: true, // Invert Y axis globally (all modes)
// swap_xy: true, // Swap X and Y axes globally
..Default::default()
};

let mut pmw3360_processor = PointingProcessor::new(&keymap, pmw3360_proc_config);
let mut pmw3610_processor = PointingProcessor::new(&keymap, pmw3610_proc_config);

run_all!(pmw3610_processor, /* other processors and devices */)
```

## Per-Layer Pointing Modes

You can configure different pointing behaviors for each layer. This allows you to use the same pointing device for different purposes:

- **Cursor mode** (default): Normal mouse movement
- **Scroll mode**: Converts movement to scroll wheel/pan
- **Sniper mode**: Precision mode with reduced sensitivity

### Example: Three-Mode Configuration

```rust
use rmk::input_device::pointing::{
PointingProcessor, PointingProcessorConfig, PointingMode, ScrollConfig, SniperConfig
};

let mut pointing_processor = PointingProcessor::new(&keymap, PointingProcessorConfig::default());

// Configure per-layer modes
pointing_processor
.set_layer_mode(0, PointingMode::Cursor) // Layer 0: Normal cursor
.set_layer_mode(1, PointingMode::Scroll(ScrollConfig {
divisor_x: 8, // Pan sensitivity (higher = slower). 0 disables horizontal pan.
divisor_y: 8, // Wheel sensitivity (higher = slower). 0 disables vertical scroll.
invert_x: false, // Set true to reverse horizontal pan direction
invert_y: false, // Set true to reverse scroll wheel direction
})) // Layer 1: Scroll mode
.set_layer_mode(2, PointingMode::Sniper(SniperConfig {
divisor: 4, // Precision divisor (higher = slower, more precise)
invert_x: false, // Set true to reverse X movement in sniper mode
invert_y: false, // Set true to reverse Y movement in sniper mode
})); // Layer 2: Sniper mode
```

### Usage in Keymap

Use momentary layer switches (`MO(n)`) or layer toggles (`TG(n)`) to activate different pointing modes:

```rust
// In your keymap:
// - Hold MO(1) to activate scroll mode (trackball becomes scroll wheel)
// - Hold MO(2) to activate sniper mode (precision aiming)
// - Release to return to normal cursor mode

let keymap = [
// Layer 0 (Cursor mode)
[KC_A, KC_B, MO(1), MO(2), ...],

// Layer 1 (Scroll mode - activated by MO(1))
[KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, ...],

// Layer 2 (Sniper mode - activated by MO(2))
[KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, ...],
];
```

### Mode Details

#### Cursor Mode
- Direct 1:1 mapping of sensor movement to cursor movement
- Best for general navigation and pointer control

#### Scroll Mode
- X-axis movement → horizontal pan
- Y-axis movement → vertical scroll wheel
- `divisor_x` / `divisor_y`: sensitivity per axis — higher = slower. **Set to `0` to disable that axis entirely** (e.g. `divisor_x: 0` disables panning)
- `invert_x`: reverses horizontal pan direction (independent of global `invert_x`)
- `invert_y`: reverses scroll wheel direction (independent of global `invert_y`)
- Recommended divisor values: 4–16 (default: 8)

#### Sniper Mode
- Reduces movement speed for precision aiming
- `divisor`: applies to both X and Y axes — higher = slower, more precise
- `invert_x` / `invert_y`: reverses movement per axis in sniper mode
- Recommended divisor values: 2–8 (default: 4)
- Useful for games, CAD, or detailed work

### Alternative: Configure All Layers at Once

```rust
use rmk::input_device::pointing::{PointingMode, ScrollConfig, SniperConfig};

// Assuming 4 layers
let layer_modes = [
PointingMode::Cursor, // Layer 0
PointingMode::Scroll(ScrollConfig::default()), // Layer 1
PointingMode::Sniper(SniperConfig { divisor: 4, ..Default::default() }), // Layer 2
PointingMode::Cursor, // Layer 3
];

let pointing_processor = PointingProcessor::new(&keymap, PointingProcessorConfig::default())
.with_layer_modes(layer_modes);
```

::: tip
When switching between layers, the motion accumulator is automatically reset to prevent unexpected jumps. This ensures smooth transitions between different pointing modes.
:::
2 changes: 1 addition & 1 deletion docs/docs/main/docs/features/input_device.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ use rmk::run_all;
// Create your devices and processors
let mut matrix = Matrix::new(row_pins, col_pins, debouncer);
let mut encoder = RotaryEncoder::new(pin_a, pin_b, 0);
let mut adc_device = NrfAdc::new(saadc, event_types, interval, None);
let mut adc_device = NrfAdc::new(saadc, [AnalogEventType::Battery], [0], interval, None);
let mut batt_proc = BatteryProcessor::new(2000, 2806);

// Run them concurrently using join and run_all!
Expand Down
1 change: 1 addition & 0 deletions examples/use_rust/nrf52840_ble/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ async fn main(spawner: Spawner) {
let mut adc_device = NrfAdc::new(
saadc,
[AnalogEventType::Battery],
[0],
embassy_time::Duration::from_secs(12),
None,
);
Expand Down
1 change: 1 addition & 0 deletions examples/use_rust/nrf52840_ble_split/src/central.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ async fn main(spawner: Spawner) {
let mut adc_device = NrfAdc::new(
saadc,
[AnalogEventType::Battery],
[0],
embassy_time::Duration::from_secs(12),
None,
);
Expand Down
1 change: 1 addition & 0 deletions examples/use_rust/nrf52840_ble_split/src/peripheral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ async fn main(spawner: Spawner) {
let mut adc_device = NrfAdc::new(
saadc,
[AnalogEventType::Battery],
[0],
embassy_time::Duration::from_secs(12),
None,
);
Expand Down
23 changes: 22 additions & 1 deletion examples/use_rust/nrf52840_ble_split_dongle/src/central.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ use rmk::input_device::Runnable;
use rmk::input_device::adc::{AnalogEventType, NrfAdc};
use rmk::input_device::battery::BatteryProcessor;
use rmk::input_device::pmw3610::{BitBangSpiBus, Pmw3610, Pmw3610Config};
use rmk::input_device::pointing::{PointingDevice, PointingProcessor, PointingProcessorConfig};
use rmk::input_device::pointing::{
PointingDevice, PointingMode, PointingProcessor, PointingProcessorConfig, ScrollConfig, SniperConfig,
};
use rmk::input_device::rotary_encoder::RotaryEncoder;
use rmk::keyboard::Keyboard;
use rmk::matrix::Matrix;
Expand Down Expand Up @@ -236,12 +238,31 @@ async fn main(spawner: Spawner) {
let pmw3610_spi = BitBangSpiBus::new(pmw3610_sck, pmw3610_sdio);
let mut pmw3610_device =
PointingDevice::<Pmw3610<_, _, _>>::new(0, pmw3610_spi, pmw3610_cs, pmw3610_motion, pmw3610_config);

// Configure pointing processor with per-layer modes:
// Layer 0: Normal cursor movement
// Layer 1: Scroll mode (trackball becomes scroll wheel)
// Layer 2: Sniper mode (precision, 1/4 speed)
let mut pointing_processor = PointingProcessor::new(&keymap, PointingProcessorConfig::default());
pointing_processor
.set_layer_mode(0, PointingMode::Cursor)
.set_layer_mode(1, PointingMode::Scroll(ScrollConfig {
divisor_x: 8, // Pan sensitivity (higher = slower)
divisor_y: 8, // Wheel sensitivity (higher = slower)
invert_x: false, // Set true to reverse horizontal pan direction
invert_y: false, // Set true to reverse scroll wheel direction
}))
.set_layer_mode(2, PointingMode::Sniper(SniperConfig {
divisor: 4, // Precision divisor (higher = slower, more precise)
invert_x: false, // Set true to reverse X movement in sniper mode
invert_y: false, // Set true to reverse Y movement in sniper mode
}));

// Initialize the encoder processor
let mut adc_device = NrfAdc::new(
saadc,
[AnalogEventType::Battery],
[0],
embassy_time::Duration::from_secs(12),
None,
);
Expand Down
3 changes: 3 additions & 0 deletions rmk-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,9 @@ pub struct InputDeviceConfig {
pub struct JoystickConfig {
// Name of the joystick
pub name: String,
/// Device id used to match this joystick with its JoystickProcessor.
/// If omitted, ids are assigned sequentially starting from 0.
pub id: Option<u8>,
// Pin a of the joystick
pub pin_x: String,
// Pin b of the joystick
Expand Down
Loading