Files
argo/web/controller-input.js
T
PitresAli 8ccd7689f7 Build Argo V2 control mappings and telemetry dashboard
Add configurable Xbox motion and action bindings, frame-aware six-axis MAVLink controls, sensor logging, camera viewing and recording, and a faster startup path. Synchronize both app copies and document companion hardware setup and verification limits.
2026-10-22 20:22:05 +02:00

126 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Controller conditioning is independent of drawing and MAVLink transport. */
((root)=>{
const clamp=value=>Math.max(-1,Math.min(1,Number.isFinite(value)?value:0));
function deadZone(value,zone=.08){
value=clamp(value);zone=Math.max(0,Math.min(.4,zone));
return Math.abs(value)<=zone?0:Math.sign(value)*(Math.abs(value)-zone)/(1-zone);
}
function defaults(pad){
return {turn:0,forward:1,vertical:pad?.axes?.length>=4?3:pad?.axes?.length>=3?2:-1,
invertTurn:false,invertForward:true,invertVertical:true,deadZone:.08};
}
function read(pad,config){
if(config?.version===2)return readProfile(pad,config);
if(!pad?.connected||!pad.axes)return null;
const axis=(index,invert)=>{
if(index===-1)return 0;
if(!Number.isInteger(index)||index<0||index>=pad.axes.length||!Number.isFinite(pad.axes[index]))return null;
return deadZone(pad.axes[index],config.deadZone)*(invert?-1:1);
};
let turn=axis(config.turn,config.invertTurn),forward=axis(config.forward,config.invertForward),vertical=axis(config.vertical,config.invertVertical);
if(turn===null||forward===null||vertical===null)return null;
const length=Math.hypot(turn,forward);if(length>1){turn/=length;forward/=length}
return {turn,forward,vertical};
}
class Smoother{
constructor(){this.reset()}
reset(){this.value=0;this.last=null;this.release=null}
step(target,now){
target=clamp(target);
const dt=this.last===null?0:Math.max(0,Math.min(.05,(now-this.last)/1000));this.last=now;
if(target===0){
if(!this.release)this.release={at:now,start:this.value};
const t=Math.min(1,Math.max(0,(now-this.release.at)/360)),ease=t*t*(3-2*t);
this.value=this.release.start*(1-ease);
}else{
this.release=null;
this.value+=(target-this.value)*(1-Math.exp(-dt/.1));
if(Math.abs(target-this.value)<.001)this.value=target;
}
return this.value;
}
}
class Motion{
constructor(){this.axes={turn:new Smoother(),forward:new Smoother(),vertical:new Smoother()}}
reset(){Object.values(this.axes).forEach(axis=>axis.reset())}
step(target,now){
const demand=Object.fromEntries(Object.entries(this.axes).map(([key,axis])=>[key,axis.step(target[key],now)]));
const length=Math.hypot(demand.turn,demand.forward);if(length>1){demand.turn/=length;demand.forward/=length}
return demand;
}
}
const names=['forward','lateral','vertical','turn','pitch','roll'];
const pair=(negative='off',positive='off')=>({negative,positive,scale:1});
function preset(name='triggers3'){
const bindings=Object.fromEntries(names.map(axis=>[axis,pair()]));
bindings.forward=pair('button:6','button:7');
bindings.vertical=pair('button:0','button:1');
bindings.turn=pair('axis:0:-','axis:0:+');
if(name==='sixaxis'){
bindings.lateral=pair('axis:2:-','axis:2:+');
bindings.pitch=pair('axis:1:-','axis:1:+');
bindings.roll=pair('button:4','button:5');
}else if(name==='classic'){
bindings.forward=pair('axis:1:+','axis:1:-');
bindings.vertical=pair('axis:3:+','axis:3:-');
}
return {version:2,preset:name,deadZone:.08,expo:.15,bindings,
actions:{lights:'button:2',stream:'button:3',record:'button:8'}};
}
function migrate(config,pad){
if(config?.version===2)return structuredClone(config);
if(!config)return preset(pad&&pad.mapping!=='standard'?'classic':'triggers3');
const result=preset('classic');
for(const axis of ['forward','vertical','turn']){
const index=config[axis],invert=config['invert'+axis[0].toUpperCase()+axis.slice(1)];
result.bindings[axis]=Number.isInteger(index)&&index>=0?
pair(`axis:${index}:${invert?'+':'-'}`,`axis:${index}:${invert?'-':'+'}`):pair();
}
result.deadZone=config.deadZone??.08;
result.actions={lights:'off',stream:'off',record:'off'};
return result;
}
function validProfile(config){
const source=value=>typeof value==='string'&&/^(off|button:(?:[0-9]|[12][0-9]|3[01])|axis:(?:[0-9]|1[0-5]):[+-])$/.test(value);
return config?.version===2&&Number.isFinite(config.deadZone)&&config.deadZone>=0&&config.deadZone<=.3&&
Number.isFinite(config.expo)&&config.expo>=0&&config.expo<=1&&names.every(axis=>{
const item=config.bindings?.[axis];return item&&source(item.negative)&&source(item.positive)&&
(item.negative==='off'||item.negative!==item.positive)&&Number.isFinite(item.scale)&&item.scale>=0&&item.scale<=1;
})&&['lights','stream','record'].every(action=>source(config.actions?.[action])&&!config.actions[action].startsWith('axis:'));
}
function inputValue(pad,source,zone){
if(source==='off')return 0;
const parts=source.split(':'),index=Number(parts[1]);
if(parts[0]==='button'){
const button=pad.buttons?.[index];if(!button||!Number.isFinite(button.value))return null;
return index===6||index===7?Math.max(0,deadZone(button.value,zone)):(button.pressed?1:0);
}
const value=pad.axes?.[index];
return Number.isFinite(value)?Math.max(0,deadZone(value,zone)*(parts[2]==='-'?-1:1)):null;
}
function readProfile(pad,config){
if(!pad?.connected||!validProfile(config))return null;
const result={};
for(const name of names){
const item=config.bindings[name],negative=inputValue(pad,item.negative,config.deadZone),positive=inputValue(pad,item.positive,config.deadZone);
if(negative===null||positive===null)return null;
const value=clamp(positive-negative);
result[name]=((1-config.expo)*value+config.expo*value**3)*item.scale;
}
return result;
}
function actions(pad,config){
return Object.fromEntries(Object.entries(config?.actions||{}).map(([name,source])=>[name,!!pad&&inputValue(pad,source,0)>.5]));
}
function sources(pad){
const choices=[['off','Off']],axes=['Left X','Left Y','Right X','Right Y'];
for(let index=0;index<Math.max(4,pad?.axes?.length||0);index++)for(const sign of ['-','+'])
choices.push([`axis:${index}:${sign}`,`${axes[index]||'Axis '+index} ${sign==='-'?'−':'+'}`]);
const buttons=['A','B','X','Y','LB','RB','LT','RT','View','Menu','Left click','Right click','D-pad up','D-pad down','D-pad left','D-pad right','Guide'];
for(let index=0;index<Math.max(17,pad?.buttons?.length||0);index++)choices.push([`button:${index}`,buttons[index]||'Button '+index]);
return choices;
}
root.ControllerInput={deadZone,defaults,read,Motion,names,preset,migrate,validProfile,actions,sources};
if(typeof module!=='undefined')module.exports=root.ControllerInput;
})(typeof window!=='undefined'?window:globalThis);