0
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?

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

Last updated at Posted at 2025-03-04

【5】DualSenseの情報をRaspberryPiに飛ばす

DualSenseの入力が取得できたので、次はRaspberryPi側に飛ばします。
できるだけ低遅延のほうが間違いなく良いので
WebSocket経由で情報を飛ばします。


目次


WebSocketクライアント作成

WebSocket自体の解説は、先人の詳しい情報が山程溢れていますので
可能な限り端折っていきます。

WebSocketでデータを送信するクライアント部分になります。
例示しているのは、かなり古いコードですが
データを送るだけならば特に問題はないので、そのまま使います。
例によって簡単なサービス化されたファイルになっています。

必要な機能としては

  • 後の方で必須になるので、適当なサーバー証明でhttps化出来ていること
  • Socket送信部分が、Callbackに渡せるように

以上が満たせていれば、ChatGPTにでも
「WebSocketを使ったクライアント側の機能を作って」
のように軽い感じで返ってきたコードで必要十分と思います。

本件の制作環境では、nextjsで制作を進めたので
package.jsonのスクリプトに、13.5以降で追加された
「--experimental-https」オプションを追記し、クライアント側のhttps化を行っています。

  "scripts": {
    "dev": "next dev --turbopack --experimental-https",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "depen

ファイル名は「socket-client.ts」にて制作しています。

コードの全文はこちらから

socket-client.tsの中身

type wsData {
    job: string;
    room?: string;
    client?: string;
    data: any;
}

type NextCallType = {
    [K in 'open' | 'message' | 'close' | 'error']: 
        ((e: Event | MessageEvent) => void) | false 
}

export class SocketClient2Service {
    private static instance: SocketClient2Service;

    private socket: WebSocket | undefined;
    private port = 3000;
    private url = 'ws://localhost';

    private onRoom = false;
    private onEvent = false;

    private Nexts: NextCallType = {
        open: false,
        message: false,
        close: false,
        error: false
    };

    // メッセージ受信後の処理
    private next: { [key: string]: Function } = {
        on: () => {
            return null;
        },
        room: () => {
            return null;
        }
    };

    public constructor(port = 0, url = '') {
        this.port = port > 0 ? port : this.port;
        this.url = url !== '' ? url : this.url;
        try {
            this.socket = new WebSocket(this.url + ':' + this.port);
        } catch (error) {
            console.error(error);
        }
    }

    public static call(
        port = 3000,
        url = 'http://localhost'
    ): SocketClient2Service {
        if (!SocketClient2Service.instance) {
            SocketClient2Service.instance = new SocketClient2Service(port, url);
        }
        return SocketClient2Service.instance;
    }


    public setNext(
        next: (e: Event | MessageEvent) => void,
        target: 'open' | 'message' | 'close' | 'error'
    ): SocketClient2Service {
        this.Nexts[target] = next
        return this
    }

    /**
     * 待受開始
     */
    public listen(): SocketClient2Service {
        if (this.onEvent || this.socket === undefined) {
            return this;
        }

        this.socket.addEventListener('open', (e: Event) => {
            console.log('Open WebSocket', e);
            if (this.Nexts.open !== false) {
                this.Nexts.open(e);
            }
        });

        this.socket.addEventListener('message', (m: MessageEvent) => {
            const d = this.fromJson(m.data)
            DataTypeHelper.call().setFile(d.data).check()
            if (this.Nexts.message !== false) {
                this.Nexts.message(d)
            }
        })
        this.socket.addEventListener('close', (e: Event) => {
            console.log('Close WebSocket', e)
            if (this.Nexts.close !== false) {
                this.Nexts.close(e)
            }
        })
        this.socket.addEventListener('error', (e: Event) => {
            console.log('Error::')
            console.error(e)
            if (this.Nexts.error !== false) {
                this.Nexts.error(e)
            }
        })
        this.onEvent = true

        return this
    }

    /**
     * メッセージの送信
     * @param data wsData
     * @returns
     */
    public send(data: wsData): SocketClient2Service {
        if (this.socket === undefined) return this
        this.socket.send(this.toJson(data))
        return this
    }

    public joinRoom(room = ''): SocketClient2Service {
        if (this.onRoom) {
            return this;
        }
        const _room = room === '' ? this.rand() : room;
        console.log(`room: ${_room}`);
        this.send({
            job: 'join',
            room: _room,
            data: {}
        });
        this.onRoom = true;

        return this;
    }

    private rand() {
        const S =
            'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        const N = 16
        return Array.from(Array(N))
            .map(() => S[Math.floor(Math.random() * S.length)])
            .join('')
    }

    private toJson(data: object): string {
        return JSON.stringify(data)
    }

    private fromJson(data: string): any {
        return JSON.parse(data)
    }
}


DualSensの情報を投げる処理

Websocketのクライアントサービスを読み込み

  • 実際の接続を行う機能
  • WebHIDからの入力をサーバーに送信する

この2機能を持たせたファイルを作ります。

WebSocketサーバーと接続

作成済みのサービスから接続部分を順に呼ぶだけなので
非常に簡素です。

接続先は、RaspberryPiの設定に合わせて適時変えて下さい。
今回は

  • 接続先IP : 192.168.11.10
  • 接続先ポート : 8800

にて処理を書いています。

接続部分で1秒待ちが入っているのは
本来は接続イベントをキャッチした後に続く処理を明記しますが
書き換えが面倒であったため、1秒待てばほぼ100%接続が終わってるであろうと
1秒間のスリープを挟んでいます。

    // インスタンス作成
    ws = SocketClient.call(
            8800,                    // RaspberryPi側のWebSocketのポート
            'https://192.168.11.10', // RaspberryPi側のIP
        )
    // サーバーと通信開始
    ws.listen()
    await sleep(1000)
    // 部屋に入る
    ws.joinRoom()

ですので、本来はWebSocketのサービス側で
処理できていないとだめな部分ですが
今回はさして重要ではありませんのでスルーします。

WebSocketサーバーに投げる

サーバーと接続し、送信の準備が出来ると
WebHIDからの入力イベントを処理したコールバック関数から
ボタンのマッピングを行った変数を受け取り
そのまま投げる部分が要りますので

下記のように

JSON文字列に変換して投げるだけ

の処理を作ります。

    ws.send({
        job: 'other',
        data: JSON.stringify({
            task: 'ctrl',
            button: {
                up: state.buttons.dPadUp,
                down: state.buttons.dPadDown,
                left: state.buttons.dPadLeft,
                right: state.buttons.dPadRight,
                left_stick_x: state.axes.leftStickX,
                left_stick_y: state.axes.leftStickY,
                left_stick: state.buttons.leftStick,
                right_stick_x: state.axes.rightStickX,
                right_stick_y: state.axes.rightStickY,
                right_stick: state.buttons.rightStick,
                triangle: state.buttons.triangle,
                circle: state.buttons.circle,
                cross: state.buttons.cross,
                square: state.buttons.square,
                l1: state.buttons.l1,
                l2: state.buttons.l2,
                l3: state.buttons.l3,
                r1: state.buttons.r1,
                r2: state.buttons.r2,
                r3: state.buttons.r3,
                left_axes: convertRange(state.axes.l2),
                right_axes: convertRange(state.axes.r2),
                start: state.buttons.start,
                select: state.buttons.select,
            }
        })
    })

コントローラーのボタンOn/Offはそのまま送って問題無いのですが
アナログ部分、スピード調整を行うトリガーの入力は
0.0~1.0の範囲となり、モーターコントローラーの0~255と異なります。
ですので、アナログ入力の数値範囲を変換する関数「convertRange」を
トリガーの入力にだけ挟んでいます。

// DCモーターのパワーを、0~255の間に変換する
export const convertRange = (value: number): number => {
    if (value < 0 || value > 1) {
        throw new Error('Value must be between 0.0 and 1.0');
    }
    return Math.round(value * 254) + 1;
}

以下、サンプルの全文です。
このファイルを「socket.helper.ts」というファイル名で保存し、使います。

socket.helper.tsのコード
import { SocketClient } from './socket_client'
import { ControlType } from '@/src/definisions'
import { DualSense } from './webhid_ds'

let ws: SocketClient | null = null

// WebSocketサーバーに接続
export const socketConnect = async (): Promise<void> => {
    // インスタンス作成
    ws = SocketClient.call(
            8800,                    // RaspberryPi側のWebSocketのポート
            'https://192.168.11.10', // RaspberryPi側のIP
        )
    // サーバーと通信開始
    ws.listen()
    await sleep(1000)
    // 部屋に入る
    ws.joinRoom()
    
}

// WebHIDのサービスに渡す、コールバック関数
export const buttonSend = async (
    state: DualSense['state'],
    data: DataView
): Promise<void> => {
    if (!ws) {
        console.log('ws is not connected')
        return
    }

    ws.send({
        job: 'other',
        data: JSON.stringify({
            task: 'ctrl',
            button: {
                up: state.buttons.dPadUp,
                down: state.buttons.dPadDown,
                left: state.buttons.dPadLeft,
                right: state.buttons.dPadRight,
                left_stick_x: state.axes.leftStickX,
                left_stick_y: state.axes.leftStickY,
                left_stick: state.buttons.leftStick,
                right_stick_x: state.axes.rightStickX,
                right_stick_y: state.axes.rightStickY,
                right_stick: state.buttons.rightStick,
                triangle: state.buttons.triangle,
                circle: state.buttons.circle,
                cross: state.buttons.cross,
                square: state.buttons.square,
                l1: state.buttons.l1,
                l2: state.buttons.l2,
                l3: state.buttons.l3,
                r1: state.buttons.r1,
                r2: state.buttons.r2,
                r3: state.buttons.r3,
                left_axes: convertRange(state.axes.l2),
                right_axes: convertRange(state.axes.r2),
                start: state.buttons.start,
                select: state.buttons.select,
            }
        })
    })
}

// DCモーターのパワーを、0~255の間に変換する
export const convertRange = (value: number): number => {
    if (value < 0 || value > 1) {
        throw new Error('Value must be between 0.0 and 1.0');
    }
    return Math.round(value * 254) + 1;
}

// sleep
const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec))


DualSenseの情報をサーバーに投げる

  • WebSocketのクライアント
  • サーバーへの接続と、コントローラーの入力の送信
  • DualSenseと接続し、入力を受ける

前回と今回で、必要な機能が揃ったと思います。
Bluetooth接続されたDualSenseを、WebHID経由で操作情報を受け取り、WebSocket経由でサーバーに投げる
という一連の処理を続けて行います。

BluetoothとDualSenseの接続は
WebHIDの呼び出し時に、機器の選択が1クッション入ります。
この段階で繋いでも問題ありませんので、気がついたタイミングで繋いでおいてください。

機能は既に出来てますので
順に呼ぶだけで機能します。

今の段階では、サーバー側が無いのでエラーになります。

import { ReturnSuccess, ReturnError } from "@/src/definisions"
import { ButtonStatus, init } from "@/src/_lib/bluetooth/ds4.service"
import { socketConnect, buttonSend } from "@/src/domain/Control/helper/socket.helper"
import { DualSenseStateType } from "../../../_lib/bluetooth/_helper/webhid_ds"

const BS = ButtonStatus

export const controlDispatch = async (
): Promise<ReturnSuccess<string> | ReturnError> => {
    // WebSocket接続
    await socketConnect()
    // DualSense接続
    initDS4()
    // WebHID情報が更新されるたびにWebSocketで送信
    init(buttonSend)

    return {
        status: true,
        message: ''
    }
}

実装の際は、ブラウザのボタンイベント等に紐づけし
「controlDispatch」を呼び出してもらうと
クライアント側の処理が順に行われます。


WebSocketサーバー作成

次にサーバー側に入ります。

クライアント側と同じく、かなり前に作ったサービスをそのまま利用しています。
nodejsの問題でGPIOの制御側と同じJavascriptに変更しています

そして、先人達の知恵が大量に見つかりますので
WebSocketの実装部分の説明は割愛します。

このサービスは「socket-server.js」で保存しています。

コードの全文はこちらから

socket-server.jsの中身
const ws = require('ws')
const fs = require('fs')
const https = require('https')

let certs = {
    key: '',
    cert: '',
}

let server = null
let port   = 8800
//const app = express()
let _server = null

let jobStack = {
    cast    : [],
    other   : [],
}

    // メッセージ受信後の処理
const next = {
    cast    : () => {return null},
    other   : () => {return false},
}

/**
 * 証明書を設定
 * @param key string
 * @param cert string
 * @returns SocketServer2Service
 */
function setCerts(key, cert) {
    certs.key  = fs.readFileSync(key)
    certs.cert = fs.readFileSync(cert)
}

/**
 * WebSocketの待受ポート設定
 * @param port number 
 * @returns SocketServer2Service
 */
function setPort(_port) {
    _port = (typeof(_port) === 'number' ) ? _port : port
}

/**
 * メッセージ受信後の処理を設定
 * クライアントから送信された[message]を
 * そのまま受け取り処理する関数を任意で追加出来る
 * ※上書きではなく処理の追加なので注意
 * 
 * @param next Function 
 * @param target string Casts
 * @returns 
 */
function setNext(
    _next = (data) => {console.log(data)},
    target
) {
    try {
        _next('a')
        next[target] = _next
    } catch (error) {
        
    }
}

/**
 * 接続中の全クライアントに送信
 * @param data any(文字列 or 配列 or オブジェクト)
 * @returns ScoketServer2Service
 */
function sendAllClient(data) {
    server.clients.forEach((client) => {
        send(client, 'cast', data)
    })
}


function otherRequest(data) {
    next.other(data)
}

/**
 * websocketサーバーを起動
 * @returns 
 */
function start() {
    if (certs.key !== '' && certs.cert !== '') {
        _server = https.createServer({
            key: certs.key,
            cert: certs.cert,
        })
        server = new ws.Server({ server : _server })
        console.log(`
    [Start SSL Socket Server] Listen Port :: ${port}
        `);
    } else {
        server = new ws.Server({ port : port})
        console.log(`
    [Start Socket Server] Listen Port :: ${port}
        `);
    }

    // サーバーアクション設定
    action()

    if (_server !== null) {
        _server.listen(port)
    }
}

/**
 * 受信時の動作を登録
 * @returns 
 */
function action() {
    server.on('connection', (s) => {
        // IDを付与
        s.ipAddress = s._socket.remoteAddress.replace(/^.*:/g,'')
        s.port      = s._socket.remotePort
        s.unique    = buildId()
        s.id        = `${s.ipAddress}:${s.port}:${s.unique}`

        // 生成されたIDを返す
        s.send(JSON.stringify({
            job : 'connection',
            id  : s.id,
        }))

        // 待受開始
        s.on(
            'message',
            async (message) => {
                const m = fromJson(message)
                await lisnHub(m, s)
                executeJob()
            }
        )
    })
}

/**
 * job毎に処理を振り分け
 * @param m any 受信データ
 * @param s any 接続ClientのSocket
 * @returns Promise<SocketServer2Service>
 */
async function lisnHub(m, s) {

    if (m.job === 'cast') {
        const job = (next.cast() !== null)
                        ? next.cast(m) : {to: 'cast', data: m.data}
        addJob(job, 'cast')
    }

    if (m.job === 'other') {
        const job = {to: 'other', data: m.data}
        addJob(job, 'other')
    }

}

/**
 * Jobスタックにデータを追加
 * @param job {to: string, data: any} 送信先と送信データ
 * @param send Casts 送信方式、全配信、部屋配信、指定配信
 * @returns SocketServer2Service
 */
function addJob(
    job,
    send
) {
    jobStack[send].push(job);
}

/**
 * スタックされたジョブを実行
 * @returns SocketServer2Service
 */
function executeJob() {

    for (const key in jobStack) {
        if (Object.prototype.hasOwnProperty.call(jobStack, key)) {
            if (jobStack[key].length > 0) {
                jobStack[key].map((job) => {
                    if (key === 'cast') {
                        sendAllClient(job.data);
                    } else if (key === 'other') {
                        otherRequest(fromJson(job.data));
                    }
                })
                jobStack[key] = [];
            }
        }
    }
}

/**
 * クライアントに送信
 * @param client any WebSocketオブジェクト
 * @param job string 送信処理種別
 * @param data any 送信データの中身(JSON文字列)
 * @returns SocketServer2Service
 */
function send(client, job, data) {
    client.send(toJson({
        job     : job,
        data    : data
    }))
}

/**
 * ClientIDを生成
 * @returns string ランダム生成されたClinetID
 */
function buildId() {
    return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}

function toJson(data) {
    return JSON.stringify(data);
}

function fromJson(data) {
    return JSON.parse(data);
}

module.exports = {
    setCerts,
    setPort,
    setNext,
    sendAllClient,
    start,
    action,
    lisnHub,
    addJob,
    executeJob,
    send,
    buildId,
    toJson,
    fromJson
};


サーバー証明書

使用するサーバー証明書は
openssl等を使用し、適時作成するか、手元の証明書を活用ください。

openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr

オレオレ証明書の場合
不明なサーバー証明を許可する必要があるので
WebSocketサーバー起動後、該当のポートにブラウザでアクセスする必要があります。

接続クライアントが実質1台のみ
通信内容も定型で、それ以外を考える必要も無いので
かなり簡素に

データが来たら、GPIOの制御関数に渡す

のみ行います。

以下の内容を記述した、「test.js」を保存します。

const socket = require('./socket_server')
const control = require('./control.helper')

const keys = {
    key: './keys/key.pem',
    cert: './keys/cert.pem'
}

function setup() {
    socket.setCerts(keys.key, keys.cert)
    control.setup()
    socket.setNext(
        async (data) => {
            if (data.task === 'ctrl') {
                control.control(data.button)
            }
        },
        'other'
    )
    return this
}

const listen_start = () => {
    socket.start()
}

setup()
listen_start()

操作情報でGPIOを操作する

ボタン数は非常に多くありますが
とりあえず、ハンドルとアクセル(前進・後退)のみあればいいので

入力キー 操作
左キー ハンドルを左に
右キー ハンドルを右に
右トリガー 前進
左トリガー 後退

以上の入力を割り振ります。
後々、ブースト機能や、ライト点灯、ミスト噴射、何かしら射出等、付けても面白くなります。

モーターのパワー、ハンドル角の制御変数と合わせて
このようなオブジェクトにまとめています。

const MotorPorts = {
    MOROT1_IN: 19,
    MOROT1_OUT: 26,
    MORTOR_PWM: 12,
    MORTOR_POWER: 1,
    HANDLE: 17,
    INITIAL_ANGLE: 850,
    INITIAL: false
}

初期化処理は、GPIOのサンプルと同じで
DCモーターと、サーボの処理が一緒になっただけです。
複数回呼ばれるとよろしく無いので
実行済みフラグを入れています。

    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)

websocketサーバーのところで実際に呼ばれていた
入力情報を受け取る部分です。

見たまんまですが
押されたボタンに合わせて紐づけされたGPIOに出力を送ります。

const control = (button) => {
    
    ButtonMap = button

    if (button.right_axes > 1) {
        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) {
        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.left) {
        console.log(gpio.searvoWrite(MotorPorts.HANDLE, 1150))
    }
    if (button.right) {
        console.log(gpio.searvoWrite(MotorPorts.HANDLE, 550))
    }
}

この内容をまとめて、「control.helper.js」で保存しています。
コードの全文は以下

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)
}

const control = (button) => {
    
    ButtonMap = button

    if (button.right_axes > 1) {
        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) {
        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.left) {
        console.log(gpio.searvoWrite(MotorPorts.HANDLE, 1150))
    }
    if (button.right) {
        console.log(gpio.searvoWrite(MotorPorts.HANDLE, 550))
    }
}


module.exports = {
    setup,
    control
}


試験

完成した内容を、下記コマンドで実行し
待受状態にした後

sudo node test.js

ブラウザから接続処理を開始すると
コントローラーの入力に合わせて、各モーターが動きますが
問題が起こります。

DCモーターは動きっぱなしで
サーボモーターはセンター位置に戻りません

次回はこの問題を修正します。




UZAYA

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

0
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
0
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?