【6】DualSenseのチャタリング?問題対応
DualSenseの入力情報をRaspberryPi側に飛ばしましたが
期待する動作とは異なっています。
今回はその問題の修正にとりかかります。
目次
- 【1】Raspberry pi の、GPIOをTypescriptから操作
- 【2】DCモーターをPWMで速度制御
- 【3】サーボモーターを制御
- 【4】DualSenseをブラウザに接続
- 【5】DualSenseの情報をRaspberryPiに飛ばす
- 【6】DualSenseのチャタリング?問題対応
- 【7-1】RaspberryPiから低遅延で映像を飛ばす
- 【7-2】RaspberryPiから低遅延で映像を飛ばす
- 【8】ThreeJSでVRもどきを作成
- 【9-1】iPhoneの加速度から頭の向きをVRに反映
- 【9-2】スマホVRゴーグル向けデザインに変える
- 【10】ラジコン本体の製作
- 【11】パワーアップとバッテリー問題の解決
送られてくる信号がなんか違う
DualSenseから信号が取れました、が送られてくる信号が
■ 予想
ゲームパッドON! → 押した信号!
ゲームパッドOFF! → 離した信号!
■ 現実
ゲームパッドON! → ミリ秒単位で信号が送られ続ける
と言う状態になります。
映像で見るとこんな感じです。
この全自動連射機の信号を
一定時間内に送られてくる信号は、押しっぱなしと見なす。
という変換を行う必要があります。
おしっぱなし、とみなす時間が長いと、連射が出来ませんし
時間が短いと、ラジコンの操作がカクカクしますので
ちょうどいい塩梅に調整する必要があります。
押しっぱなしと連射
某名人に習うと、秒間16連射が、恐らく人類が出せる最大値として
一般人が可能な連射速度を、だいたい秒間10連射、と仮定すると
- 100ミリ秒に1度の入力を超える場合。
恐らく連射ではなく、押しっぱなしである。
と言う前提をまず決めます。
ハードウェア的にはミリ秒単位で処理できるので
今後のハードな使い方も考慮し
- 50ミリ秒以内の再入力は、押しっぱなしとみなす。
- 最終入力から、50ミリ秒経過した時、入力の終わりとみなす。
これを条件にします。
秒間20連射を超えるやつは、多分いないはず
押しっぱなし判定処理
つまるところ、都度時間を取得し
その差を計算してなにかする。ということになります。
ON操作といっしょに、時間を管理する変数に
取得したミリ秒を格納し
const now = new Date().getTime()
PushedTimer.up = now
console.log(gpio.write(16, 1))
- 取得したミリ秒と、変数に記録されたミリ秒を比較
- 50以上の場合に、OFF操作を実行する
この処理を追加します。
const intervalCheck = () => {
const now = new Date().getTime()
Object.keys(PushedTimer).forEach((button) => {
if (PushedTimer[button] > 0 && now - PushedTimer[button] > 50) {
if (ButtonMap[button] === false) {
switch (button) {
case 'up':
gpio.write(16, 0)
break
}
}
}
})
}
上記の方法を、アナログトリガーに適応した場合
上手く止まらず、停止後にすぐ加速したりと
カクカクした謎の動きをします。
トリガーから手を離しても急にOffにならず
短い時間で急速に0になるため
50ミリ秒で切ってしまうと、止まりきっておらず
カクつくの原因になるようです。
もう1点、DCモーターはアナログ値が0の場合、0をPWMに送ると
モーターがフルパワーでブンマワリます。
アナログ値が0になった場合は、1を送り、モーターを停止させる必要があります。
以上を踏まえ、何パターンか調整を行った結果
450ミリ秒開けると、妥当な動きになりました。
アナログ用のOff処理が下記になります。
Object.keys(TriggerTimer).forEach((button) => {
if (TriggerTimer[button] > 0 && (now - TriggerTimer[button]) > 450) {
if (ButtonMap[button] > 0) {
if (MotorPorts.MORTOR_POWER > 1) {
MotorPorts.MORTOR_POWER = 1
console.log('stop')
TriggerTimer[button] = 0
ButtonMap[button] = 1
console.log(gpio.write(MotorPorts.MOROT1_IN, 0))
console.log(gpio.write(MotorPorts.MOROT1_OUT, 0))
gpio.pwmWrite(MotorPorts.MORTOR_PWM, MotorPorts.MORTOR_POWER)
} else {
ButtonMap[button] = 0
}
}
}
})
このOff処理を、50ミリ秒のインターバルを挟んでループさせることで
ボタンを離した処理、を自動的に行います。
setTimeout(() => {
intervakCheck()
}, 50)
Off処理を含めたコード全体は下記になります。
control.helper.js
const gpio = require('./gpio.service')
let ButtonMap = {
up: false,
down: false,
left: false,
right: false,
left_stick_x: 0,
left_stick_y: 0,
left_stick: false,
right_stick_x: 0,
right_stick_y: 0,
right_stick: false,
triangle: false,
circle: false,
cross: false,
square: false,
l1: false,
l2: false,
l3: false,
r1: false,
r2: false,
r3: false,
left_axes: 1,
right_axes: 1,
start: false,
select: false,
}
const MotorPorts = {
MOROT1_IN: 19,
MOROT1_OUT: 26,
MORTOR_PWM: 12,
MORTOR_POWER: 1,
HANDLE: 17,
INITIAL_ANGLE: 850,
INITIAL: false
}
const setup = async () =>{
if (MotorPorts.INITIAL) {
return
}
gpio.addPort({port: MotorPorts.MOROT1_IN, mode: 'out'})
gpio.addPort({port: MotorPorts.MOROT1_OUT, mode: 'out'})
gpio.addPort({port: MotorPorts.MORTOR_PWM, mode: 'out'})
gpio.addPort({port: MotorPorts.HANDLE, mode: 'out'})
await gpio.initGpio()
console.log(gpio.write(MotorPorts.MOROT1_IN, 0))
console.log(gpio.write(MotorPorts.MOROT1_OUT, 0))
console.log(gpio.pwmWrite(MotorPorts.MORTOR_PWM, MotorPorts.MORTOR_POWER))
MotorPorts.INITIAL = true
gpio.searvoWrite(MotorPorts.HANDLE, MotorPorts.INITIAL_ANGLE)
intervalButton()
}
const control = (button) => {
const now = new Date().getTime()
ButtonMap = button
if (button.right_axes > 1) {
TriggerTimer.right_axes = now
MotorPorts.MORTOR_POWER = Number(button.right_axes)
console.log(gpio.write(MotorPorts.MOROT1_IN, 0))
console.log(gpio.write(MotorPorts.MOROT1_OUT, 1))
console.log(gpio.pwmWrite(MotorPorts.MORTOR_PWM, MotorPorts.MORTOR_POWER))
}
if (button.left_axes > 1 && button.right_axes === 1) {
TriggerTimer.left_axes = now
MotorPorts.MORTOR_POWER = Number(button.left_axes)
console.log(gpio.write(MotorPorts.MOROT1_IN, 1))
console.log(gpio.write(MotorPorts.MOROT1_OUT, 0))
console.log(gpio.pwmWrite(MotorPorts.MORTOR_PWM, MotorPorts.MORTOR_POWER))
}
if (button.up) {
PushedTimer.up = now
console.log(gpio.write(16, 1))
}
if (button.down) {
PushedTimer.down = now
console.log(gpio.write(20, 1))
}
if (button.left) {
PushedTimer.left = now
PushedTimer.right = 0
console.log(gpio.searvoWrite(MotorPorts.HANDLE, 1150))
}
if (button.right) {
PushedTimer.left = 0
PushedTimer.right = now
console.log(gpio.searvoWrite(MotorPorts.HANDLE, 550))
}
}
const PushedTimer = {
up: 0,
down: 0,
left: 0,
right: 0,
left_stick: 0,
right_stick: 0,
triangle: 0,
circle: 0,
cross: 0,
square: 0,
l1: 0,
l2: 0,
l3: 0,
r1: 0,
r2: 0,
r3: 0,
start: 0,
select: 0,
}
const TriggerTimer = {
left_stick_x: 0,
left_stick_y: 0,
right_stick_x: 0,
right_stick_y: 0,
left_axes: 0,
right_axes: 0,
}
const intervalButton = () => {
const now = new Date().getTime()
Object.keys(PushedTimer).forEach((button) => {
if (PushedTimer[button] > 0 && now - PushedTimer[button] > 50) {
if (ButtonMap[button] === false) {
switch (button) {
case 'left':
gpio.searvoWrite(MotorPorts.HANDLE, MotorPorts.INITIAL_ANGLE)
break
case 'right':
gpio.searvoWrite(MotorPorts.HANDLE, MotorPorts.INITIAL_ANGLE)
break
}
}
}
})
Object.keys(TriggerTimer).forEach((button) => {
if (TriggerTimer[button] > 0 && (now - TriggerTimer[button]) > 450) {
if (ButtonMap[button] > 0) {
if (MotorPorts.MORTOR_POWER > 1) {
MotorPorts.MORTOR_POWER = 1
console.log('stop')
TriggerTimer[button] = 0
ButtonMap[button] = 1
console.log(gpio.write(MotorPorts.MOROT1_IN, 0))
console.log(gpio.write(MotorPorts.MOROT1_OUT, 0))
gpio.pwmWrite(MotorPorts.MORTOR_PWM, MotorPorts.MORTOR_POWER)
} else {
ButtonMap[button] = 0
}
}
}
})
setTimeout(() => {
intervalButton()
}, 50)
}
module.exports = {
setup,
control
}
試験
変更を加えたコードでDualSenseのボタン操作を行うと
こんな感じで、想定通りの動きをしています。
ここまでで、ラジコンに必要な要素は大体揃った状態になりましたので
次回からは、ブラウザからラジコンの逆
ラジコン側から映像をブラウザに飛ばす処理に入ります。
UZAYA
Uzayaでは、多分仕事を求めています。
何かの役に立ちそうでしたら、是非お知らせを。