-
Notifications
You must be signed in to change notification settings - Fork 1
IDE
This page describes the structure of the IDE.
Wheel can run in three different modes: a demo version here on github, a NodeJS version and an electron version. To allow this to work the code is separated into three layers: backend, shared and frontend.
The backend code is the code which actually connects to an EV3 or Powered Up device.
Some code like constant declarations and some device code is shared between the frontend and the backend.
The frontend code contains the presentation layer which shows up on the screen.
To decouple different components, a global dispatcher is used. For example, when an error occurs in the compiler, then a message is dispatched. The compiler has does not know anything about the console and the console does not know anything about the compiler. The console listens to messages from the dispatcher and when a for the console a relevant message comes in then it's handled there.
The following code shows how a component starts listening to messages:
dispatcher.on('Console.Log', this, this.onLog);
The dispatcher.on function returns a function which can be called to remove the listener.
When a log message should be shown then a message can be dispatched:
dispatcher.dispatch('Console.Log', {type: 'Info', message: 'This is an information message...'});
The dispatcher is a special kind of emitter. There are also a lot of classes which are emitters, for example the SettingsState class. This class emits a message if a settings is changed.
The base class for all dialogs is Dialog which can be found in the file js/frontend/lib/components/Dialog.js.
All screen components live in a layer. The screen layer is the first layer, where uiId equals 1. All components which can have a tab focus have a uiId.
If a dialog is shown then a different uiId is activated. A uiId is pushed to a stack, the highest value on the stack is the active component layer. All components which don't have the same uiId as the popup are disabled. When the dialog is closed then the last uiId popped from the stack and is activated again. All components with that uiId can receive tab focus again. This ensures that components which are not on the active dialog can not receive tab focus.
An instance from the UIState class is passed to every component. The component listens to an event which is triggered if the uiId changes:
this._onGlobalUIId = ui.addEventListener('Global.UIId', this, this.onGlobalUIId);
The dom nodes of a dialog are only attached to the document while the dialog is visible. Once the dialog is hidden again the dom nodes of the dialog are removed from the document.
When the frontend needs information or data from the backend for example to connect to a device or retrieve a file then this is done through a data provider. To get a data provider, the getDataProvider function is used. The data provider is a dependency which has different implementations for electron, the browser and the demo website.
All dataproviders have a getData function:
getData: function(method, uri, params, callback) {}
The method can be post or get and the uri is a path string. The params
parameter is an object with keys/values.
The callback will be called when the data is received and has a single data
parameter:
onData: function(data) {}
For both the EV3 and Powered Up devices the state of the device is represented in a sub class of the BasicDeviceState class which is located in the (js/frontend/vm/BasicDeviceState.js) file.
The EV3State class (js/frontend/vm/ev3/EV3State.js) and the Powered Up class (js/frontend/vm/poweredup/PoweredUpState.js) communicate to the backend through a data provider. These classes do not communicate directly with the devices. This allows the IDE to run in electron as well as in a browser.
The communication is done through an abstraction layer, a data provider.
The demo version in the browser uses the web version of the Powered Up library. The NodeJS version uses de same library but it's running in a server and the data provides communicates through HTTP. The electron uses the library but makes direct calls.
Separate devices are represented as layers with the device state class. The base class for a layer is BasicLayerState located in the file js/frontend/vm/BasicLayerState.js.
For EV3 devices, each layer represents either the primary device or for layers higher than one it represents a daisy chained device. The maximum number of EV3 devices is four.
For Powered Up devices, each layer represents a specific device which also has to be connected separately. The number of Powered Up devices which can be connected is currently eight.
When the state of a device changes then the device class emits a signal.
NXT signals:
Class | Signal | Description |
---|---|---|
device/nxt/LayerState | NXT.Layer*.Motor.Changed* | A motor position changed, layer 0-3, changed 0-2. |
device/nxt/LayerState | NXT.Layer*.Sensor.Assigned* | A sensor type is detected, layer 0-3, assigned 0-3. |
device/nxt/LayerState | NXT.Layer*.Sensor.Changed* | A sensor value is detected, layer 0-3, changed 0-3. |
EV3 signals:
Class | Signal | Description |
---|---|---|
device/ev3/LayerState | EV3.Layer*.Motor.Assigned* | A motor is connected, layer 0-3, assigned 0-3. |
device/ev3/LayerState | EV3.Layer*.Motor.Changed* | A motor position changed, layer 0-3, changed 0-3. |
device/ev3/LayerState | EV3.Layer*.Sensor.Assigned* | A sensor is connected, layer 0-3, assigned 0-3. |
device/ev3/LayerState | EV3.Layer*.Sensor.Changed* | A sensor value changed, layer 0-3, changed 0-3. |
Powered Up signals:
Class | Signal | Description |
---|---|---|
device/poweredup/LayerState | PoweredUp.Layer*.Uuid | The uuid value changed, layer 0-9. |
device/poweredup/LayerState | PoweredUp.Layer*.Type | A device is detected, layer 0-9. |
device/poweredup/LayerState | PoweredUp.Layer*.Tilt | The tilt value changed, layer 0-9. |
device/poweredup/LayerState | PoweredUp.Layer*.Button | A button value changed, layer 0-9. |
device/poweredup/LayerState | PoweredUp.Layer*.Accel | The acceleration value changed, layer 0-9. |
device/poweredup/LayerState | PoweredUp.Layer*.Assigned* | A motor or sensor is connected, layer 0-9, assigned 0-3. |
device/poweredup/LayerState | PoweredUp.Layer*.Changed* | A motor or sensor value changed, layer 0-9, changed 0-3. |
Spike signals:
Class | Signal | Description |
---|---|---|
device/spike/LayerState | Spike.Layer*.Gyro | The gyro value changed, layer 0-3. |
device/spike/LayerState | Spike.Layer*.Accel | The acceleration value changed, layer 0-3. |
device/spike/LayerState | Spike.Layer*.Pos | The position value changed, layer 0-3. |
device/spike/LayerState | Spike.Layer*.Battery | The battery level (0..100%), layer 0-3. |
device/spike/LayerState | Spike.Layer*.Button | The button value changed, layer 0-3. |
device/spike/LayerState | Spike.Layer*.Assigned* | A motor or sensor is connected, layer 0-9, assigned 0-3. |
device/spike/LayerState | Spike.Layer*.Changed* | A motor or sensor value changed, layer 0-9, changed 0-3. |
The backend is accessed through a data provider. A data provider is acquired by a call to the getDataProvider function declared in the file js/frontend/lib/dataprovider/dataProvider.js.
When the electron version is active then an instance of ElectronDataProvider (js/frontend/lib/dataprovider/ElectronDataProvider.js) is returned. If the NodeJS or browser version is active then an instance of the HttpDataProvider (js/frontend/lib/dataprovider/HttpDataProvider.js) class is returned.
In the data provider there are routes which are either accessed through HTTP or called immediately.
Since the HTTP calls are asynchronous the direct calls are also made asynchronous. This is done to prevent a leaky abstraction. By making the direct calls asynchronous it is ensured that the all implementations of the data provider behave the same way.
Because the demo version does not have an actual server or electron backend it uses a virtual file system which is extended by local storage for custom files.
┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ frontend/ide/simulator/SimulatorModules │
│ │
│ ┏━━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━┓ │
│ ┃ MotorModule ┃ ┃ SensorModule ┃ ┃ SoundModule ┃ ┃ ... ┃ │
│ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ │
│ └┈┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈ ┈ ┈ ┈ │
│ ┊ ┊ │
│ ┏━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━┓ ┊ │
│ ┃ SettingsState.getActiveDevice ┃ Event signal │
│ ┗━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━┛ ┊ │
│ ┊ ┏━━━━━━━━━┷━━━━━━━━━━┓ │
│ ┊ ┃ SimulatorModules ┃ │
│ ┊ ┗━━━━━━━━━┯━━━━━━━━━━┛ │
│ ┊ ┌┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈ ┈ ┈ ┈ │
│ ┊┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ │
│ ┊┃ EV3Plugin ┃ ┃ PoweredUpPlugin ┃ ┃ SpikePlugin ┃ │
│ ┊┗━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━┛ │
│ ┊ │
│ frontend/vm/device/* ┊ │
│ ┊ │
│ ┌┈┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┐ │
│ ┊ ┊ ┊ ┊ │
│ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ │
│ ┃ EV3State ┃ ┃ NXTState ┃ ┃ PoweredUpState ┃ ┃ SpikeState ┃ │
│ ┃ ┌────────────┐ ┃ ┃ ┌────────────┐ ┃ ┃ ┌────────────┐ ┃ ┃ ┌────────────┐ ┃ │
│ ┃ │ LayerState ├┐ ┃ ┃ │ LayerState ├┐ ┃ ┃ │ LayerState ├┐ ┃ ┃ │ LayerState ├┐ ┃ │
│ ┃ └┬───────────┘├┐ ┃ ┃ └┬───────────┘├┐ ┃ ┃ └┬───────────┘├┐ ┃ ┃ └┬───────────┘├┐ ┃ │
│ ┃ └┬───────────┘│ ┃ ┃ └┬───────────┘│ ┃ ┃ └┬───────────┘│ ┃ ┃ └┬───────────┘│ ┃ │
│ ┃ └────────────┘ ┃ ┃ └────────────┘ ┃ ┃ └────────────┘ ┃ ┃ └────────────┘ ┃ │
│ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ ┗━━━━━━━━━━┯━━━━━━━━━━━┛ │
│ ┊ ┊ ┊ ┊ │
│ └┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┘ │
│ ┊ │
│ ┊ │
└──────────────────────────────────────────────────┼──────────────────────────────────────────────────┘
┊
┏━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┓
┃ Data provider ┃
┃ - ElectronDataProvider ┃
┃ - HttpDataProvider ┃
┗━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━┛
┊
┌──────────────────────────────────────────────────┼──────────────────────────────────────────────────┐
│ ┊ │
│ shared/device/* ┊ │
│ ┊ │
│ ┌┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┈┴┈┈┈┈┈┈┈┈┈┈┈┬┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┐ │
│ ┊ ┊ ┊ ┊ │
│ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ ┏━━━━━━━━━━┷━━━━━━━━━━━┓ │
│ ┃ EV3 ┃ ┃ NXT ┃ ┃ PoweredUp ┃ ┃ Spike ┃ │
│ ┗━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━┛ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────┘
Wheel has a number of different editors:
The IDE has a main wrapper div which contains the editors. Each time a file is opened a new child div with an editor is added to the main wrapper. Only one of these editors is visible.
The base class for all editors is Editor in the file js/frontend/ide/editor/editors/Editor.js.
The following tree shows the hierarchy of the editor classes:
──┬ Editor This class handles:
│ * The state of all editors
│ * Events to open files, close files and create files
│
└──┬ Editors This class has the following:
│ * A wrapper div for all editors
│ * Code to create a new Editor sub class instance based on the file extension
│ * Image loader
│ * Sound load
│
├─── home/HomeScreen The home screen is always created but only visible if there are no editors open
...
├─── form/FormEditor The same class can exist as multiple instances for different files
├─── form/FormEditor
└─── text/WheelEditor