Embed WTP as an iframe#

For educational use, WebTigerPython can be embedded in any website as an iframe and controlled from the parent page with the postMessage API.

Embedding the iframe#

The iframe can be embedded in any website with the iframe html tag. A whole working example can be found in the Section Demo.

<iframe src="https://webtigerpython.ethz.ch" id="iframe" allow="usb;bluetooth;clipboard-write">
</iframe>

To enable all features of WTP, the iframe needs to be allowed to use usb (micro:bit, Calliope), bluetooth (Root Robot, xBot) and clipboard-write.

The initial state of the iframe (code, layout, language, …) can be set directly in the src URL, see URL arguments.

Messages can be sent to the iframe with JavaScript. First, we need a reference to the iframe; in this example we look it up by the id specified above.

// Create a reference to the iframe
const iframe = document.querySelector("#iframe");

// Create a JavaScript object that can be sent to the iframe. See Messages
const message = {
  type: "files",
  data: [{ name: "main.py", data: "print(42)" }],
};

// Use the postMessage API to send messages to the iframe
iframe.contentWindow.postMessage(message, "*");

The types of messages that can be sent to the iframe and what they do are specified under Messages/to iframe.

To receive messages from the iframe, register a listener. In this basic example, we log the data of the received messages to the console.

window.addEventListener("message", function (event) {
  console.log(event.data);
});

The types of messages that are sent by the iframe are specified under Messages/from iframe.

Tip

Wait for the pythonReady message before sending the first message. Messages sent before WTP has finished loading are lost, and messages that start Python code (run_code, debug_code) need Python to be loaded.

Messages#

Every message is a JavaScript object with two attributes:

  • type: specifies the content of the message

  • data: the data specified by the type (omitted for messages without data)

Example:

{
  type: "settings", // type of the message
  data: {           // data defined by the type
    dark_mode: true,
  },
}

To iframe#

Whenever a message is sent to the iframe, it is parsed and the IDE executes the task. After the task is done, the iframe responds with a message of type ready whose data is the type of the handled message. If handling the message fails, a postMessage_error message containing the error is sent before ready.

settings#

Settings are sent to the iframe to configure WTP. All fields specified in data are updated, while fields that are not specified stay the same.

Invalid values (e.g. an unknown device) are answered with a postMessage_error message. Unknown fields are ignored and only logged as a warning in the browser console of the iframe. Files can not be set with the settings, use the files message instead.

Fields that can be specified:

Field

Type

Description

Example

lang

String

IDE language code (en, de, fr, it, es, ru, sk, hi)

"de"

layout

Array<Array | string | object>

The editor layout. Nested array, can only contain one canvas (see layout)

["Editor", ["Canvas", "Console"]]

hide_topbar

Boolean

Hides the top bar

true

dark_mode

Boolean

Toggles dark mode

true

device

String

Device to be programmed: "-" (none), "micro:bit", "Calliope mini 1/2", "Calliope mini 3", "Oxocard", "RootRobot", "xBot", "ESP32" (only when experimental features are enabled)

"micro:bit"

error_messages

String[]

Error enhancement methods to use

["TigerPython"]

cascade_input

Boolean

Forward input() calls to the parent page instead of showing the input dialog (see input)

true

debug_mode

Boolean

Enables the debugger

true

breakpoints

Object

Breakpoints per file

{"main.py": [2, 5]}

tutorial_size

Number

Width of the tutorial panel (see layout)

0

file_system_size

Number

Width of the file system sidebar in percent (0 closes it)

20

playground

Object

Environment of the robotics simulator, as exported by the environment editor

vanilla_python

Boolean

Deprecated. Use WebEduPy for standard Python instead. On webtigerpython.ethz.ch, true redirects to WebEduPy

false

tag

String

Tag that is attached to logged code events

"class-4a"

Note

full_screen is still accepted as a legacy alias of hide_topbar.

Example settings:

{
  type: "settings",
  data: {
    lang: "en",
    layout: ["Editor", ["Canvas", "Console"]],
    tutorial_size: 0,
    device: "micro:bit",
    error_messages: [],
    dark_mode: true,
  },
}

get_settings#

Queries the current settings. The iframe answers with a message of type settings.

Example:

// Query:
{ type: "get_settings" }

// Response:
{
  type: "settings",
  data: {
    lang: "en",
    error_messages: ["TigerPython"],
    dark_mode: false,
    layout: ["Editor", ["Canvas", "Console"]],
    device: null,
    full_screen: false,
    cascade_input: false,
    tutorial_size: 0,
    debug_mode: false,
    vanilla_python: false,
    playground: "...",
  },
}

layout#

The layout is part of the settings, however due to its complexity, it is described separately.

The layout is a nested array. The first level specifies the UI elements on the horizontal axis, separated by vertical separators. The array can contain strings describing the components ("Editor", "Console", "Canvas") or another (nested) array containing further components / arrays. The levels always alternate between horizontal and vertical splitting. Instead of strings, it is also possible to use objects with the fields type and size to additionally specify the size. Look at the examples below for inspiration.

Examples:

["Editor","Console"]

{
  type: "settings",
  data: {
    layout: ["Editor", "Console"],
  },
}

https://webtigerpython.ethz.ch/#?layout=[“Editor”,“Console”]

[["Editor","Console"],"Canvas"]

{
  type: "settings",
  data: {
    layout: [["Editor", "Console"], "Canvas"],
  },
}

https://webtigerpython.ethz.ch/#?layout=[[“Editor”,“Console”],“Canvas”]

[{"type":["Editor","Console"],"size":20},"Canvas"] with hidden top bar

{
  type: "settings",
  data: {
    layout: [{ type: ["Editor", "Console"], size: 20 }, "Canvas"],
    hide_topbar: true,
  },
}

https://webtigerpython.ethz.ch/#?layout=[{“type”:[“Editor”,“Console”],“size”:20},“Canvas”]&hide_topbar=true

files#

Replaces the whole workspace with the given files. The files are sent as an array of objects which all contain name and data. The name can contain a path with folders (separated by /). If a main.py exists, it is opened in the editor, otherwise the first file.

See Multiple files for how files work in WTP.

Example files:

{
  type: "files",
  data: [
    { name: "main.py", data: "import helper\nhelper.greet()" },
    { name: "helper.py", data: "def greet():\n    print('Hello')" },
    { name: "lib/tools.py", data: "x = 1" },
  ],
}

Tip

To change the files of an iframe that is already loaded, use this message. Changing only the part after # of the iframe’s src (e.g. a new code= or fs= argument) does not reload WTP.

get_files#

Queries the current files of the workspace, including unsaved changes in the editor. The iframe answers with a message of type files, whose data is an object with the file paths as keys. name contains only the file name without folders. Text files are returned as strings, binary files as Blob.

Example:

// Query:
{ type: "get_files" }

// Response:
{
  type: "files",
  data: {
    "main.py": { name: "main.py", data: "import helper\nhelper.greet()" },
    "helper.py": { name: "helper.py", data: "def greet():\n    print('Hello')" },
    "lib/tools.py": { name: "tools.py", data: "x = 1" },
  },
}

set_run_file#

Pins the file that is executed by the run button and by run_code. Without a pinned run file, the file currently open in the editor is run.

{
  type: "set_run_file",
  data: { filename: "game/main.py" },
}

run_code#

Starts the Python execution of the run file.

{ type: "run_code" }

stop_execution#

Aborts the Python execution. The aborted program reports an error message (e.g. KeyboardInterrupt), followed by full_output and program_finished.

{ type: "stop_execution" }

input#

If cascade_input is enabled, every call of input() (and inputInt(), inputFloat(), …) sends an input message (see From iframe) to the parent and waits. The parent answers with a message of type input; the data is used as the user’s answer.

{ type: "input", data: "42" }

Debugger messages#

The debugger can be controlled from the parent page as well.

Caution

Debugging inside an iframe currently does not work: debug_code (and the debug button inside the iframe) runs the program to the end without pausing and without output, so the step messages have no effect. The breakpoint messages (set_breakpoints, get_breakpoints) work.

Type

Data

Description

debug_code

-

Enables debug mode and starts the code in the debugger

debug_step

-

Executes the next line (only while debugging)

debug_breakpoint

-

Runs until the next breakpoint (only while debugging)

debug_continue

-

Runs the rest of the program without stopping (only while debugging)

set_breakpoints

{ filename: "main.py", lines: [2, 5] }

Sets the breakpoints of a file. Currently only main.py is supported

get_breakpoints

-

Answers with a breakpoints message, e.g. { type: "breakpoints", data: { filename: "main.py", lines: [2, 5] } }

Each successful debugger step is acknowledged with a debug_action message.

From iframe#

Triggered by events such as code execution, the iframe sends messages to the parent window.

Type

Sent when

Data

ready

After every handled message

Type of the handled message

pythonReady

WTP and Python (Pyodide) have finished loading. From now on, messages can be sent

-

postMessage_error

A message to the iframe could not be handled

Error message

settings

Answer to get_settings

Settings object

settings_update

A setting was changed (e.g. by the user or a settings message)

Object containing only the changed settings, with the same keys as get_settings (e.g. full_screen for hide_topbar)

files

Answer to get_files

Files object

files_changed

A file was saved in the editor

Array of changed file paths

code

Code is run, exported or flashed to a device

Code in the editor

code_run

Code execution starts

Code that is executed

output

Text is printed to the console

Printed text

error

An error occurred

Error message

full_output

Code execution ended (also when aborted)

Complete content of the console (no data if the console is empty)

program_finished

Code execution ended (after full_output)

-

input

The program calls input() and cascade_input is enabled

{ prompt: "Your name?", type: "string" }

breakpoints

Answer to get_breakpoints

{ filename, lines }

debug_action

A debugger step was executed

"successful"

register_device, disconnect_device, flash, run_code, stop_code, export_code

The corresponding device action is triggered

{ device: "microbit" }

Caution

Not every line is sent as a separate output message. Sometimes multiple lines are bundled together.

Examples:

{ type: "output", data: "test\n" }

{ type: "error", data: "NameError: name 'x' is not defined\n" }

{ type: "full_output", data: "test\n" }

Demo#

Specify Config and send it to Editor above

Output From the Editor is logged below

Source Code of the Demo#

<body>
  <iframe
    src="https://webtigerpython.ethz.ch/"
    id="iframe"
    style="height: 500px; width: 100%"
    allow="usb;bluetooth;clipboard-write"
  >
  </iframe>
  <script
    src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.1.1/jsoneditor.min.js"
    integrity="sha512-ZZTYi+tU/p2AEQSfuloqzXrZZiPkgPrXF1Pcj4OVAm4rsUnUPGme+gga6eAMbzKu+gPa1uciaOVIcxCGIsuLEQ=="
    crossorigin="anonymous"
    referrerpolicy="no-referrer"
  ></script>
  <div id="app">
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.1.1/jsoneditor.css"
      integrity="sha512-iOFdnlwX6UGb55bU5DL0tjWkS/+9jxRxw2KiRzyHMZARASUSwm0nEXBcdqsYni+t3UKJSK7vrwvlL8792/UMjQ=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    />
    <p>Specify Config and send it to Editor above</p>
    <button onclick="setCode(run)">run</button>
    <button onclick="setCode(stop_exec)">stop_exec</button>
    <button onclick="setCode(one_file)">one_file</button>
    <button onclick="setCode(two_files)">two_files</button>
    <button onclick="setCode(two_files_subfolder)">two_files_subfolder</button>
    <button onclick="setCode(get_files)">get_files</button>
    <button onclick="setCode(get_settings)">get_settings</button>
    <div id="messageInput" style="height: 500px"></div>
    <button onclick="sendMessage()">Send Config</button>
    <p>Output From the Editor is logged below</p>
    <div id="outputArea"></div>
  </div>
  <script>
    const iframe = document.querySelector("#iframe");
    const input = document.querySelector("#messageInput");
    const options = {
      mode: "code",
      modes: ["code", "form", "text", "tree", "view", "preview"],
    };
    const editor = new JSONEditor(input, options);
    // set json
    const one_file = {
      type: "files",
      data: [{ name: "main.py", data: "print(42)" }],
    };
    const two_files = {
      type: "files",
      data: [
        {
          name: "main.py",
          data: "import drawing\n" + "drawing.draw()",
        },
        {
          name: "drawing.py",
          data:
            "from gturtle import *\n" +
            "def draw():\n" +
            "  makeTurtle()\n" +
            "  forward(30)\n" +
            "  right(90)\n" +
            "  forward(30)\n",
        },
      ],
    };
    const two_files_subfolder = {
      type: "files",
      data: [
        {
          name: "main.py",
          data: "fp = open('test/hello.txt', 'r')\nprint(fp.read())\nfp.close()",
        },
        {
          name: "test/hello.txt",
          data: "Hello World!",
        },
      ],
    };
    const run = { type: "run_code" };
    const stop_exec = { type: "stop_execution" };
    const get_files = { type: "get_files" };
    const get_settings = { type: "get_settings" };
    function setCode(text) {
      editor.set(text);
    }
    editor.set(one_file);
    function sendMessage() {
      iframe.contentWindow.postMessage(JSON.parse(editor.getText()), "*");
    }
    const output = document.querySelector("#outputArea");
    output.innerHTML = "";
    window.addEventListener("message", function (event) {
      console.log(event.data);
      output.innerHTML = output.innerHTML + "<br>" + JSON.stringify(event.data);
    });
  </script>
</body>