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


## 【６】DualSenseのチャタリング？問題対応

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


<br></br>
## 目次
* [【１】Raspberry pi の、GPIOをTypescriptから操作](https://qiita.com/Yasukosan/items/1bf9480c10de7d69c2a4)
* [【２】DCモーターをPWMで速度制御](https://qiita.com/Yasukosan/items/291c76c10be135fd7595)
* [【３】サーボモーターを制御](https://qiita.com/Yasukosan/items/e81c9c9d0159b32e5893)
* [【４】DualSenseをブラウザに接続](https://qiita.com/Yasukosan/items/bf3a085145db84a48823)
* [【５】DualSenseの情報をRaspberryPiに飛ばす](https://qiita.com/Yasukosan/items/400883be94751966221a)
* [【６】DualSenseのチャタリング？問題対応](https://qiita.com/Yasukosan/items/7e52609699b4b71196ca)
* [【７-１】RaspberryPiから低遅延で映像を飛ばす](https://qiita.com/Yasukosan/items/98426f1ba4dea4c9ec5f)
* [【７-２】RaspberryPiから低遅延で映像を飛ばす](https://qiita.com/Yasukosan/items/2246c8a32602ca77ace9)
* [【８】ThreeJSでVRもどきを作成](https://qiita.com/Yasukosan/items/e15e8fd002cf49e3d04d)
* [【９-１】iPhoneの加速度から頭の向きをVRに反映](https://qiita.com/Yasukosan/items/9b4333770baea4110361)
* [【９-２】スマホVRゴーグル向けデザインに変える](https://qiita.com/Yasukosan/items/7f1bb7a9d95cb76f8117)
* [【１０】ラジコン本体の製作](https://qiita.com/Yasukosan/items/0d37c1df5b5d6c02257b)
* 【１１】パワーアップとバッテリー問題の解決

<br></br>
## 送られてくる信号がなんか違う
DualSenseから信号が取れました、が送られてくる信号が

■ 予想
ゲームパッドON！　→　押した信号！
ゲームパッドOFF！　→　離した信号！

■ 現実
ゲームパッドON！　→　ミリ秒単位で信号が送られ続ける


と言う状態になります。
映像で見るとこんな感じです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/PhQMpJuojKY?si=uG30kJEU6-DG9Ikp" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


この全自動連射機の信号を

**一定時間内に送られてくる信号は、押しっぱなしと見なす。**

という変換を行う必要があります。
おしっぱなし、とみなす時間が長いと、連射が出来ませんし
時間が短いと、ラジコンの操作がカクカクしますので
ちょうどいい塩梅に調整する必要があります。

<br></br>
## 押しっぱなしと連射

某名人に習うと、秒間１６連射が、恐らく人類が出せる最大値として
一般人が可能な連射速度を、だいたい秒間１０連射、と仮定すると

* **１００ミリ秒に１度の入力を超える場合。
恐らく連射ではなく、押しっぱなしである。**

と言う前提をまず決めます。

ハードウェア的にはミリ秒単位で処理できるので
今後のハードな使い方も考慮し

* **５０ミリ秒以内の再入力は、押しっぱなしとみなす。**
* **最終入力から、５０ミリ秒経過した時、入力の終わりとみなす。**

これを条件にします。
秒間20連射を超えるやつは、多分いないはず


<br></br>
## 押しっぱなし判定処理

つまるところ、都度時間を取得し
その差を計算してなにかする。ということになります。


ON操作といっしょに、時間を管理する変数に
取得したミリ秒を格納し

```typescript
const now = new Date().getTime()
PushedTimer.up = now
console.log(gpio.write(16, 1))
```

* 取得したミリ秒と、変数に記録されたミリ秒を比較
* ５０以上の場合に、OFF操作を実行する

この処理を追加します。

```typescript
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にならず
短い時間で急速に０になるため
５０ミリ秒で切ってしまうと、止まりきっておらず
カクつくの原因になるようです。

もう１点、DCモーターはアナログ値が０の場合、０をPWMに送ると
モーターがフルパワーでブンマワリます。
アナログ値が０になった場合は、１を送り、モーターを停止させる必要があります。

以上を踏まえ、何パターンか調整を行った結果
４５０ミリ秒開けると、妥当な動きになりました。

アナログ用のOff処理が下記になります。

```typescript
    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処理を、５０ミリ秒のインターバルを挟んでループさせることで
ボタンを離した処理、を自動的に行います。
```typescript
    setTimeout(() => {
        intervakCheck()
    }, 50)
```





Off処理を含めたコード全体は下記になります。
<details><summary>control.helper.js</summary>

```javascript
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
}
```

</details>


## 試験

変更を加えたコードでDualSenseのボタン操作を行うと
こんな感じで、想定通りの動きをしています。

<iframe width="560" height="315" src="https://www.youtube.com/embed/ZNUHQOjTe8k?si=sndczVFBYbRShtK7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

ここまでで、ラジコンに必要な要素は大体揃った状態になりましたので
次回からは、ブラウザからラジコンの逆
ラジコン側から映像をブラウザに飛ばす処理に入ります。

<br></br>
<br></br>
# UZAYA
Uzayaでは、多分仕事を求めています。
何かの役に立ちそうでしたら、是非お知らせを。