Skip to content

How to: Create a new block

Charles P edited this page Jul 24, 2017 · 2 revisions

How to create a new block:

Summary:

The process for creating a new block is a bit more complex than it would seem to need to be. It is important to understand the role of PXT, which provides the underlying block/typescript editing technology, as well as being the wrapper web site. Blapp is what is known as a PXT "target", meaning that as the user creates their code, PXT generates code that targets the Blapp runtime environment.

Unlike other block programming frameworks, PXT is actually a blocks=>typescript=>[output language] compiler. The output language for the micro:bit and various other controllers is C++. For us, it is javascript.

In the simplest of all PXT targets, there is a top level sim directory. Runtime functions are added to sim/api.ts. A command is run, and libs/[libname]/sims.d.ts is generated from api.ts, containing essentially a header file for the functions in api.ts, which is embedded, along with other *.ts files in libs/[libname] into a big file called target.json. This file is part of the website, and is downloaded (cached smartly) by the PXT page. So in the simplest world, you add your function to sim/api.ts, along with appropriate metadata (using the //% notation), and hey presto you have a new block.

Unfortunately Blapp is not the simplest of all targets. For various reasons, we greatly prefer not to put our block implementation in sim/api.ts (See ChParker for details). So:

Detail:

To add a hypothetical block Foo to an existing namespace, e.g. UI:

  1. Add code to libs/core/ui.ts (or whatever the source file corresponding to the top-level namespace)

libs/core/ui.ts:

...
    /**
      * This is the help text for the Foo block
      * @param arg help text for arg goes here
     **/
     //% weight=100
     //% color=#935BA5
     //% blockId=user_interface
     //% block="Foo arg %arg"
     export function foo(arg: string) {
         fooImpl(arg);
     }

Note: at this time, foo can only take arguments which are simple types, or enums that are defined in libs/core/types.d.ts. If you need more, come talk to ChParker.

  1. Add a placeholder function for fooImpl to sim/api.ts

sim/api.ts:

...
    //%
    export function fooImpl(arg:string):void {
         // leave this blank or return null if the block returns a value
    }
  1. Add the actual foo implementation to client/shared/src/util/uiApi.ts (or whatever the corresponding *.ts file for your namespace):

client/shared/src/util/uiApi.ts:

...
    export function fooImpl(arg:string): void {
        console.log('fooImpl called');
        // do whatever the block does
    }

Having created the definition and the implementation, you are now ready to test your new block. In the top level (blapp) directory:

\blapp> npm run build-pxt

This will spew a very long list of compile errors, followed by several pages of other messages, followed by the text: Docs written.

At this point, assuming that you are running a session of http://localhost:3000/pxt/index.html, via npm run start (which is the default development practice), you need to hit Refresh in the browser. Note that the PXT page will refresh, and then refresh again. Wait for it. At this point, your block should be available for use.

Question: why all this rigamarole?

To answer this question, it is important to consider that different parts of the code live in very different parts of the system: libs/core/*.ts : all these files get embedded into a .JSON file, Target.json, which is downloaded by the PXT framework and gets compiled by their internal compiler (which is based on/related to Typescript 1.8.7) sim/api.ts : In a simple PXT target, this would be script living in the simulator.html page (that is where the preview lives). One reason we can't do that is because this code is run through a special process during npm run build-pxt that requires that it be compilable by Typescript 1.8.7. Another reason is that this file lives outside of the codebases which constitute the web and mobile applications.Therefore this file effectively doesn't exist at runtime. It is used to generate the libs/core/shims.d.ts file which gets embedded into Target.json. Think of it as the header file for the runtime environment. client/src/shared/util/*Api.ts : These are the actual implementations, built using Webpack and the React Native build process, respectively. This code lives in both the web and mobile applications.

Question: Why do we require a *Impl() routine for every function?

Very simple routines can be implemented directly inside libs/core/*.ts namespaces. But typically a routine will require access to the runtime, client/shared/src/util/CodegenRuntime.ts. CodegenRuntime uses all sorts of types which the PXT compiler could not handle. Therefore it is necessary to isolate any CodegenRuntime calling code from code that gets built into Target.json

Clone this wiki locally