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.
This commit is contained in:
2026-10-22 20:22:05 +02:00
parent 5e91c28f1e
commit 8ccd7689f7
38 changed files with 4295 additions and 1897 deletions
+73 -1
View File
@@ -10,6 +10,7 @@
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;
@@ -48,6 +49,77 @@
return demand;
}
}
root.ControllerInput={deadZone,defaults,read,Motion};
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);