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.
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');
}
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)}`);
}
});
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');
}
✓ 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
| Property | Type | Description |
|---|---|---|
connected |
boolean | Connection status |
deviceInfo |
object | {name, firmwareVersion, uid, pollRate} |
sensors |
object | All cached sensor values |
| 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 |
| 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 |
{
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
}
}
Learning Mode: Builds calibration as you use it. First value becomes center.
Calibrated Mode: Initialize with known values for instant setup.
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
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));
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');
| 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);
| 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 |
| 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 |
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();
// 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');
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');
}
| 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 |
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!
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 }
// 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);
// 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);
}
}
});
// Properly destroy LED UI and remove styles
led.destroy();
// The container element is now empty and can be reused
| 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 |