UNPKG

dualshock

Version:

Node.js module for DualShock (3 and 4) controllers.

242 lines (224 loc) 9.66 kB
//Node DualShock Library, ©2022 Pecacheu. GNU GPL v3.0 import fs from 'fs'; import hid from 'node-hid'; import chalk from 'chalk'; import {fileURLToPath} from 'url'; const RD_ERR="Error: could not read from HID device", MAPDIR=new URL('.',import.meta.url)+"mapping/", MAPPATH=fileURLToPath(MAPDIR); //Extra Useful Functions: function error(text, dt) { throw chalk.red("Error: ")+chalk.dim(text)+(dt?"\n"+dt:""); } function obMax(o) {let k=Object.keys(o),i=0,l=k.length,m=0,s;for(;i<l;i++)if(o[k[i]]>m)m=o[k[i]],s=i;return k[s]} function objAdd(a,b) {for(let k=Object.keys(b),i=0,l=k.length;i<l;i++)a[k[i]]=b[k[i]]} //Controller Mappings: const dir=fs.readdirSync(MAPPATH), mapping={}, api={}; for(let i=0,l=dir.length,f; i<l; i++) { f=dir[i].split('.'); if(f[1] == 'json') mapping[f[0]] = JSON.parse(fs.readFileSync(MAPPATH+dir[i], 'utf8')); else if(f[1] == 'js') api[f[0]] = await import(MAPDIR+dir[i]); } //Get a list of available gamepads. export function getDevices(type) { const hidDev=hid.devices(), dev=[]; for(let i=0,l=hidDev.length,d,t,s; i<l; i++) { d=hidDev[i]; t=getTypeAndStyle(d); s=t[1]; t=t[0]; if(t) { d.type=t; d.style=s; d.mode=getMode(d); dev.push(d); } } if(type) { if(typeof type == "string") { type=type.toLowerCase(); return dev.filter(function(d){return d.type==type}); } error("'"+type+"' is not a supported controller type!"); } else return dev; } //Get the model type of a gamepad. export function getType(dev) { return getTypeAndStyle(dev)[0]; } function getTypeAndStyle(dev) { if(dev.type != null) return [dev.type,dev.style]; const gk=Object.keys(mapping); for(let i=0,l=gk.length,m; i<l; i++) { m=mapping[gk[i]]; if(dev.vendorId == m.vendor) { if(typeof m.product == 'object') { const s=m.product, p=Object.keys(s); for(let a=0,b=p.length; a<b; a++) { if(dev.productId == p[a]) return [gk[i],s[p[a]]]; } } else if(dev.productId == m.product) return [gk[i],null]; } } return [false,null]; } function getMode(dev) { const type=getType(dev), a=api[type]; if(a && a._getMode) return a._getMode(dev); return null; } //Get a list of special features the gamepad supports. export function getFeatures(dev) { const type=getType(dev), m=mapping[type]; if(m) return m.special || []; return false; } //Open a gamepad device for communication. export function open(dev, opt) { if(typeof opt != "object") opt={}; let gType=getTypeAndStyle(dev), gStyle=gType[1], gmp; gType=gType[0]; if(!gType) error("HID device is not a supported controller!"); try {gmp=new hid.HID(dev.path)} catch(e) { error("Could not connect to the controller!", e); } //Gamepad API: const gFn=api[gType]; if(gFn) { if(gFn._init) gFn._init.call(gmp); if(gFn._parse) gmp.parser=gFn._parse.bind(gmp); for(let n=0,g=Object.keys(gFn),c=g.length,k; n<c; n++) { k=g[n]; if(k[0] != '_') gmp[k]=gFn[k].bind(gmp); } } //Internal Variables: gmp.type=gType; if(gStyle) gmp.style=gStyle; if(dev.mode) gmp.mode=dev.mode; gmp.msData={}; gmp.fData={}; gmp.map=Object.assign({},mapping[gType]); if(gmp.map.special) { const s=gmp.map.special, s2={}; for(let i=0,l=s.length; i<l; i++) s2[s[i]]=true; gmp.map.special=s2; } //Options Config: gmp.aSAmt = typeof opt.smoothAnalog == "number" ? opt.smoothAnalog : 5; gmp.aFAmt = typeof opt.joyDeadband == "number" ? opt.joyDeadband : 2; gmp.mSAmt = typeof opt.smoothMotion == "number" ? opt.smoothMotion : 5; gmp.mFAmt = typeof opt.moveDeadband == "number" ? opt.moveDeadband : 1; //Smoothing Data Storage: const aKeys=Object.keys(gmp.map.analog); for(let i=0,l=aKeys.length;i<l;i++) { gmp.msData[aKeys[i]]=[]; if(aKeys[i].indexOf('Stick') == 1) gmp.fData[aKeys[i]]=0; } if(gmp.map.motion) { const mKeys=Object.keys(gmp.map.motion); for(let i=0,l=mKeys.length;i<l;i++) { gmp.msData[mKeys[i]]=[]; gmp.fData[mKeys[i]]=0; } } //Event Listeners: gmp.on("data", parseInput.bind(gmp)); gmp.on("error", function(err) {if(err == RD_ERR) { if(gmp.ondisconnect) gmp. ondisconnect.call(gmp)} else if(gmp.onerror) gmp.onerror.call(gmp, err)}); return gmp; } //Generic Gamepad API: function parseInput(data) { let ofs=0; if(data[0] == this.map['bt-id-byte']) { if(this.map['bt-offset']) ofs=this.map['bt-offset']; } let uTrig,trig; if(this.ondigital || this.onupdate) { //Digital: const digital=parseDigital(data, this, ofs); if(!this.digital) this.digital=digital; uTrig=handle(digital, this.digital, this.ondigital?this.ondigital.bind(this):0); } if(this.onanalog || this.onupdate) { //Analog: const analog=parseAnalog(data, this, ofs); if(!this.analog) this.analog=analog; trig=handle(analog, this.analog, this.onanalog?this.onanalog.bind(this):0); if(trig && this.onupdate) {if(!uTrig) uTrig=trig; else objAdd(uTrig, trig)} } if(this.onmotion && this.map.special.motion) { //Motion: const motion=parseMotion(data, this, ofs); if(!this.motion) this.motion=motion; trig=handle(motion, this.motion, this.onmotion.bind?this.onmotion.bind(this):null); if(trig && this.onupdate) {if(!uTrig) uTrig=trig; else objAdd(uTrig, trig)} } if(this.onstatus && this.map.special) { //Status: const status=parseStatus(data, this, ofs); if(!this.status) this.status=status; trig=handle(status, this.status, this.onstatus.bind?this.onstatus.bind(this):null); if(trig && this.onupdate) {if(!uTrig) uTrig=trig; else objAdd(uTrig, trig)} } if(this.parser) { //Custom: if(!uTrig) uTrig={}; trig=this.parser(data,uTrig,ofs); if(trig && this.onupdate) objAdd(uTrig, trig); if(!Object.keys(uTrig).length) uTrig=null; } if(uTrig && this.onupdate) this.onupdate.call(this,uTrig); //Frame Update. } //Parsing Assist: function newDat() { return { get cross() {return this.a}, set cross(v) {}, get circle() {return this.b}, set circle(v) {}, get square() {return this.x}, set square(v) {}, get triangle() {return this.y}, set triangle(v) {} }} function parseDigital(data, gpad, ofs) { const dArr=newDat(), map=gpad.map.button, keys=Object.keys(map); for(let i=0,l=keys.length,key,dPos,dSub; i<l; i++) { key=keys[i]; if(key == 'hat') { dSub=data[map[key]+ofs] & 0x0F; let u=false,d=false,l=false,r=false; switch(dSub) { case 0: u=true; break; case 1: u=r=true; break; case 2: r=true; break; case 3: d=r=true; break; case 4: d=true; break; case 5: d=l=true; break; case 6: l=true; break; case 7: u=l=true; } dArr['up']=u; dArr['down']=d; dArr['left']=l; dArr['right']=r; } else { dPos=map[key][0]+ofs, dSub=map[key][1]; dArr[key]=(data[dPos] & 1<<dSub) != 0; if(map[key][2]) dArr[key]=!dArr[key]; } } return dArr; } function parseAnalog(data, gpad, ofs) { const dArr=newDat(), map=gpad.map.analog, keys=Object.keys(map), sArr=gpad.msData, fArr=gpad.fData, sAmt=gpad.aSAmt, fAmt=gpad.aFAmt; for(let i=0,l=keys.length,key,dPos,val; i<l; i++) { key=keys[i]; if(typeof map[key] == "object") { dPos=map[key][0]+ofs; val=0; const bits=map[key][2]; let b=map[key][1]; for(let o=0; o<bits; o++,b++) if(data[dPos+Math.floor(b/8)] & 1<<b%8) val += 1<<o; if(map[key][3]) val=val/map[key][3]*255; } else dPos=map[key]+ofs, val=data[dPos]; if(sAmt) val=smooth(val,sArr[key],sAmt); if(fAmt && typeof fArr[key] == "number") { val=filter(val, fArr[key], fAmt); fArr[key]=val; } dArr[key]=val; } return dArr; } function parseMotion(data, gpad, ofs) { const dArr={}, map=gpad.map.motion, keys=Object.keys(map), bits=Math.abs(map['bits']), sign=map['bits'] < 0, uMax=1 << bits, sMax=uMax/2, bytes=Math.floor(bits / 8), mask=(1 << (bits % 8))-1; for(let i=0,l=keys.length,key,val,b,o; i<l; i++) { key=keys[i]; if(key == 'bits') continue; o=map[key]+ofs, val=0; for(b=0; b<bytes; b++) val += data[o++] << b*8; val += (data[o] & mask) << b*8; if(sign && val >= sMax) val=val-uMax; dArr[key]=val; } return dArr; } function parseStatus(data, gpad, ofs) { const dArr={}, map=gpad.map.status, keys=Object.keys(map); for(let i=0,l=keys.length,key,mCon; i<l; i++) { key=keys[i], mCon=map[key]; if(typeof mCon == "object") { let mDat=Object.assign({},mCon); delete mDat["index"]; let dat=data[mCon["index"]+ofs],sKeys=Object.keys(mDat),mVal=obMax(mDat),lDst=mDat[mVal]; for(let s=0,g=sKeys.length,sKey,sVal,dst; s<g; s++) { sKey=sKeys[s], sVal=mDat[sKey]; if(typeof sVal == "number") { dst=mDat[sKey]-dat; if(dst>=0&&dst<lDst) dArr[key]=sKey,lDst=dst; } else if(parseStatusExp(sVal, data, ofs)) { dArr[key]=sKey; break; } } if(!dArr[key]) dArr[key]=mVal; } else dArr[key]=data[mCon+ofs]; } return dArr; } function parseStatusExp(arr, dat, ofs) { let tExp=0; for(let i=0,l=arr.length,exp; i<l; i++) { exp=arr[i]; if(exp[2] ? dat[exp[0]+ofs]>=exp[1] : dat[exp[0]+ofs]<=exp[1]) tExp++; } return tExp==l; } function smooth(input, prevData, amt) { let sum=0; prevData.push(input); if(prevData.length > amt) prevData.shift(); for(let i=0,l=prevData.length; i<l; i++) { sum += prevData[i]; } return Math.floor(sum / prevData.length); } function filter(input, prevVal, amt) { if(Math.abs(input - 127) <= amt) return 127; if(Math.abs(input - prevVal) > amt) return input; return prevVal; } function handle(data, prev, func) { const keys=Object.keys(data),u={}; for(let i=0,l=keys.length; i<l; i++) { if(data[keys[i]] != prev[keys[i]]) { prev[keys[i]]=data[keys[i]]; u[keys[i]]=true; if(func)func(keys[i],data[keys[i]]); } } return Object.keys(u).length?u:false; }