MH4 Documentation

WebHID Driver and Modules for MH4 Controller

⚠️ Critical: Setup Mode Required

Controller MUST be in setup mode (100-300Hz) to work with these modules.

To enter setup mode: Hold the PS button while connecting the USB cable.

If the controller is at 1000Hz or higher, it's in normal mode and won't work.

MH4 Documentation (Markdown)

            

Requirements

  • Chrome or Edge browser (WebHID support required)
  • HTTPS or localhost (WebHID requirement)
  • MH4 controller in setup mode (100-300Hz)

Minimal Example

import { MH4, Events } from 'https://developer.mariusheier.com/modules/core/mh4-hid-streamlined.js';

const mh4 = new MH4();

// Listen for setup mode requirement
mh4.addEventListener(Events.SETUP_MODE_REQUIRED, (e) => {
    alert(`Controller is at ${e.detail.pollRate}Hz!
Hold PS button while connecting USB cable.`);
});

// Connect to controller
if (await mh4.connect()) {
    console.log('Connected!', mh4.deviceInfo);
    
    // Listen for sensor changes (non-blocking)
    mh4.addEventListener(Events.DATA_UPDATE, (e) => {
        const { type, data } = e.detail;
        
        if (type === 'left-stick') {
            console.log(`Left: ${data.x}, ${data.y}`);
        }
    });
    
    // Or get cached values anytime (non-blocking)
    const sensors = mh4.getSensorData('all');
}

Complete Integration Example

import { MH4, Events } from 'https://developer.mariusheier.com/modules/core/mh4-hid-streamlined.js';
import { LUT } from 'https://developer.mariusheier.com/modules/core/lut.js';

const mh4 = new MH4();
const leftLUT = new LUT('left');

// Initialize LUT with known calibration (instant setup)
leftLUT.initializeWithCalibration({
    center: { x: 8192, y: 8192 },
    xRange: { min: 0, max: 16383 },
    yRange: { min: 0, max: 16383 },
    invertX: true,
    invertY: false
});

// Set response curve
leftLUT.setActiveLUT('smooth'); // Exponential smoothing

// Connect and process data
await mh4.connect();

mh4.addEventListener(Events.DATA_UPDATE, (e) => {
    if (e.detail.type === 'left-stick') {
        // Transform raw values to -1 to +1 range
        const normalized = leftLUT.getTransformed(
            e.detail.data.x, 
            e.detail.data.y
        );
        
        console.log(`Normalized: ${normalized.x.toFixed(2)}, ${normalized.y.toFixed(2)}`);
    }
});

CDN Import

Key Features

  • Non-blocking: All sensor reads are cached, never blocks UI
  • Event-driven: Subscribe to changes instead of polling
  • Auto-detection: Detects if controller is in wrong mode
  • Background updates: Continuous sensor monitoring

Connection Flow

const mh4 = new MH4();

// IMPORTANT: Set up event listeners BEFORE connecting
mh4.addEventListener(Events.CONNECTED, (e) => {
    console.log('Device:', e.detail.deviceName);
    console.log('Firmware:', e.detail.firmwareVersion);
    console.log('Poll Rate:', e.detail.pollRate);
});

mh4.addEventListener(Events.SETUP_MODE_REQUIRED, (e) => {
    // Controller is not in setup mode!
    console.error(`Controller at ${e.detail.pollRate}Hz, needs 100-300Hz`);
    alert('Hold PS button while connecting USB!');
});

mh4.addEventListener(Events.ERROR, (e) => {
    console.error('Error:', e.detail.message);
});

// Connect (shows device picker)
const success = await mh4.connect();
if (!success) {
    console.error('Connection failed');
}

Reading Sensors

✓ Recommended: Use cached reads and events (non-blocking)

// Method 1: Event-driven (best for real-time)
mh4.addEventListener(Events.DATA_UPDATE, (e) => {
    const { type, data } = e.detail;
    
    switch(type) {
        case 'left-stick':
            // data.x and data.y are 0-16383 (14-bit)
            updateLeftStick(data.x, data.y);
            break;
        case 'right-stick':
            updateRightStick(data.x, data.y);
            break;
        case 'triggers':
            // data.l2 and data.r2 are 0-4095 (12-bit)
            updateTriggers(data.l2, data.r2);
            break;
        case 'buttons':
            // data.extra1 through data.extra6 are boolean
            updateButtons(data);
            break;
    }
});

// Method 2: Get cached values (best for periodic checks)
setInterval(() => {
    const all = mh4.getSensorData('all');
    // Returns immediately with cached values
    updateDisplay(all);
}, 16); // 60fps, won't block

✗ Avoid: Don't use readSensorDirect() in loops - causes stuttering

API Reference

Properties

Property Type Description
connected boolean Connection status
deviceInfo object {name, firmwareVersion, uid, pollRate}
sensors object All cached sensor values

Methods

Method Returns Description
connect() Promise<boolean> Connect to device (shows picker)
disconnect() Promise<void> Disconnect from device
getSensorData(type) object Get cached sensor data (non-blocking)
readSensorDirect(type) Promise<object> Force read (avoid in loops!)
reboot() Promise<void> Exit setup mode, return to normal

Events

Event Detail Description
Events.CONNECTED {deviceName, firmwareVersion, pollRate, uid} Successfully connected
Events.DISCONNECTED - Device disconnected
Events.DATA_UPDATE {type, data, all} Sensor value changed
Events.SETUP_MODE_REQUIRED {pollRate, deviceName} Wrong mode detected
Events.ERROR {message} Error occurred

Sensor Data Format

{
    leftStick: {
        x: 0-16383,  // 14-bit value
        y: 0-16383,  // 14-bit value
        timestamp: 123456789
    },
    rightStick: {
        x: 0-16383,  // 14-bit value
        y: 0-16383,  // 14-bit value
        timestamp: 123456789
    },
    triggers: {
        l2: 0-4095,  // 12-bit value
        r2: 0-4095,  // 12-bit value
        timestamp: 123456789
    },
    buttons: {
        extra1: false,  // PD0
        extra2: false,  // PD1
        extra3: false,  // PD2
        extra4: false,  // PD3
        extra5: false,  // PD4
        extra6: false,  // PD15
        timestamp: 123456789
    }
}

CDN Import

Two Operating Modes

Learning Mode: Builds calibration as you use it. First value becomes center.

Calibrated Mode: Initialize with known values for instant setup.

Quick Setup (Calibrated Mode)

import { LUT } from 'https://developer.mariusheier.com/modules/core/lut.js';

const lut = new LUT('left');

// Initialize with known calibration (instant setup)
lut.initializeWithCalibration({
    center: { x: 8192, y: 8192 },
    xRange: { min: 0, max: 16383 },
    yRange: { min: 0, max: 16383 },
    invertX: true,
    invertY: false
});

// Apply response curve
lut.setActiveLUT('smooth');

// Ready to use immediately!
const normalized = lut.getTransformed(rawX, rawY);
console.log(normalized.x, normalized.y); // -1 to +1 range

Learning Mode (Auto-Calibration)

const lut = new LUT('left');

// Feed values - first one becomes center
// IMPORTANT: Make sure stick is centered for first value!
lut.addValue(8192, 8192); // This becomes center

// As you move the stick, feed more values
mh4.addEventListener(Events.DATA_UPDATE, (e) => {
    if (e.detail.type === 'left-stick') {
        const { x, y } = e.detail.data;
        
        // Add to learning set
        lut.addValue(x, y);
        
        // Get transformed (auto-calibrates as it learns)
        const transformed = lut.getTransformed(x, y);
    }
});

// Save calibration when done
const calibration = lut.serialize();
localStorage.setItem('calibration', JSON.stringify(calibration));

Dual Stick Support

import { DualStickLUT } from 'https://developer.mariusheier.com/modules/core/lut.js';

const dualLUT = new DualStickLUT();

// Initialize both sticks at once
dualLUT.initializeWithCalibration({
    left: {
        center: { x: 8192, y: 8192 },
        xRange: { min: 0, max: 16383 },
        yRange: { min: 0, max: 16383 },
        invertX: true,
        invertY: false
    },
    right: {
        center: { x: 8192, y: 8192 },
        xRange: { min: 0, max: 16383 },
        yRange: { min: 0, max: 16383 },
        invertX: false,
        invertY: false
    }
});

// Transform values for each stick
const leftNorm = dualLUT.getTransformed(leftX, leftY, 'left');
const rightNorm = dualLUT.getTransformed(rightX, rightY, 'right');

Response Curves

Curve Description Use Case
linear No modification (1:1) Raw input
smooth Exponential (power 2.2) Smooth camera control
deadzone 10% center deadzone Eliminate drift
precision S-curve (fine center, fast edges) Precise aiming
// Apply a response curve
lut.setActiveLUT('smooth');

// Check available curves
console.log(lut.availableLUTs);
// ['linear', 'smooth', 'deadzone', 'precision']

// Remove curve (back to linear)
lut.setActiveLUT(null);

API Reference

LUT Class

Method Returns Description
new LUT(stickId) LUT Create LUT for 'left' or 'right' stick
initializeWithCalibration(config) void Set up with known calibration (calibrated mode)
addValue(x, y) void Add raw value (learning mode only)
getTransformed(x, y) {x, y} Get normalized values (-1 to +1)
setInversion(invertX, invertY) void Set axis inversion
setActiveLUT(name) void Apply response curve
serialize() object Export all calibration data
deserialize(data) void Import calibration data

Properties

Property Type Description
mode string 'learning' or 'calibrated'
center {x, y} Center point
xRange {min, max} X axis range
yRange {min, max} Y axis range
isReady boolean Ready to transform values
hasData boolean Has calibration data

CDN Import

Basic Usage

import { DataStore } from 'https://developer.mariusheier.com/modules/core/data-store.js';

const store = new DataStore('myapp');

// Store any JSON-serializable data
store.set('settings', {
    theme: 'dark',
    sensitivity: 0.8
});

// Retrieve with optional default
const settings = store.get('settings', { theme: 'light' });

// Check existence
if (store.exists('calibration')) {
    const cal = store.get('calibration');
}

// Remove item
store.remove('old-data');

// Clear all app data
store.clear();

Namespaced Storage

// Use sub-namespaces for organization
store.set('profile', userData, 'user:123');
store.set('profile', userData, 'user:456');

// Retrieve from namespace
const user123 = store.get('profile', null, 'user:123');

// Clear specific namespace
store.clear('user:123');

// List keys in namespace
const keys = store.keys('user:123');

Dual Stick Storage

import { DualStickDataStore } from 'https://developer.mariusheier.com/modules/core/data-store.js';

const dualStore = new DualStickDataStore();

// Save calibration for each stick
dualStore.setStickData('left', leftLUT.serialize());
dualStore.setStickData('right', rightLUT.serialize());

// Load calibration
const leftData = dualStore.getStickData('left');
if (leftData) {
    leftLUT.deserialize(leftData);
}

// Get both sticks at once
const dualData = dualStore.getDualStickData();
// Returns: { version: 2, left: {...}, right: {...}, metadata: {...} }

// Check if calibrated
if (dualStore.hasStickData('left')) {
    console.log('Left stick is calibrated');
}

API Reference

DataStore Methods

Method Returns Description
set(key, value, subNamespace) boolean Store value (auto JSON serialized)
get(key, defaultValue, subNamespace) any Retrieve value (auto JSON parsed)
exists(key, subNamespace) boolean Check if key exists
remove(key, subNamespace) void Delete key
clear(subNamespace) void Clear namespace or all
keys(subNamespace) string[] List all keys
getAll(pattern) object Get all matching items

CDN Import

Initialization

LED module creates its own UI. Just provide a container element ID.

import { MH4LED } from 'https://developer.mariusheier.com/modules/ui/mh4-led.js';

const led = new MH4LED();

// Initialize with container and options
led.init('led-container', {
    mode: 'breathing',      // 'off', 'static', 'breathing'
    intensity: 100,           // 0-100
    primaryColor: '#FF00FF',  // Hex color
    secondaryColor: '#00FFFF', // For breathing mode
    period: 3                 // Breathing period (seconds)
});

// The LED UI is now rendered in the container!

Programmatic Control

Important: Use setMode(), setIntensity(), etc. NOT updateSettings() (doesn't exist!)

// Change mode
led.setMode('static');     // 'off', 'static', 'breathing'

// Adjust brightness
led.setIntensity(75);       // 0-100

// Change colors
led.setColors('#FF0000', '#0000FF'); // primary, secondary

// Adjust breathing speed
led.setPeriod(2.5);         // seconds

// Get current state
const state = led.getState();
console.log(state);
// { mode, intensity, primaryColor, secondaryColor, period }

Event Handling

// Listen for any state change
led.on('state-changed', (state) => {
    console.log('LED state:', state);
});

// Listen for specific changes
led.on('mode-changed', (mode) => {
    console.log('Mode:', mode);
});

led.on('intensity-changed', (intensity) => {
    console.log('Brightness:', intensity);
});

led.on('color-changed', ({ primary, secondary }) => {
    console.log('Colors:', primary, secondary);
});

// Remove listener
const handler = (state) => console.log(state);
led.on('state-changed', handler);
led.off('state-changed', handler);

Integration Example

// React to controller input
mh4.addEventListener(Events.DATA_UPDATE, (e) => {
    const { type, data } = e.detail;
    
    if (type === 'triggers') {
        // Map trigger pressure to LED brightness
        const brightness = Math.round((data.l2 / 4095) * 100);
        led.setIntensity(brightness);
        
        // Map R2 to color hue
        const hue = Math.round((data.r2 / 4095) * 360);
        const color = `hsl(${hue}, 100%, 50%)`;
        led.setColors(color, color);
    }
    
    if (type === 'buttons') {
        // Button 1 cycles modes
        if (data.extra1) {
            const modes = ['off', 'static', 'breathing'];
            const current = led.getState().mode;
            const next = modes[(modes.indexOf(current) + 1) % 3];
            led.setMode(next);
        }
    }
});

Cleanup

// Properly destroy LED UI and remove styles
led.destroy();

// The container element is now empty and can be reused

API Reference

Methods

Method Returns Description
init(containerId, options) void Create and render LED UI
setMode(mode) void Set mode: 'off', 'static', 'breathing'
setIntensity(value) void Set brightness (0-100)
setColors(primary, secondary) void Set colors (hex format)
setPeriod(seconds) void Set breathing period (0.5-10)
getState() object Get current state
on(event, callback) this Add event listener
off(event, callback) this Remove event listener
destroy() void Clean up UI and styles