상세 컨텐츠

본문 제목

엔트리 하드웨어 개발

프로젝트/엔트리

by ryujt 2019. 11. 26. 21:47

본문

교재의 일부

2년에 걸쳐서 프로그래밍 교육용 하드웨어와 소프트웨어를 개발을 해왔습니다. 마지막에는 엔트리를 추가 개발해야 했는데, 약간 고생한 편이라서 생각날 때마다 팁을 정리해보려고 합니다.

 

개발환경 설치

https://entrylabs.github.io/docs/guide/ 문서를 따라서 진행하였습니다. 설치부터 조금 삐걱거리기 시작했는데요. 소스가 빌드되지 않아서 문서에 있는 Node.js와 npm의 버전을 문서와 같이 오래 전 것을 찾아서 설치해보아도 마찬가지였습니다. 문제의 이슈는 "npm install --global --production windows-build-tools"로 설치하는 C++ 툴이 제 PC에 설치되어 있는 Visual Studio와 충돌하는 것으로 보였습니다. 아무것도 설치되지 않은 클린 PC에서 개발환경을 설치하고 빌드하여 빌드된 소스 폴더를 통째로 다시 복사해와서 사용하였습니다. 한 번 빌드되면 엔트리 하드웨어에 추가되는 파일들은 변경사항이 바로 적용이되기 때문에 굳이 개발 PC에서 빌드하지 않고 저처럼 다른 PC에서 빌드하여 복사해와도 문제 없습니다.

 

하드웨어 블록 만들기

문서에 있는 하드웨어 블록 작성이 방법이 오래 된 것인지 적용에 다소 어려움을 겪었습니다. 그래서 저는 아두이노 소스를 그대로 복사하여 원하는 기능을 변경하면서 코드의 전반적인 흐름과 구조를 이해하는것부터 시작하였습니다. 제가 만들어야 하는 하드웨어는 파이썬을 사용하고 있어서 방식이 전혀 달랐지만, 일단 구조를 파악하는데에 중점을 두고 원래 하드웨어 개발에서는 처음부터 다시 시작하는 방법을 선택했습니다. (이부분도 다른 회사의 제품 코드를 일부 활용하기도 했습니다)

 

entry-hw 소스

  • requestInitialData(): 하드웨어 접속이 되면 최초에 한 번 실행됩니다.
  • handleRemoteData(): 엔트리에서부터 들어오는 메시지를 처리하는 함수입니다. 저희 제품의 경우에는 블록마다 파이썬 코드를 하드웨어에 전송하면, 하드웨어의 인터프리터가 이를 해석하고 실행하는 방식을 선택하였습니다.
  • requestLocalData(): 엔트리에서 들어온 메시지를 가공하여, 하드웨어에 시리얼 통신으로 전송하는 함수입니다. 버퍼(큐)에 있는 메시지들을 하나씩 읽어와서 처리하는 구조입니다.
  • handleLocalData(): 하드웨어에서 시리얼 통신으로 입력받은 메시지를 처리하는 함수입니다.

아래는 전체 소스코드입니다.

const _ = require('lodash');
const BaseModule = require('./baseModule');

class AsomeBot extends BaseModule {
    constructor() {
        super();

        this.isDraing = false;
        this.sendBuffer = [];

        this.receivedText = "";

        this.msg_id = '';
        this.sendToEntry = {
            msg_id: "",
            distance: 0,
            udp_msg: "",
        };
    }

    connect() {
    }

    socketReconnection() {
        this.socket.send(this.handler.encode());
    }

    requestInitialData() {
        console.log("requestInitialData");

        var init_str = "import asomebot; import hcsr04; asomebot.ready(5, 6, 7,8); hcsr04.open(3, 2)\r";
        return Buffer.from(init_str, "ascii");
    }

    setSerialPort(sp) {
        this.sp = sp;
    }

    setSocket(socket) {
        this.socket = socket;
    }

    checkInitialData(data, config) {
        return true;
    }

    validateLocalData(data) {
        return true;
    }

    requestRemoteData(handler) {
        var sendToEntry = this.sendToEntry;
        // console.log("to Entry: ", sendToEntry);

        for (var key in sendToEntry) {
            handler.write(key, sendToEntry[key]);
        }
        return;
    }

    handleRemoteData({ receiveHandler = {} }) {
        const { data: handlerData } = receiveHandler;
        if (_.isEmpty(handlerData)) {
            return;
        }

        if (handlerData.msg_id == undefined) {
            console.log("from handlerData.msg_id == undefined", handlerData);
            return;
        }

        if (handlerData.msg_id != this.msg_id) {
            console.log("from Entry: ", handlerData);

            this.msg_id = handlerData.msg_id;
            this.sendBuffer.push(Buffer.from(handlerData.msg + "\r", 'ascii'));
            this.sendBuffer.push(Buffer.from("'#I'" + "'D " + handlerData.msg_id + "'\r", 'ascii'));

            // 초음파 센서를 이동 블록등과 함께 사용할 때 신호가 처리 안되는 경우가 있다.
            // 반복 실행해도 상관없는 코드이기 때문에 모든 명령 수행시에 추가로 실행하여 최신 측정값을 갱신해 둔다.
            this.sendBuffer.push(Buffer.from("print('#D' + 'T ' + str(hcsr04.get_distance()) + '  ###')\r"));
        }
    }

    requestLocalData() {
        var self = this;

        if (!this.isDraing && this.sendBuffer.length > 0) {
            this.isDraing = true;
            var msg = this.sendBuffer.shift();
            console.log("to AsomeBot: ", msg.toString(), this.sendBuffer.length);
            this.sp.write(msg, function() {
                if (self.sp) {
                    self.sp.drain(function() {
                        self.isDraing = false;
                    });
                }
            });
        }

        return null;
    }

    handleLocalData(data) {
        // console.log("handleLocalData: ", this.receivedText);

        this.receivedText = this.receivedText + data.toString();

        var index = this.receivedText.indexOf('\r');
        while (index >= 0) {
            var line = this.receivedText.substring(0, index);
            this.receivedText = this.receivedText.substring(index + 1);

            console.log("from AsomeBot: ", line);

            if (line.indexOf('#DT') >= 0) {
                var values = line.split(" ");
                if (values.length > 1) this.sendToEntry.distance = values[1];
            }
            if (line.indexOf('#UDP') >= 0) {
                var values = line.split(" "); 
                if (values.length > 1) {
                    this.sendToEntry.udp_id = this.msg_id;
                    this.sendToEntry.udp_msg = values[1];
                }
            }
            if (line.indexOf('#ID') >= 0) this.sendToEntry.msg_id = line;

            index = this.receivedText.indexOf('\r');
        }
    }

    setSocketData({ socketData, data }) {
    }

    lostController() { }

    disconnect(connect) {
        connect.close();
        this.sp = null;
    }

    reset() {
        this.sp = null;
        this.isPlaying.set(0);
        this.sendBuffer = [];
        this.receivedText = "";
    }
}

module.exports = new AsomeBot();

 

entryjs 소스의 일부분과 설명

asomebot_toggle_led: {
    template: Lang.template.asomebot_toggle_led,
    color: EntryStatic.colorSet.block.default.HARDWARE,
    outerLine: EntryStatic.colorSet.block.darken.HARDWARE,
    skeleton: 'basic',
    statements: [],
    params: [
        // 블록 내에 입력창의 데이터 타입 (문자열)
        {
            type: 'Block',
            accept: 'string',
        },

        // 블록 맨 뒤에 아이콘이 달린 꼬리 표시
        {
            type: 'Indicator',
            img: 'block_icon/hardware_icon.svg',
            size: 12,
        },
    ],

    // 입력창에 입력된 데이터의 키값
    paramsKeyMap: {
        VALUE: 0,
    },
    events: {},
    def: {
        params: [
            // 콤보 박스 형태로 데이터를 직접 입력하지 않고 선택할 수 있는 방식
            // 디폴트는 'on'이며, "on", "off" 중에 선택할 수 있는 형태
            {
                type: 'arduino_get_digital_toggle',
                params: ['on'],
            },
            null
        ],
        type: 'asomebot_toggle_led',
    },
    class: 'Basic',
    isNotFor: ['AsomeBot'],
    func: function(sprite, script) {
        var sq = Entry.hw.sendQueue;
        var pd = Entry.hw.portData;

        // 입력창에서 입력받은 값을 가져온다.
        var value = script.getValue('VALUE');

        if (!script.is_started) {
            script.is_started = true;

            // 하드웨어에서 명령을 처리할 동안 기다리기 위해서 고유 id 발급
            script.msg_id = random_str(16);
            sq.msg_id = script.msg_id;

            // 실행해야할 파이썬 코드
            sq.msg = format_str("OutputPin(4).{0}()", value);

            // script가 리턴되면 블록 실행이후 다음으로 넘어가지 않고 기다리게 된다.
            return script;
        } 

        // 만약 리턴받은 id가 발급했던 id와 같으면 이 블록 실행을 마치고 다음 실행으로 넘어가도록 한다.action-button
        if ((pd.msg_id) && (pd.msg_id.indexOf(script.msg_id) >= 0)) {
            delete script.is_started;
            delete script.msg_id;
            return script.callReturn();
        }

        return script;
    },
    syntax: undefined,
},