1. Demo




2. Overview

This is not the first time I write about graphical programming for embedded devices. You can find out my previous posts:

Scratch Programming to Control Robot

Scratch Programming 102

However, this time, it is a different story. We don't just make some plug-ins for the existing platforms, we are going to modify the software and make it work our way. Of course, everything is open-source. Everyone should be able to do it. But, does it take time and effort to dig into their source code?

The answer is: "It may be easier than you think!". In this post, I want to show you the steps to include a new hardware platform and build your customized version of one existing graphical programming software. And then, everyone will be able to do it themselves.

Alright, let's start.


2.1. Entry, a Scratch-style programming platform:

Scratch-style programming platform, in short, is the software platform that provides a graphical way to program: instructions are represented as colorful blocks, instead of boring textual lines. These programs are designed to help everyone, especially children, to learn programming and creative thinking.

Ladies and gentlemen, I would like to introduce you Entry, a Scratch-style graphical programming platform.
From Entry homepage: Entry is an education platform created to help anyone learn to code. Students are able to learn to code while playing. Teachers are able to teach and manage students effectively.
This open-source software program comes from Korea, developed and maintained by Entry Labs. It is written in HTML5 and JavaScript.






Tho they provide Developer guide, there is no English manual, so it may be painful for non-Korean speakers to catch up (including me). Fortunately, Google Translate worked quite well, and their source code is not too complicated to follow, so I still managed to get it.




2.2. System architecture

The system architecture of Entry is quite similar to Scratch, except that here they provide an official Hardware Connector app for users.

There are two core modules in Entry: Entry-js and Entry-hw.

+ Entry-js is the editor work-space module. If you want to create new programming blocks or modify user interface, you will need to check this module.

+ Entry-hw, or Entry Hardware Connector module, is used to allow Entry editor interact with hardware devices.

By design concept, Entry editor can be loaded on web browsers, however, due to security reason, web browsers can not interact directly with serial COM port, so that's why a hardware connector app is needed. They already supported tons of hardware devices in Entry-hw, but if yours is not on the list, you can add and define the communication interface for your device.


3.Steps to include your hardware platform

3.1.
Define new platform in Hardware connector

Source code for Entry-hw is available here.

After download, to start Entry HW Connector program, you can browse to the downloaded folder, and run this command from the command line ( Don't forget to install Node.js beforehand ):
Code:
  
 npm install
and then launch the program:
Code:
 
 npm start



Basically, there are a lot of hardware platforms supported by Entry HW Connector app. If you want to include a new one, you need to browse to \app\modules\, and create three files:

+ Your_Device_Name.json (Define configuration)

+ Your_Device_Name.png (Icon file)

+ Your_Device_Name.js (Define interface functions with Entry editor and hardware device)

The use of these files is straightforward. You can take a look at other hardware platforms in \app\modules\ folder, e.g., Arduino, and see how they made it work.

Below is a sample phpoc.json file that I've created:

Code:
{
   "id": "240199",
   "name": {
       "en": "PHPoC Blue",
       "ko": "PHPoC 블루"
   },
   "platform": ["win32"],
   "icon": "phpoc_test.png",
   "module": "phpoc_test.js",
   "url": "www.phpoc.com",
   "select_com_port": true,
   "entry": {
       "protocol": "json"
   },
   "hardware": {
       "type": "serial",
       "control": "slave",
       "vendor": ["Sollae Systems"],
       "baudRate": 115200
   }
}
Note that in the json file above, you will need to define a unique hardware ID for your device. For testing, you can set it as anything different from the existing IDs.

Actually, this part is just for your information, because in my project I don't use Hardware connector app. Wonder why? Because with PHPoC device I am using, I can connect it with Entry editor via WebSocket (no security restriction tho), so there is no need of serial port connection neither Hardware connector app. But I already tried this step, so you can still leave me questions if you have any.

My customized version of Entry with PHPoC

3.2. Design programming blocks for your hardware platform:

Next is Entry-js, the editor work-space module.
Code:
 
 npm install
You can use this build command:
Code:
 
 npm run dist
It will compile source files into \dist\entry.js file.

You can use the command below to run a test server on localhost.
Code:
 
 npm run serve

Programming blocks
There is a lot of stuff in entry-js, but for adding new programming blocks for your hardware, you just need to follow the steps below:

- Browse to \src\playground\blocks\, create block_Your_Device_Name.js

You can refer to other block definition files in the same folder to create your own. Here you will define the shape, color, the data type, the functions to handle exchanging data with Hardware connector.

Code:
Entry.PHPoC = {
   name: 'phpoc',
   imageName: 'phpoc.png',
   title: {
       "ko": "PHPoC 보드",
       "en": "PHPoC Board"
   },
    deviceVersion: '99',
    deviceID: '24.1',

....            


Entry.PHPoC.setLanguage = function() {
   return {
       ...
       en: {
           template: {
           phpoc_connect: 'Connected to Websocket %1',
               phpoc_get_analog_value: 'Value of Analog port %1',
               phpoc_value_map: 'Map %1 from %2 ~ %3 to %4 ~ %5', 

               ...                
           }
       },
   };  
};
- In the same folder, open index.js, then include your newly created block_Your_Device_Name.js file, and also remember to add your hardware device to Hardware list here.

For example, in Your_Device_Name.json, you have defined your hardware ID as below:

Code:
 
 "id": "2C0103",
In block_Your_Device_Name.js, you already defined your class as below:

Code:
Entry.Your_Device = {
    name: 'your_device',
}
Then, in index,js, you need to include the file above:

Code:
 
  require('./block_Your_Device_Name');
and include your device object:

Code:
Entry.HARDWARE_LIST = {
   '1.1': Entry.Arduino,
...
   '2C.1': Entry.Your_Device,
}
Just remember, only first 4 digits in the defined hardware ID are used here.

Following the steps above, basically you can add new hardware and define new programming blocks.


4. Build your customized graphical programming software:


Entry offline is a desktop app (based on Electron framework) that includes both editor and hardware connector module. You can download source code from here.

In entry-offline folder, the hardware connector module is available at:

\src\renderder\bower_components\entry-hw\app\modules\

And the editor work-space module, is available at:

\src\renderder\bower_components\entryjs\dist\entry .min.js

Note that here, this module has been compiled and minified. You can refer to 3.2 to compile entry.js file from entry-js module.

You also need to add the new object types, which you declared in block_Your_Device_Name.js, to \src\renderer\src\static.js. Probably you will know what to do after taking a look at the file and referring to other included objects, it is quite straightforward. You can refer to the below:

Code:
EntryStatic.getAllBlocks = function() {
    ...

       {
           category: 'arduino',
           blocks: [
               'arduino_download_connector',
                ...

         'phpoc_connect',
        'phpoc_get_digital',
            'phpoc_get_analog_value',
        'phpoc_value_map',        
        'phpoc_get_ultrasonic_value',
        'phpoc_set_digital',
        'phpoc_set_led',
        'phpoc_set_servo',                        
        'phpoc_disconnect',    

      }
}
Finally, after finish your editing, you may run some commands for packaging and building the customized software. Let's refer to package.json file to select the appropriate commands. For example, I use the following command to make installation file for Windows 64-bit:

Code:
 
 npm run make:win
You can download my customized version of Entry from here.

Don't forget to upload PHPoC script to your board before running this program.

Cheers.



Source code

block_phpoc.js: Declare hardware blocks and the functions to interface with PHPoC

Code:
/*

- Created by Homer(https://www.hackster.io/Homer)

*/

'use strict';

Entry.PHPoC = {
    name: 'phpoc',
    imageName: 'phpoc.png',
    title: {
        "ko": "PHPoC 보드",
        "en": "PHPoC Board"
    },
    deviceVersion: '99',
    deviceID: '24.1',
    isConnected: false,
    setZero: function() {
        let that = Entry.PHPoC;
        that.recentCommands = {};
        Entry.hw.update();
        let rstCmd = that.genCommand(that.commands.RST);
        that.sendWSdata(rstCmd);
    },
    commands: {
        POLL: 0,        
        RST: 1,
        GET: 2,
        SET: 3
    },
    sensorTypes: {
        DIGITAL: 1,
        ANALOG: 2,
        SERVO_PIN: 3,
        ULTRASONIC: 4,
    },
    inputData: {
        DIGITAL: {
            '8': 1,
            '9': 1,
            '12': 1,
            '13': 1,
            '14': 1,
            '15': 1,
            '16': 1,
            '17': 1,
            '18': 1,
            '19': 1,
            '20': 1,
            '21': 1,
        },
        ANALOG: {
            '0': 0,
            '1': 0,
            '2': 0,
            '3': 0,
            '4': 0,
            '5': 0,
            '6': 0,
        },
        ULTRASONIC: 0,
    },
    readablePorts: [],
    recentCommands: {},

    toogleBlockView: function(isShown){
        let that = Entry.PHPoC;
        let hw = Entry.hw;
        if (isShown){
            hw.selectedDevice = that.deviceID;
            hw.hwModule = hw.hwInfo[Entry.PHPoC.deviceID];
            that.showBlocks();
        }
        else {
            Entry.propertyPanel && Entry.propertyPanel.removeMode("hw");
            hw.selectedDevice = undefined;
            hw.hwModule = undefined;
            Entry.dispatchEvent("hwChanged");
        }        
    },
    showBlocks: function () {
        var self = Entry.playground;

        var WS = self.mainWorkspace;
        if (!WS) return;
        var blockMenu = WS.blockMenu;
        if (!blockMenu) return;

        var hw = Entry.hw;
        if (hw) {
            hw.banHW();
            if (hw.hwModule) {
                blockMenu.unbanClass(hw.hwModule.name);
            }
        } else {
            blockMenu.banClass("arduinoConnected", true);
            blockMenu.banClass("arduinoConnect", true);
            blockMenu.unbanClass("arduinoDisconnected", true);
            Entry.hw.banHW();
        }

        blockMenu.hwCodeOutdated = true;
        blockMenu._generateHwCode(true);
        blockMenu.reDraw();
    },

    webSocketClient: null,

    initWS: function(url){
        let that = Entry.PHPoC;
        if(!that.webSocketClient){
            that.webSocketClient = new WebSocket('ws://'+url+'/entry', 'text.phpoc');
            console.log(that.webSocketClient);
            that.webSocketClient.onerror = that.ws_onerror;
            that.webSocketClient.onopen = that.ws_onopen;
            that.webSocketClient.onclose = that.ws_onclose;
            that.webSocketClient.onmessage = that.ws_onmessage;
        }
    },

    closeWS: function(){
        let that = Entry.PHPoC;
        let hw = Entry.hw;    
        that.webSocketClient = null;
        that.clearPolling();
        that.recentCommands = {};
        Entry.propertyPanel && Entry.propertyPanel.removeMode("hw");
        hw.selectedDevice = undefined;
        hw.hwModule = undefined;
        Entry.dispatchEvent("hwChanged");
        if(that.isConnected){
            Entry.toast.warning(Lang.Msgs.warn, 'PHPoC WebSocket disconnected');
            that.isConnected = false;
        }
    },

    ws_onopen: function() {
        let that = Entry.PHPoC;
        let hw = Entry.hw;
        console.log('WebSocket Client Connected');
        that.isConnected = true;
        hw.connected = true;
        that.toogleBlockView(true);
        Entry.dispatchEvent("hwChanged");
        let descMsg = Lang.Msgs.hw_connection_success_desc2;
        Entry.toast.success(Lang.Msgs.hw_connection_succes  s, descMsg);

        if (Entry.playground && Entry.playground.object) {
            Entry.playground.setMenu(Entry.playground.object.o  bjectType);
        }
        Entry.PHPoC.sendPolling();                
    },
    sendWSdata: function(dt){
        let that = Entry.PHPoC;
        if (that.isConnected){
            that.webSocketClient.send(dt);
            console.log(dt);
        }            
    },
    ws_onerror: function(){
        console.log('WebSocket Connection Error');
        Entry.PHPoC.closeWS();
    },    
    ws_onclose: function() {
        console.log('WebSocket Client Closed');
        let that = Entry.PHPoC;
        Entry.PHPoC.closeWS();
    },

    ws_onmessage: function(msg) {
        let that = Entry.PHPoC;
        if (typeof msg.data === 'string') {
            let recvData = msg.data.split("\n");
            recvData.forEach(function(dt){
            let fields = dt.split("/");
            let c = parseInt(fields[0]);
            let t = parseInt(fields[1]);
            let p = fields[2];
            let v = parseInt(fields[3]);
            if (c == that.commands.GET){
                switch(t){
                    case (that.sensorTypes.ANALOG):
                        that.inputData.ANALOG[p] = v;
                        break;
                    case (that.sensorTypes.DIGITAL):
                        Entry.PHPoC.inputData.DIGITAL[p] = v;
                        break;
                    case (that.sensorTypes.ULTRASONIC):
                        that.inputData.ULTRASONIC = parseFloat(p).toFixed(2);
                        break;
                    }
            }
            });    
        }
    },

    pollingFunc: null,
    sendPolling: function(){    
        if(!Entry.PHPoC.pollingFunc){
            let pollCmd = Entry.PHPoC.genCommand(Entry.PHPoC.commands.POLL);
            Entry.PHPoC.pollingFunc = setInterval(function(){
                Entry.PHPoC.sendWSdata(pollCmd);
            }, 500);
        }
    },
    clearPolling: function(){
        clearInterval(this.pollingFunc);
        Entry.PHPoC.pollingFunc = null;
    },


    sendCommand: function(obj){
        let that = Entry.PHPoC;
        if (obj){                        
            if (obj.cmd === that.commands.GET){
                if (!that.recentCommands['GET']){
                    that.recentCommands['GET'] = {};
                }    
                if (
                    typeof obj.port === 'string' ||
                    typeof obj.port === 'number'
                ) {
                    if ((obj.type !== that.recentCommands.GET.type)||
                        (obj.port !== that.recentCommands.GET.port)) {    
                            that.recentCommands.GET = obj;                        
                            that.sendWSdata(that.genCommand(obj.cmd, obj.type, obj.port));
                        }
                }
                else if (Array.isArray(obj.port)) {

                    if ((obj.type !== that.recentCommands.GET.type)||
                        (obj.port[0] !== that.recentCommands.GET.port[0])||
                        (obj.port[1] !== that.recentCommands.GET.port[1])) {    
                            that.recentCommands.GET = obj;                        
                            that.sendWSdata(that.genCommand(obj.cmd, obj.type, obj.port[0], obj.port[1]));
                    }
                }
            }
            else if (obj.cmd === that.commands.SET) {
                if (!that.recentCommands['SET']){
                    that.recentCommands['SET'] = {};
                }
                if (
                    typeof obj.port === 'string' ||
                    typeof obj.port === 'number'
                ) {
                    if ((obj.type !== that.recentCommands.SET.type)||
                        (obj.data !== that.recentCommands.SET.data)||
                        (obj.port !== that.recentCommands.SET.port)) {    
                            that.recentCommands.SET = obj;                        
                            that.sendWSdata(that.genCommand(obj.cmd, obj.type, obj.port, obj.data));
                        }
                }
                else if (Array.isArray(obj.port)) {
                    if ((obj.type !== that.recentCommands.SET.type)||
                        (obj.port[0] !== that.recentCommands.SET.port[0])||
                        (obj.port[1] !== that.recentCommands.SET.port[1])) {    
                            that.recentCommands.SET = obj;                        
                            that.sendWSdata(that.genCommand(obj.cmd, obj.type, obj.port[0], obj.port[1]));
                    }
                }                    
            }                                
        }
    },

    genCommand: function(cmd, type, port, value){
        let str = cmd;
        if (type!=null)
            str += '/' + type;
        if (port!=null)
            str += '/' + port;
        if (value!=null)
            str += '/' + value;
        return (str + '\n');
    },


};

Entry.PHPoC.setLanguage = function() {
    return {
        ko: {
            template: {
                phpoc_connect: 'WebSocket에 연결됨 %1',
                phpoc_get_analog_value: '아날로그 %1 번 센서값',
                phpoc_value_map: '%1 의 범위를 %2 ~ %3 에서 %4 ~ %5 로 바꾼값',
                phpoc_get_ultrasonic_value:'울트라소닉 Trig %1 Echo %2 센서값',
                phpoc_get_digital: '디지털 %1 번 센서값',

                phpoc_set_digital: '디지털 %1 번 %2 %3',
                phpoc_set_servo: '핀 %1 번 핀의 서보모터를 %2 의 각도로 정하기 %3',                
                phpoc_set_led: 'LED %1 번 %2 %3',
                phpoc_disconnect: 'Stop WebSocket connection %1',
            }
        },
        en: {
            template: {
                phpoc_connect: 'Connected to Websocket %1',
                phpoc_get_analog_value: 'Value of Analog port %1',
                phpoc_value_map: 'Map %1 from %2 ~ %3 to %4 ~ %5',
                phpoc_get_ultrasonic_value: 'Read ultrasonic sensor Trig pin %1 Echo pin %2',               
                phpoc_get_digital: 'Input value of Digital port %1',

                phpoc_set_digital: 'Set digital port %1 output as %2 %3',
                phpoc_set_servo: 'Set servo pin %1 angle as %2 %3',
                phpoc_set_led: 'Set onboard LED %1 output %2 %3',
                phpoc_disconnect: 'Stop WebSocket connection %1',                
            }
        },
    };  
};


Entry.PHPoC.getBlocks = function() {
    return {
        //region PHPoC

        phpoc_show_block: {
            skeleton: 'basic_button',
            color: '#eee',
            isNotFor: ['arduinoDisconnected','arduinoConnect', 'arduinoConnected'],
            params: [{
                type: 'Text',
                text: Lang.Blocks.PHPOC_toggle,//'Toggle PHPoC Blocks',//Lang.Blocks.ARDUINO_reconnect,
                color: '#333',
                align: 'center'
            }],
            events: {
                mousedown: [function () {
                    //Entry.hw.retryConnect();
                    if (!Entry.hw.selectedDevice){
                        Entry.PHPoC.toogleBlockView(true);
                    }
                    else if (Entry.hw.selectedDevice===Entry.PHPoC.deviceID){
                        Entry.PHPoC.toogleBlockView(false);
                    }
                }]
            }
        },            
        phpoc_analog_list: {
            color: '#00979D',
            skeleton: 'basic_string_field',
            statements: [],
            template: '%1',
            params: [
                {
                    type: 'Dropdown',
                    options: [
                        ['A0', '0'],
                        ['A1', '1'],
                        ['A2', '2'],
                        ['A3', '3'],
                        ['A4', '4'],
                        ['A5', '5'],
                    ],
                    value: '0',
                    fontSize: 11,
                },
            ],
            events: {},
            def: {
                params: [null],
            },
            paramsKeyMap: {
                PORT: 0,
            },
            func: function(sprite, script) {
                return script.getField('PORT');
            },
            syntax: { js: [], py: [] },
        },

        phpoc_digital_list: {
            color: '#00979D',
            skeleton: 'basic_string_field',
            statements: [],
            template: '%1',
            params: [
                {
                    type: 'Dropdown',
                    options: [
                        ['8', '8'],
                        ['9', '9'],
                        ['12', '12'],
                        ['13', '13'],
                        ['14', '14'],
                        ['15', '15'],
                        ['16', '16'],
                        ['17', '17'],
                        ['18', '18'],
                        ['19', '19'],
                        ['20', '20'],
                        ['21', '21'],                        
                    ],
                    value: '0',
                    fontSize: 11,
                },
            ],
            events: {},
            def: {
                params: [null],
            },
            paramsKeyMap: {
                PORT: 0,
            },
            func: function(sprite, script) {
                return script.getField('PORT');
            },
            syntax: { js: [], py: [] },
        },        
        phpoc_ht_list: {
            color: '#00979D',
            skeleton: 'basic_string_field',
            statements: [],
            template: '%1',
            params: [
                {
                    type: 'Dropdown',
                    options: [
                        ['HT0', 'HT0'],
                        ['HT1', 'HT1'],
                        ['HT2', 'HT2'],
                        ['HT3', 'HT3'],
                    ],
                    value: 'HT0',
                    fontSize: 11,
                },
            ],
            events: {},
            def: {
                params: [null],
            },
            paramsKeyMap: {
                PORT: 0,
            },
            func: function(sprite, script) {
                return script.getField('PORT');
            },
            syntax: { js: [], py: [] },
        },
        phpoc_get_digital: {
            color: '#00979D',
            fontColor: '#fff',
            skeleton: 'basic_boolean_field',
            params: [
                {
                    type: 'Block',
                    accept: 'string',
                },
            ],
            events: {},
            def: {
                params: [
                    {
                        type: 'phpoc_digital_list',
                        params: ['8'],
                    },
                ],
                type: 'phpoc_get_digital',
            },
            paramsKeyMap: {
                PORT: 0,
            },
            class: 'PHPoCGet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {
                let port = script.getNumberValue('PORT', script);
                if (Entry.PHPoC.readablePorts.indexOf(port) == -1){                    
                    let obj_get = {
                        type: Entry.PHPoC.sensorTypes.DIGITAL,
                        cmd: Entry.PHPoC.commands.GET,
                        port: port
                    };                    
                    Entry.PHPoC.sendCommand(obj_get);
                    Entry.PHPoC.readablePorts.push(port);
                }
                let DIGITAL = Entry.PHPoC.inputData.DIGITAL;
                return DIGITAL ? DIGITAL[port] || 0 : 0;
            },
            syntax: { js: [], py: [] },
        },    
        phpoc_get_analog_value: {
            color: '#00979D',
            fontColor: '#fff',
            skeleton: 'basic_string_field',
            statements: [],
            params: [
                {
                    type: 'Block',
                    accept: 'string',
                },
            ],
            events: {},
            def: {
                params: [
                    {
                        type: 'phpoc_analog_list',
                    },
                ],
                type: 'phpoc_get_analog_value',
            },
            paramsKeyMap: {
                PORT: 0,
            },
            class: 'PHPoCGet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {
                let port = script.getValue('PORT', script);
                let ANALOG = Entry.PHPoC.inputData.ANALOG;
                return ANALOG ? ANALOG[port] || 0 : 0;
            },
            syntax: { js: [], py: [] },
        },
        phpoc_value_map: {
            color: '#00979D',
            fontColor: '#fff',
            skeleton: 'basic_string_field',
            statements: [],
            params: [
                {
                    type: 'Block',
                    accept: 'string',
                },
                {
                    type: 'Block',
                    accept: 'string',
                },
                {
                    type: 'Block',
                    accept: 'string',
                },
                {
                    type: 'Block',
                    accept: 'string',
                },
                {
                    type: 'Block',
                    accept: 'string',
                },
            ],
            events: {},
            def: {
                params: [
                    {
                        type: 'number',
                        params: ['20'],
                    },
                    {
                        type: 'number',
                        params: ['0'],
                    },
                    {
                        type: 'number',
                        params: ['4095'],
                    },
                    {
                        type: 'number',
                        params: ['0'],
                    },
                    {
                        type: 'number',
                        params: ['100'],
                    },
                ],
                type: 'phpoc_value_map',
            },
            paramsKeyMap: {
                PORT: 0,
                VALUE2: 1,
                VALUE3: 2,
                VALUE4: 3,
                VALUE5: 4,
            },
            class: 'PHPoCGet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {
                let result = script.getValue('PORT', script);
                let value2 = script.getNumberValue('VALUE2', script);
                let value3 = script.getNumberValue('VALUE3', script);
                let value4 = script.getNumberValue('VALUE4', script);
                let value5 = script.getNumberValue('VALUE5', script);
                let stringValue4 = script.getValue('VALUE4', script);
                let stringValue5 = script.getValue('VALUE5', script);
                let isFloat = false;

                if (
                    (Entry.Utils.isNumber(stringValue4) &&
                        stringValue4.indexOf('.') > -1) ||
                    (Entry.Utils.isNumber(stringValue5) &&
                        stringValue5.indexOf('.') > -1)
                ) {
                    isFloat = true;
                }

                if (value2 > value3) {
                    let swap = value2;
                    value2 = value3;
                    value3 = swap;
                }
                if (value4 > value5) {
                    let swap = value4;
                    value4 = value5;
                    value5 = swap;
                }
                result -= value2;
                result = result * ((value5 - value4) / (value3 - value2));
                result += value4;
                result = Math.min(value5, result);
                result = Math.max(value4, result);

                if (isFloat) {
                    result = Math.round(result * 100) / 100;
                } else {
                    result = Math.round(result);
                }

                return result;
            },
            syntax: { js: [], py: [] },
        },
        phpoc_get_ultrasonic_value: {
            color: '#00979D',
            fontColor: '#fff',
            skeleton: 'basic_string_field',
            statements: [],
            params: [
                {
                    type: 'Block',
                    accept: 'string',
                },
                {
                    type: 'Block',
                    accept: 'string',
                },
            ],
            events: {},
            def: {
                params: [
                    {
                        type: 'phpoc_ht_list',
                        params: ['HT2'],
                    },
                    {
                        type: 'phpoc_ht_list',
                        params: ['HT3'],
                    },
                ],
                type: 'phpoc_get_ultrasonic_value',
            },
            paramsKeyMap: {
                PORT1: 0,
                PORT2: 1,
            },
            class: 'PHPoCGet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {

                let port1 = script.getValue('PORT1', script);
                let port2 = script.getValue('PORT2', script);



                let obj_set = {
                    type: Entry.PHPoC.sensorTypes.ULTRASONIC,
                    cmd: Entry.PHPoC.commands.SET,
                    port: [port1, port2],
                };

                Entry.PHPoC.sendCommand(obj_set);

                let obj_get = {
                    type: Entry.PHPoC.sensorTypes.ULTRASONIC,
                    cmd: Entry.PHPoC.commands.GET,
                    port: [port1, port2],
                };
                Entry.PHPoC.sendCommand(obj_get);                                    
                return Entry.PHPoC.inputData.ULTRASONIC || 0;
            },
            syntax: { js: [], py: [] },
        },
        phpoc_set_digital: {
            color: '#00979D',
            skeleton: 'basic',
            statements: [],
            params: [
                {
                    type: 'Block',
                    accept: 'string',
                },
                {

                    type: 'Dropdown',
                    options: [
                        ['HIGH', '1'],
                        ['LOW', '0'],
                    ],
                    value: '1',
                },
                {
                    type: 'Indicator',
                    img: 'block_icon/hardware_03.png',
                    size: 12,
                },
            ],
            events: {},
            def: {
                params: [
                    {
                        type: 'phpoc_digital_list',
                        params: ['9'],
                    },
                    null,
                    null,

                ],
                type: 'phpoc_set_digital',
            },
            paramsKeyMap: {
                PORT: 0,
                VALUE: 1,
            },
            class: 'PHPoCSet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {                
                let port = script.getNumberValue('PORT', script);
                let value = script.getField('VALUE');
                let index = Entry.PHPoC.readablePorts.indexOf(port);
                if (index > -1){    
                    Entry.PHPoC.readablePorts.splice(index, 1);
                }
                let obj_set = {
                    type: Entry.PHPoC.sensorTypes.DIGITAL,
                    data: value,
                    cmd: Entry.PHPoC.commands.SET,
                    port: port
                };
                Entry.PHPoC.sendCommand(obj_set);
                return script.callReturn();
            },
            syntax: { js: [], py: [] },
        },
        phpoc_set_led: {
            color: '#00979D',
            skeleton: 'basic',
            statements: [],
            params: [
                {
                    type: 'Dropdown',
                    options: [
                        ['1', '30'],
                        ['2', '31'],
                    ],
                    value: '30',
                },
                {

                    type: 'Dropdown',
                    options: [
                        ['OFF', '1'],
                        ['ON', '0'],
                    ],
                    value: '1',
                },
                {
                    type: 'Indicator',
                    img: 'block_icon/hardware_03.png',
                    size: 12,
                },
            ],
            events: {},
            def: {
                params: [
                    null,
                    null,
                    null,
                ],
                type: 'phpoc_set_led',
            },
            paramsKeyMap: {
                PORT: 0,
                VALUE: 1,
            },
            class: 'PHPoCSet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {
                let port = script.getField('PORT');
                let value = script.getField('VALUE');                

                let obj_set = {
                    type: Entry.PHPoC.sensorTypes.DIGITAL,
                    data: value,
                    cmd: Entry.PHPoC.commands.SET,
                    port: port
                };
                Entry.PHPoC.sendCommand(obj_set);

                return script.callReturn();
            },
            syntax: { js: [], py: [] },
        },
        phpoc_set_servo: {
            color: '#00979D',
            skeleton: 'basic',
            statements: [],
            params: [
                {
                    type: 'Block',
                    accept: 'string',
                },
                {
                    type: 'Block',
                    accept: 'string',
                },
                {
                    type: 'Indicator',
                    img: 'block_icon/hardware_03.png',
                    size: 12,
                },
            ],
            events: {},
            def: {
                params: [
                    {
                        type: 'phpoc_ht_list',
                    },
                    {
                        type: "number",
                        params: ['45']
                    },
                    null,
                ],
                type: 'phpoc_set_servo',
            },
            paramsKeyMap: {
                PORT: 0,
                VALUE: 1,
            },
            class: 'PHPoCSet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {
                let port = script.getValue('PORT', script);
                let value = script.getNumberValue('VALUE', script);
                value = Math.min(180, value);
                value = Math.max(0, value);


                let obt_set = {
                    type: Entry.PHPoC.sensorTypes.SERVO_PIN,
                    data: value,
                    cmd: Entry.PHPoC.commands.SET,
                    port: port,
                };
                Entry.PHPoC.sendCommand(obt_set);


                return script.callReturn();
            },
            syntax: { js: [], py: [] },
        },    
        phpoc_connect: {
            color: '#00979D',
            fontColor: '#fff',
            skeleton: 'basic_boolean_field',
            statements: [],
            params: [
                {
                    type: 'Block',
                    accept: 'string',
                    value: '0.0.0.0',
                },
            ],
            events: {},
            def: {
                params: [
                    null,
                    //null,
                ],
                type: 'phpoc_connect',
            },
            paramsKeyMap: {
                ADDR: 0,
            },
            class: 'PHPoCGet',
            isNotFor: ['phpoc'],
            func: function(sprite, script) {            
                let url = script.getStringValue('ADDR', script);
                Entry.PHPoC.initWS(url);        
                return Entry.PHPoC.isConnected;//script.callReturn();
            },
            syntax: { js: [], py: [] },
        },
        phpoc_disconnect: {
            color: '#00979D',
            class: 'PHPoCSet',
            skeleton: 'basic',
            statements: [],
            params: [
                {
                    type: 'Indicator',
                    img: 'block_icon/hardware_03.png',
                    size: 12,
                },
            ],
            events: {},
            def: {
                params: [
                    null,
                ],
                type: 'phpoc_disconnect',
            },
            paramsKeyMap: {
                ADDR: 0,
            },
            isNotFor: ['phpoc'],
            func: function(sprite, script) {
                if (Entry.PHPoC.webSocketClient)
                    Entry.PHPoC.webSocketClient.close();

                return script.callReturn();
            },
            syntax: { js: [], py: [] },
        },        
        //endregion PHPoC
    };
}

task0.php: This file needs to be loaded and run on PHPoC device.

PHP Code:
<?php
if(_SERVER("REQUEST_METHOD"))
    exit; 
// avoid php execution via http request

include_once "/lib/sd_340.php";
include_once 
"/lib/sn_tcp_ws.php";

define("WS_ID"1);
define("WS_PATH""entry");

define("POLL""0");
define("RST""1");
define("GET""2");
define("SET""3");

// Define peripheral types
define("DIGITAL""1");
define("ANALOG""2");
define("SERVO""3");
define("ULTRASONIC""4");

define("PWM_PERIOD"20000); 
define("WIDTH_MIN"771);
define("WIDTH_MAX"2193);

$uio_list = array(8912131415161718192021);

function 
init_device(){
    global 
$uio_list;
    
ht_pwm_setup(00PWM_PERIOD"us");
    
ht_pwm_setup(10PWM_PERIOD"us");
    
ht_pwm_setup(20PWM_PERIOD"us");
    
ht_pwm_setup(30PWM_PERIOD"us");

    for(
$i=0$i<12$i++){
        
uio_setup(0$uio_list[$i], "in_pu"); 
    }
    
uio_setup(030"out high");
    
uio_setup(031"out high");
}

function 
set_ultrasonic_trig($pin){
    
ht_ioctl($pin"reset");
    
ht_ioctl($pin"set div us");
    
ht_ioctl($pin"set mode output pulse");
    
ht_ioctl($pin"set repc 1");
    
ht_ioctl($pin"set count 5 10"); 
}

function 
set_ultrasonic_echo($pin){
    
ht_ioctl($pin"reset");
    
ht_ioctl($pin"set div us");
    
ht_ioctl($pin"set mode capture toggle");
    
ht_ioctl($pin"set trigger from pin rise");
    
ht_ioctl($pin"set repc 4");
}

function 
get_ht_pin($pin){
    switch(
$pin){
        case 
"HT0":
            return 
0;
        case 
"HT1":
            return 
1;
        case 
"HT2":
            return 
2;
        case 
"HT3":
            return 
3;
        default:
            return -
1;
    }
}

function 
get_ultrasonic_value($echo_pin$trig_pin){
    
ht_ioctl($echo_pin"start"); 
    
ht_ioctl($trig_pin"start"); 

    
usleep(100000); 
    
ht_ioctl($trig_pin"stop");

    
$us ht_ioctl($echo_pin"get count 1");
    
$dist $us 340.0 2;
    
$dist $dist 10000;
    return 
$dist;
}

function 
servo_set_angle($id$angle
{             
    
$width WIDTH_MIN + (int)round((WIDTH_MAX WIDTH_MIN) * $angle 180.0);
    if((
$width >= WIDTH_MIN) && ($width <= WIDTH_MAX))
        
ht_pwm_width($id$widthPWM_PERIOD);
}

init_device();
$use_ultrasonic FALSE;
ws_setup(WS_IDWS_PATH"text.phpoc");

$rwbuf "";
while(
1)
{
    if(
ws_state(WS_ID) == TCP_CONNECTED)
    {
        
$rlen ws_read(WS_ID$rwbuf);
        if(
$rlen)
        {
            
$cmd_list explode("\n",$rwbuf);
            
$len count($cmd_list)-1;
            
$time_out=0;
            for (
$idx 0$idx<$len$idx++){
                
$ret "";        
                echo 
$cmd_list[$idx], "\r\n";
                
$cmd explode("/",$cmd_list[$idx]);
                switch(
$cmd[0]){
                    case 
SET:
                        switch(
$cmd[1]){
                            case 
DIGITAL:
                                
$pin = (int)$cmd[2];
                                
uio_setup(0$pin"out high");
                                if (
$cmd[3] == "0")
                                    
uio_out(0$pinLOW);            
                                else
                                    
uio_out(0$pinHIGH);
                                break;
                            case 
SERVO:
                                
$pin get_ht_pin($cmd[2]);
                                if (
$pin>-1){
                                    
servo_set_angle($pin, (int)$cmd[3]);
                                }
                                break;
                            case 
ULTRASONIC:
                                
$trig_pin get_ht_pin($cmd[2]);
                                
$echo_pin get_ht_pin($cmd[3]);
                                
set_ultrasonic_trig($trig_pin);
                                
set_ultrasonic_echo($echo_pin);
                                break;
                        }
                        break;
                    case 
GET:
                        switch(
$cmd[1]){
                            case 
DIGITAL:
                                
uio_setup(0, (int)$cmd[2], "in_pu"); 
                                break;
                            case 
ULTRASONIC:
                                
$trig_pin get_ht_pin($cmd[2]);
                                
$echo_pin =  get_ht_pin($cmd[3]);
                                if ((
$trig_pin>-1)&&($echo_pin>-1)){
                                    
$use_ultrasonic TRUE;
                                }
                                break;
                            }
                        break;
                    case 
RST:
                        
init_device();                     
                        
$use_ultrasonic FALSE;
                        break;
                    case 
POLL:                            
                        for(
$i=0$i<12;$i++){
                            
$pin $uio_list[$i];
                            
$d_in uio_in(0$pin);
                            
$ret $ret.GET."/".DIGITAL."/".(string)$pin."/".(string)$d_in."\n";
                        }
                        if (
$use_ultrasonic){
                            
$dist get_ultrasonic_value($echo_pin$trig_pin);
                            
$ret $ret.GET."/".ULTRASONIC."/".(string)$dist."\n";    
                        }
                        for(
$pin=0$pin<=5;$pin++){
                            
adc_setup(0$pin);
                            
$adc_in adc_in(030);
                            
$ret $ret.GET."/".ANALOG."/".(string)$pin."/".(string)$adc_in."\n";
                        }
                        
ws_write(WS_ID$ret); 
                        break;
                              }
            }    
        }
    }
}
?>