Creating blocks

Ernesto Laval edited this page Jun 22, 2016 · 15 revisions

In Firstmakers-blocky we use money of the built-in blocks that come with blockly, and we also add a number of new blocks that have been created for working with Arduino and Firstmakers shields.

For a new block to b available (and to actually work), we need series of steps that are represented in the following diagram:

Block definition

The basic step for a Block to exist is to define it in Blockly. This definition is made in Javascript using the Blockly.Blocks object with a property that identifies the new block.

For example, the definition of the "light_on" block is:

// -----------
// light_on
// -----------
Blockly.Blocks['light_on'] = {
    init: function() {
      this.appendDummyInput()
        .appendField(Blockly.Msg.FIRSTMAKERS_LIGHT_ON_TITLE)
        .appendField(new Blockly.FieldDropdown([[Blockly.Msg.FIRSTMAKERS_WHITE, "13"], [Blockly.Msg.FIRSTMAKERS_RED, "7"], [Blockly.Msg.FIRSTMAKERS_YELLOW, "5"], [Blockly.Msg.FIRSTMAKERS_GREEN, "4"]]), "COLOR_PIN");
      this.setPreviousStatement(true, null);
      this.setNextStatement(true, null);
      this.setColour(Blockly.Blocks.firstmakers.HUE);
      this.setTooltip(Blockly.Msg.FIRSTMAKERS_LIGHT_ON_TOOLTIP);
      this.setHelpUrl('http://www.firstmakers.com/');
  }
};

For specific documentation on how to define new blocks in Blockly, check https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks.

In Firstmakers-Blockly we define all new blocks in the file app.blocks.definitions.js.

Most of the blocks are defined using the Javascript API (as in the 'light_on' example above), but some blocks are defined using a mixed format which also uses the JSON definition. For example:

// -----------
// servo
// -----------
Blockly.Blocks['servo'] = {
  init: function() {
    this.jsonInit({
      "message0": Blockly.Msg.FIRSTMAKERS_SET_SERVO_TITLE,
      "args0": [
        {
          "type": "field_dropdown",
          "name": "PIN",
          "options": [["3", "3"], ["8", "8"], ["9", "9"], ["10", "10"],   ["11", "11"], ["12", "12"]]
        },
        {
          "type": "input_value",
          "name": "ANGLE",
          "check": "Number"
        }
      ],
    });
    this.setInputsInline(true);
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour(Blockly.Blocks.firstmakers.HUE);
    this.setTooltip(Blockly.Msg.FIRSTMAKERS_SET_SERVO_TOOLTIP);
    this.setHelpUrl('http://www.firstmakers.com/');
  }
};

The 'this.jsonInit' instruction, allows to define a block message using the format "Set servo in pin %1 to %2 degrees" and a series of arguments (components) which will be replaced instead of the placeholders (%1, %2, ...). This is a flexible syntax that is suitable for using different languages that might use different order for the components. For example, we could use "Set pin %1 to %2" in English and "Asignar %2 al pin %1" in Spanish.

Multilanguage text

You might have noticed that in the examples of block definition above, we are not using explicit text but soemthing like this:

Blockly.Msg.FIRSTMAKERS_LIGHT_ON_TITLE

This is a message identifier that will be translated into the defintive text depending on the selected language.

The translation is achieved my defining the text for the respectives Blockly.Msg properties.

Example:

Blockly.Msg.FIRSTMAKERS_LIGHT_ON_TITLE = "prender luz"; Blockly.Msg.FIRSTMAKERS_WHITE = "blanca";

or

Blockly.Msg.FIRSTMAKERS_LIGHT_ON_TITLE = "turn on light"; Blockly.Msg.FIRSTMAKERS_WHITE = "white";

The initial version of Firstmakers-Blockly supports English and Spanish. The respective messages are defined in files

translations/firstmakersBlocks/en-js translations/firstmakersBlocks/es-js

Example of message definition:

English

Blockly.Msg.FIRSTMAKERS_MOTOR_DIRECTION_TITLE = "direction of DC motor %1 is %2"; Blockly.Msg.FIRSTMAKERS_MOTOR_DIRECTION_FORWARD = "forward"; Blockly.Msg.FIRSTMAKERS_MOTOR_DIRECTION_BACKWARD = "backwards";

Spanish

Blockly.Msg.FIRSTMAKERS_MOTOR_DIRECTION_TITLE = "dirección de motor %1 hacia %2"; Blockly.Msg.FIRSTMAKERS_MOTOR_DIRECTION_FORWARD = "adelante"; Blockly.Msg.FIRSTMAKERS_MOTOR_DIRECTION_BACKWARD = "atrás";

The tooltip message for the blocks is also defined here and it needs much improvement (help is appreciated).

The code for actually loading the specific language definition is located in controller.js through the function setLanguage

/**
 * Sets the interface language
 */
function setLanguage(langKey) {
    var deferred = $q.defer();
    
    myself.selectedLanguage = langKey;
    $translate.use(langKey);
  
     // Load Blockly's language strings.

    // Load msg definitions for the specified language (Ex msg/js/en.js)
    $http.get('./bower_components/google-blockly/msg/js/'+langKey+'.js')
    .then(function(res) {
        var blocklyMainMsgCode = res.data;
        eval(blocklyMainMsgCode);
        
        // Load firstmakers msg definitions for the specified language (Ex msg/js/en.js)
        return $http.get('./translations/firstmakersBlocks/'+langKey+'.js')
    })
    .then(function(res) {
        var blocklyFirstmakersMsgCode = res.data;
        eval(blocklyFirstmakersMsgCode);
        deferred.resolve();
    })
    .catch(function(err) {
        deferred.reject(err);
    }) 
    
    return deferred.promise;
}

New languages could be defined if the respective language file is provided and the language is added to the interface (index.hml).

    <li ng-class="{'active': controller.selectedLanguage == 'en'}"><a ng-click="controller.changeLanguage('en')" href="#" translate="BUTTON_LANG_EN"></a></li>  

Block Javascript code

When executed, each block in the workspace is transformed into Javascript code.

For example

Is transformed into the following code

fm_digitalWrite(7,true);

The generation of such code is defined in the Blockly.Javascipt object via a function that return the code for the respective block. For example:

Blockly.JavaScript['digital_pin_on'] = function(block) {
  var pin = block.getFieldValue('PIN');
  var code = 'fm_digitalWrite('+pin+',true);\n';
  return code;
};

In the generation of the code we need some values that are defined by the user through the blocks Inputs & Fields. Since fields have names, they can be obtained through the getFieldValue instruction:

var pin = block.getFieldValue('PIN');

When the javascript code is a statement that is not expected to be the input for another block (it does not return a value) we use the above mentioned sintaxis for returning the code. But when the block is an output block (its value can be used as input for other blocks) we must return an array with 2 elements (the code and the order for the operator presedence).

For example:

Blockly.JavaScript['potentiometer'] = function(block) {
  var code = 'fm_potentiometer()';
  return [code, Blockly.JavaScript.ORDER_ATOMIC];
};

The definition of all Javascript generators for Firstmakers is included in the same file as the Block definitions:

_app.blocks.definitions.js_.

Toolbox definition

The actual list of available Blocks and the definitions of its categories is done in the tolbox.xml file.

<xml id="toolbox" style="display: none">
  <category name="CAT_CONTROL" colour="120">
    <block type="wait"></block> 
    <block type="on_key"></block>    
    <block type="controls_repeat_forever"></block>
    <block type="controls_repeat_ext">
        <value name="TIMES">
          <block type="math_number">
            <field name="NUM">3</field>
          </block>
        </value>
    </block>
    <block type="controls_whileUntil"></block>
  </category>
  ...
  <category name="CAT_FIRSTMAKERS" colour="210" >
    <block type="light_on"></block>      
    <block type="light_off"></block> 
    <block type="buzzer_on"></block>      
    <block type="buzzer_off"></block> 
    
    <block type="potentiometer"></block> 
    <block type="temperature_sensor"></block> 
    <block type="light_sensor"></block> 
    <block type="audio_sensor"></block> 
    <block type="humidity_sensor"></block> 
    <block type="infrared_sensor"></block> 
    
    <block type="button"></block> 
    <block type="digital_pin_on"></block>      
    <block type="digital_pin_off"></block> 
    <block type="read_digital_pin"></block>     
    <block type="read_analog_pin"></block>     
    <block type="servo">
      <value name="ANGLE">
        <block type="math_number">
          <field name="NUM">180</field>
        </block>
      </value>
    </block> 
    <block type="motor_config"></block> 
    <block type="motor_speed">
      <value name="SPEED">
        <block type="math_number">
          <field name="NUM">100</field>
        </block>
      </value>
    </block> 
    <block type="motor_direction"></block> 
  </category>
	...
</xml>

Blockly interpreter

Some implementations of Blockly could make a direct generation and execution of the code. Firstmakers-Blockly uses the Blockly Javascript interpreter (TODO: Add link) for several reasons:

  • With the interpreter we can higlight the code that is being executed (which is useful for understanding how the code works)
  • The interpreter offers allows to manage Javascript sync calls (for example stop the code execution while waiting for a callback during a setTimeout function - which isused to implement a wait command).
  • The interpreter offers an isolated scope (sandbox) for the execution of Javascript functions. This allows us to manage the call fo external modules/libraries without the risk of conflict with global definitons.

The definition/configuration of the Blockly interpreter behaviour is made in the file

services/blockly.service.js

Here we define the API for javascript calls that we have defined in our Blockly blocks.

For example, the block 'light_on' when set to turn on the white light generates the following Javascript code:

fm_digitalWrite(13, true);

This is a Javascript command that would turn on digital pin 13. This is obvously not a standard Javascrit Command, so the interpreter needs to define how to handle it.

The function initApi in services/blockly.service.js in in charge of this definitions

function initApi(interpreter, scope) {
    var wrapper;
    ...
}

Here you will find definitions such as:

// digitalWrite(pin,state)
wrapper = function(pin,value) {
  pin = pin ? pin.toNumber() : 13;
  value = value ? value.toBoolean() : fal    
  return interpreter.createPrimitive(DeviceCommandService.digitalWrite(pin,value));
};
interpreter.setProperty(scope, 'fm_digitalWrite',
    interpreter.createNativeFunction(wrapper));

How does this work?

Whenever the interpreter finds a call to the function fm_digitalWrite(pin, value) in our code, it will call a the function

 DeviceCommandService.digitalWrite(pin,value);

DeviceCommandService is and AngularJS service that we have created to deal with commands that sre related to our physical or virtual devices (more on this in another page).

The Firstmakers Javascript commands that are recognised by the interpreter are:

  • fm_potentiometer()
  • fm_temperatureSensor()
  • fm_lightSensor()
  • fm_audioSensor()
  • fm_humiditySensor()
  • fm_infraredSensor()
  • fm_button()
  • fm_light()
  • fm_digitalWrite(pin,value)
  • fm_digitalRead(pin)
  • fm_analogRead(pin)
  • fm_buzzer(state)
  • fm_servo(pin,angle)
  • fm_motor_config(id, powerPin, dirPin)
  • fm_motor_speed(id,speed)
  • fm_motor_direction(id, dir)
  • fm_say(text)
  • fm_wait(time)

Device/Board functions

The interpreter calls functions from the DeviceCommandService service

For example

DeviceCommandService.digitalWrite(13,true)

But there is still a way to go until these commands are actually executed in the physical or virtual devices and/or boards.

DeviceCommandService

Firstmakers-Blockly assumes that we have a virtual device (the representation of a device in the screen which could simuate some operations, such as turning on a light) and it might have an external device physically connected.

The DeviceCommandService is an intermediate module that is aware of the existence of a Physical and a Virtual device (it has a reference to device object in internal variables) and decides wether the instructions/commands are sent to the physical device, virtual device or both of them.

For example,

  • If no physical device is connected, no instruction is derived to a physical device
  • If the virtual device can deal with the instruction (for example turning on a light), the instruction is derived to the virtual device
  • If the virtual device can not deal with the instruction, or the physical device has precedence (for example a sensor value is read from the physical device if present), the instruction is not sent to the virtual device.

Overview of DeviceCommandService

DeviceCommandService is defined in deviceCommand.service.js and has the following structure

angular.module('tideApp')
.service('DeviceCommandService',[function() {
  var myself = this;
 
  // Public functions
  (...)
  myself.digitalWrite = digitalWrite;
  (...)
  
  // Local variables
  var physicalDevice = null;
  var virtualDevice = null;
  
  (...)

  /**
   * Implementations of "external" functions
   */
  
  // digitalWrite
  function digitalWrite(pin,value) {
    if (virtualDevice) {
      virtualDevice.digitalWrite(pin,value);
    }
    
    if (physicalDevice) {
      physicalDevice.digitalWrite(pin,value);
    }
    
  }
  (...)

}])

Use of promises in some functions

Due to the asynchronous nature of many Javascript instructions, some of the DeviceCommandService functions do not return a direct value, but a promise to a future value.

If you are not familiar with promises, they are a convenient way to avoid the use of callbacks as parameters when calling a function. Instead a promise is return which is expected to resolved by calling a function defined with the 'then' method of the promise.

For example:

DeviceCommandService.digitalRead(13)
.then(function(value) {
  // do something with the value of pin 13
})  

Retrieving values from pins (or sensors) is an asynchronous operation in firmata, so all related functiosn are implemented as promises. For example, digitalRead:

// digitalRead
function digitalRead(pin) {
  var deferred = $q.defer();
  
  if (physicalDevice) {
    physicalDevice.digitalRead(pin)
    .then(function(value) {
      deferred.resolve(value);
    })
    .catch(function(err) {
      deferred.reject(err);
    })
  } else if (virtualDevice) {
    virtualDevice.digitalRead(pin)
    .then(function(value) {
      deferred.resolve(value);
    })
    .catch(function(err) {
      deferred.reject(err);
    })
  } else {
    deferred.reject("No board");
  }

  return deferred.promise;
}

The Javascript interpreter provides a way to handle async calls, and this is where we actually use the promise returned by some DeviceCommandService functions.

// digitalRead(pin).
wrapper = function(pin, callback) {  
  pin = pin ? pin.toNumber() : 2;
  DeviceCommandService.digitalRead(pin)
  .then(function(value) {
    callback(interpreter.createPrimitive(value));
  })
};

interpreter.setProperty(scope, 'fm_digitalRead',
    interpreter.createAsyncFunction(wrapper));

DeviceService

DeviceService is an Angular Service that provides and abstraction for dealing with Physical and Virtual Firstmakers devices. The same module (let´s think of it as a Class) is used for creating physical and virtual devices and the only difference is in the type of board linked to the device (a firmata board or a virtual board).

From the Application's point of view, the virtual device is created in the activate() function from main Controller of our App (AppController defined in controller.js).

function activate() {
    (...)
    
    myself.virtualBoard = VirtualBoardService.createVirtualBoard();
    virtualDevice = DeviceService.createDevice(myself.virtualBoard);
    DeviceCommandService.setVirtualDevice(virtualDevice);
    
    (...)
}

The physical device is also created in the AppController, but only when a new board has been detected and connected:

/**
 * Attempts to connect available ports
 */
function connectBoard(ports) {
    (...)
    
    BoardService.connect(firstPort)
    .then(function(board) {
        // Successful connection with first port!! (Yeah)
        myself.physicalBoard = board;
         
        physicalDevice = DeviceService.createDevice(myself.physicalBoard);
        (...)
        physicalDevice.activatePinMonitor();

        DeviceCommandService.setPhysicalDevice(physicalDevice);
        
        (...)
    })
    (...)
}

The Device is an intermediate entity that derives communication to actual boards (which deals mainly with reading/writing pins) and also provides some 'intelligence' to deal with Firstmakers shields.

For example, a device can:

  • Convert de value of analog pin 0 [0 to 1023] to Celsius degrees when requesting the value of the temperature sensor.
  • Continuously read values of digital and analog pins when they change, and store current values for each pin.
  • Store a Motor configuration (which pin is used for speed and which for direction)

The device functionality is defined at DeviceService in services/device.service.js

The monitor & update of pin values is triggered through the function activatePinMonitor(), which calls the board's analogRead() & digitalRead() function. These functions are expected to be called only once, since they will call a callback function each time a value is updated in the respective pin.

device.activatePinMonitor = function() {
  var myself = this;
  
  if (board && board.pins) {
    board.analogRead(0, function(value) {
        board.pins[board.analogPins[0]].value = value;
        sensorValues.temperature = valueConverter.temperature(value);

    })   
    board.analogRead(1, function(value) {
        board.pins[board.analogPins[1]].value = value;
        sensorValues.light = valueConverter.light(value);
    })          
    (...)

The activatePinMonitor() function is called on the physical device right after it´s creation. For virtual devices this monitoring is not needed since pins values are updated whenever the value is changed.

For defining additional device functions, it is needed a new function in the device object at services/device.service.js

For example:

device.servoWrite = function(pin,angle) {
    board.pinMode(pin, board.MODES.SERVO);
    board.servoWrite(pin, angle);
}

The device.servoWrite() function makes the respective calls to the board entity, modifying the pin Mode for SERVO functions and setting the right angle.

Boards (firmata.js)

The board object used for connections with physical boards is part of the firmata nodejs module (node_modules/firmata).

Besides the functionality for connecting / disconnecting boards (which is handled in BoardService at services/board.service.js) a board entity (once created) provides a Board API with the following functions (among others):

  • pinMode(pin,mode)
  • digitalWrite(pin,value)
  • digitalRead(pin,callback)
  • analogWrite(pin,value)
  • analogRead(pin,callback)
  • servoWrite(pin, degree)
  • servoConfig(pin, min, max)

A detailed descripton of the Board API can be found at https://github.com/firmata/firmata.js

VirtualBoardService

The VirtualBoardService is an Angular Service that simulates the functionality of the firmata.js Board, but does not implement the actual firmata communication with a physical board.

A virtual board is created through the function VirtualBoardService.createVirtualBoard(). For example myself.virtualBoard = VirtualBoardService.createVirtualBoard();

This board provides the following functions:

  • board.pinMode(pin,mode)
  • board.digitalWrite(pin,value)
  • board.digitalRead(pin,callback)
  • board.analogWrite(pin,value)
  • board.analogRead(pin,callback)
  • board.servoWrite(pin,angle)

It also provides the following constants:

board.HIGH = 1;
board.LOW = 0;
board.MODES = {
  INPUT: 0x00,
  OUTPUT: 0x01,
  ANALOG: 0x02,
  PWM: 0x03,
  SERVO: 0x04,
  SHIFT: 0x05,
  I2C: 0x06,
  ONEWIRE: 0x07,
  STEPPER: 0x08,
  SERIAL: 0x0A,
  IGNORE: 0x7F,
  UNKOWN: 0x10
}

And the array board.pins[] with information for each of the 20 pins included in the virtual board (14 digital pins and 6 analog pins)

    board.pins[pin] = {
        mode: null, // Current mode of pin which is on the the board.MODES. 
        value: 0, // Current value of the pin. when pin is digital and set to output it will be Board.HIGH or Board.LOW. If the pin is an analog pin it will be an numeric value between 0 and 1023. 
        supportedModes: [ ], // Array of modes from board.MODES that are supported on this pin. 
        analogChannel: 127, // Will be 127 for digital pins and the pin number for analog pins. 
        state: 0 // For output pins this is the value of the pin on the board, for digital input it's the status of the pullup resistor (1 = pullup enabled, 0 = pullup disabled) 
    }

The pin values in this array can be used to represent a virtual device state. For example, we use the Angular Directive 'virtualFirstmakers' (directives/virtualFirstmakers.js) with D3js code to modify the color of the yellow light (pin 5):

      svgContainer.select(".yellow.light")
       .attr("fill", board && board.pins[5].value ? "yellow" : "grey");
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.