1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザから操作可能なラジコンを作る【6】

Last updated at Posted at 2025-03-04

【6】DualSenseのチャタリング?問題対応

DualSenseの入力情報をRaspberryPi側に飛ばしましたが
期待する動作とは異なっています。
今回はその問題の修正にとりかかります。


目次


送られてくる信号がなんか違う

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では、多分仕事を求めています。
何かの役に立ちそうでしたら、是非お知らせを。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?