Compare commits
35 commits
Author | SHA1 | Date | |
---|---|---|---|
6e0a51ae4c | |||
1e45697958 | |||
4d53f97e29 | |||
94fcc90f42 | |||
477ba60034 | |||
33ffd893d1 | |||
d5af9a22ad | |||
4c6be6ae78 | |||
d82fb8d8cd | |||
5985485ae0 | |||
7332717376 | |||
7144866d99 | |||
bc733c93de | |||
64c7e17022 | |||
0dd1977c47 | |||
15c0680158 | |||
7608509bcc | |||
91b8cbaa4e | |||
6ef4ec5d23 | |||
709c0a3f13 | |||
8adaa887e8 | |||
e29f358b3b | |||
5143932cc0 | |||
9a5ab01c63 | |||
bdca552dea | |||
2a3ffd9914 | |||
c4ec66a926 | |||
40db157233 | |||
7296135b56 | |||
2ab13932d3 | |||
c37be90325 | |||
825fc31c30 | |||
4412306ef6 | |||
30437d24fe | |||
930e7fa408 |
47
.travis.yml
|
@ -1,22 +1,39 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "8"
|
||||
language: android
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-27.0.3
|
||||
- android-27
|
||||
|
||||
before_script:
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-27" # accept android licenses
|
||||
|
||||
install:
|
||||
- nvm install 12
|
||||
- npm install -g ionic cordova
|
||||
- npm install
|
||||
|
||||
script:
|
||||
- npm run build
|
||||
- npm run android:build # implies web build in www/ dir
|
||||
|
||||
before_deploy: "cp platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk sensebox_blockly_${TRAVIS_TAG}.apk"
|
||||
|
||||
deploy:
|
||||
provider: pages
|
||||
skip-cleanup: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
keep-history: true
|
||||
on:
|
||||
tags: true
|
||||
local_dir: www
|
||||
# deploy web build to gh-pages branch
|
||||
- provider: pages
|
||||
skip-cleanup: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
keep-history: true
|
||||
on:
|
||||
tags: true
|
||||
local_dir: www
|
||||
|
||||
- provider: releases
|
||||
api_key: $GITHUB_TOKEN
|
||||
file:
|
||||
- "sensebox_blockly_${TRAVIS_TAG}.apk"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
|
44
README.md
|
@ -1,23 +1,35 @@
|
|||
# Blockly for senseBox app
|
||||
[![Build Status](https://travis-ci.org/sensebox/blockly-app.svg?branch=master)](https://travis-ci.org/sensebox/blockly-app)
|
||||
# Blockly for senseBox app [![Build Status](https://travis-ci.org/sensebox/blockly-app.svg?branch=master)][travis] [![Fdroid Badge](https://img.shields.io/f-droid/v/de.sensebox.blockly.svg)][fdroid] [![github releases](https://img.shields.io/github/release/sensebox/blockly-app.svg?logo=github)][releases]
|
||||
|
||||
Blockly for senseBox is a visual programming editor for the senseBox:edu on mobile.
|
||||
It is based on Google's [Blockly](https://developers.google.com/blockly/) and Carlos Pereira Atencio's [Ardublockly](https://github.com/carlosperate/ardublockly), which has been forked.
|
||||
It is based on Google's [Blockly](https://developers.google.com/blockly/) and Carlos Pereira Atencio's [Ardublockly](https://github.com/carlosperate/ardublockly).
|
||||
|
||||
## Features
|
||||
### Features
|
||||
- generate Arduino code with visual drag-and-drop blocks, with blocks for the senseBox platform.
|
||||
- online compiler for senseBox MCU
|
||||
- over the air programming via WiFi for senseBox MCU
|
||||
- Android 7+ & Web-browser support. iOS support is upcoming!
|
||||
- over the air programming via WiFi
|
||||
- Android 7+ & Web-browser support. (iOS support is upcoming)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75" />][fdroid]
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="75" />][playstore]
|
||||
|
||||
[travis]: https://travis-ci.org/sensebox/blockly-app
|
||||
[releases]: https://github.com/sensebox/blockly-app/releases
|
||||
[fdroid]: https://f-droid.org/packages/de.sensebox.blockly/
|
||||
[playstore]: https://play.google.com/store/apps/details?id=de.sensebox.blockly
|
||||
|
||||
## Development
|
||||
|
||||
This is an Ionic 3 / Angular 5 application using Cordova Plugins for mobile-native functionality.
|
||||
|
||||
|
||||
### dev env setup
|
||||
For a basic web version, only Node.js 8+ is required.
|
||||
For Android & iOS builds the respective platform tooling is required.
|
||||
|
||||
This repo contains mandatory submodules; so you need to clone this repo via
|
||||
```sh
|
||||
git clone --recursive https://github.com/sensebox/blockly-app.git
|
||||
```
|
||||
|
||||
To install npm dependencies run `npm install` once.
|
||||
|
||||
Then run `npm start` to start a hot-reloading development view in the browser on <http://localhost:8100>.
|
||||
|
@ -35,22 +47,18 @@ export ANDROID_HOME=$ANDROID_SDK
|
|||
export PATH=$PATH:$ANDROID_SDK/emulator:$ANDROID_SDK/tools:$ANDROID_SDK/tools/bin:$ANDROID_SDK/platform-tools:$ANDROID_SDK/build-tools/28.0.3
|
||||
```
|
||||
|
||||
To create a signed release build, add a file `platform/android/release-signing.properties` following this template.
|
||||
You need a keystore with a valid signing key!
|
||||
|
||||
```properties
|
||||
storeFile=../../reedu-android.keystore
|
||||
storeType=jks
|
||||
keyAlias=reedu-android
|
||||
keyPassword=xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
storePassword=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
To build & deploy on an emulator or device use the `android:*` build commands defined in `package.json` (some only work on linux), for example:
|
||||
```bash
|
||||
npm run android:dev # build debug build & deploy to connected device & restart app
|
||||
```
|
||||
|
||||
###### release signing
|
||||
You need a keystore with a valid signing key!
|
||||
```
|
||||
zipalign -p 4 blockly-unsigned.apk blockly-aligned.apk
|
||||
apksigner sign -ks reedu-android.keystore --in blockly-aligned.apk --out blockly-signed.apk
|
||||
```
|
||||
|
||||
### updating blockly
|
||||
Blockly is included as a submodule, linking to <https://github.com/sensebox/ardublockly-1>.
|
||||
To update it, just pull in the commit you want, and commit the change in this repository:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget id="de.sensebox.blockly" version="1.1.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<widget id="de.sensebox.blockly" version="1.1.4" android-versionCode="10104" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<name>Blockly for senseBox</name>
|
||||
<description>graphical programming & OTA upload for senseBox MCU</description>
|
||||
<author email="kontakt@reedu.de" href="https://reedu.de/">re:edu</author>
|
||||
|
@ -15,10 +15,12 @@
|
|||
<preference name="android-minSdkVersion" value="24" />
|
||||
<preference name="BackupWebStorage" value="none" />
|
||||
<preference name="SplashMaintainAspectRatio" value="true" />
|
||||
<preference name="FadeSplashScreenDuration" value="300" />
|
||||
<preference name="FadeSplashScreenDuration" value="500" />
|
||||
<preference name="AutoHideSplashScreen" value="false" />
|
||||
<preference name="ShowSplashScreenSpinner" value="false" />
|
||||
<preference name="SplashShowOnlyFirstTime" value="false" />
|
||||
<preference name="SplashScreen" value="screen" />
|
||||
<preference name="SplashScreenDelay" value="4000" />
|
||||
<preference name="SplashScreenDelay" value="10000" />
|
||||
<platform name="android">
|
||||
<allow-intent href="market:*" />
|
||||
<icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
|
||||
|
@ -84,7 +86,6 @@
|
|||
<plugin name="cordova-plugin-ionic-keyboard" spec="^2.0.5" />
|
||||
<plugin name="wifiwizard2" spec="~3.1.0" />
|
||||
<plugin name="cordova-plugin-network-information" spec="2.0.1" />
|
||||
<plugin name="cordova-plugin-app-version" spec="0.1.9" />
|
||||
<engine name="android" spec="7.1.1" />
|
||||
<engine name="browser" spec="5.0.4" />
|
||||
</widget>
|
||||
|
|
1543
package-lock.json
generated
10
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "blockly-sensebox",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.4",
|
||||
"author": "Reedu GmbH",
|
||||
"homepage": "https://reedu.de/",
|
||||
"private": true,
|
||||
|
@ -8,7 +8,7 @@
|
|||
"start": "ionic-app-scripts serve",
|
||||
"clean": "ionic-app-scripts clean",
|
||||
"build": "ionic-app-scripts build --prod",
|
||||
"android:build": "ionic cordova build --release --prod android",
|
||||
"android:build": "npm run clean && ionic cordova build --release --prod android",
|
||||
"android:build:debug": "ionic cordova build android",
|
||||
"android:deploy": "adb install -r platforms/android/app/build/outputs/apk/debug/app-debug.apk",
|
||||
"android:start": "adb shell am force-stop de.sensebox.blockly; adb shell am start -n de.sensebox.blockly/de.sensebox.blockly.MainActivity; sleep 5; adb logcat | grep -F \"`adb shell ps | grep de.sensebox.blockly | awk -F' ' '{print $2}'`\"",
|
||||
|
@ -36,7 +36,7 @@
|
|||
"cordova-browser": "5.0.4",
|
||||
"cordova-plugin-device": "^2.0.2",
|
||||
"cordova-plugin-ionic-keyboard": "^2.1.3",
|
||||
"cordova-plugin-ionic-webview": "^2.3.3",
|
||||
"cordova-plugin-ionic-webview": "^2.4.1",
|
||||
"cordova-plugin-network-information": "^2.0.1",
|
||||
"cordova-plugin-splashscreen": "^5.0.2",
|
||||
"cordova-plugin-statusbar": "^2.4.2",
|
||||
|
@ -51,7 +51,7 @@
|
|||
"zone.js": "0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ionic/app-scripts": "^3.2.2",
|
||||
"@ionic/app-scripts": "^3.2.4",
|
||||
"typescript": "~2.6.2"
|
||||
},
|
||||
"description": "OpenSenseApps by IfGI 2018 App-Dev course.",
|
||||
|
@ -73,4 +73,4 @@
|
|||
"browser"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 40 KiB |
|
@ -1 +0,0 @@
|
|||
41e1c536c5dfb47db1311f7335b357b3
|
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 76 KiB |
|
@ -1 +0,0 @@
|
|||
287a089b0cd0ff44dfda2fd7bd7ad812
|
Before Width: | Height: | Size: 77 KiB |
|
@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||
|
||||
import { COLORS, DEFAULT_LANG } from '../constants';
|
||||
import { BlocklyPage } from '../pages/blockly/blockly';
|
||||
import { StorageProvider, SETTINGS } from '../providers/storage/storage';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'app.html'
|
||||
|
@ -26,7 +27,7 @@ export class openSenseApp {
|
|||
title: 'MENU.DOCS',
|
||||
icon: 'book',
|
||||
callback: () => {
|
||||
window.open('https://sensebox.github.io/books-v2/blockly/' + this.translate.currentLang)
|
||||
window.open(`https://sensebox.github.io/books-v2/blockly/${this.translate.currentLang}/`)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -43,12 +44,17 @@ export class openSenseApp {
|
|||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
private storage: StorageProvider,
|
||||
platform: Platform,
|
||||
statusBar: StatusBar,
|
||||
splashScreen: SplashScreen,
|
||||
) {
|
||||
this.translate.addLangs(['en', 'de']);
|
||||
this.translate.setDefaultLang(DEFAULT_LANG)
|
||||
this.translate.use(this.translate.getBrowserLang()) // @TODO: check if this works on all platforms!
|
||||
const lang = this.getPreferredLanguage()
|
||||
this.translate.use(lang)
|
||||
this.storage.get(SETTINGS).language = lang
|
||||
|
||||
platform.ready()
|
||||
.then(() => {
|
||||
if ((<any>window).cordova) {
|
||||
|
@ -66,8 +72,15 @@ export class openSenseApp {
|
|||
|
||||
// Reset the content nav to have just this page
|
||||
// we wouldn't want the back button to show in this scenario
|
||||
// this.nav.setRoot(page.component);
|
||||
this.nav.setRoot(page.component);
|
||||
}
|
||||
|
||||
private getPreferredLanguage () {
|
||||
const langsAvailable = this.translate.getLangs()
|
||||
const lang = this.storage.get(SETTINGS).language || this.translate.getBrowserLang()
|
||||
return langsAvailable.indexOf(lang) === -1 ? DEFAULT_LANG : lang
|
||||
}
|
||||
}
|
||||
|
||||
interface AppPage {
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit c43a8244e72996457ce832317bb463787645caae
|
||||
Subproject commit cc464a6466c2e6b2648e93e386d574d526cd3f40
|
|
@ -237,64 +237,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select Additional Blocks menu -->
|
||||
<div id="blocks_menu" class="modal modal-fixed-footer hidden">
|
||||
<div class="modal-content">
|
||||
<div>
|
||||
<h4><span class="translatable_addBlocksTitle">Additional Blocks</span></h4>
|
||||
</div>
|
||||
<div id="blocks_menu_body"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="waves-effect btn-flat modal-close"><span class="translatable_return">Return</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Alert: Content is loaded using JavaScript to display alerts -->
|
||||
<div id="gen_alert" class="modal modal_small modal-fixed-footer hidden">
|
||||
<div class="modal-content">
|
||||
<h5 id="gen_alert_title">Empty Alert</h4>
|
||||
<p><span id="gen_alert_body">Empty alert text</span></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="gen_alert_ok_link" href="#" class="waves-effect btn-flat modal-close"><span class="translatable_okay">Okay</span></a>
|
||||
<a id="gen_alert_cancel_link" href="#" class="waves-effect btn-flat modal-close"><span class="translatable_cancel">Cancel</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt: Content is loaded using JavaScript to display input prompt -->
|
||||
<div id="gen_prompt" class="modal modal_small modal-fixed-footer hidden">
|
||||
<div class="modal-content">
|
||||
<p><span id="gen_prompt_message">Prompt message</span></p>
|
||||
<p><input id="gen_prompt_input" value="test" /></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="gen_prompt_ok_link" href="#" class="waves-effect btn-flat modal-close"><span class="translatable_okay">Okay</span></a>
|
||||
<a id="gen_prompt_cancel_link" href="#" class="waves-effect btn-flat modal-close"><span class="translatable_cancel">Cancel</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Local Modal to be shown if Ardublockly Server is not running. -->
|
||||
<div id="not_running_dialog" class="modal">
|
||||
<div class="modal-content">
|
||||
<div>
|
||||
<h4 id="gen_alert_title"><span class="translatable_noServerTitle">Ardublockly app not running</span></h4>
|
||||
</div>
|
||||
<div class="translatable_noServerBody">
|
||||
<p>For all the Ardublockly features to be enabled, the Ardublockly desktop application must be running locally on your computer.</p>
|
||||
<p>If you are using an online version you will not be able to configure the settings nor load the blocks code into an Arduino.</p>
|
||||
<p>Installation instruction can be found in the <a href="https://github.com/sensebox/ardublockly">Ardublockly repository</a>.</p>
|
||||
<p>If you have Ardublockly already installed, make sure the application is running correctly.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="gen_alert_ok_link" href="#" class="waves-effect btn-flat modal-close"><span class="translatable_okay">Okay</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Desktop version of Ardublockly JS, needs to be loaded first. -->
|
||||
<script src="blockly/ardublockly/ardublockly_desktop.js"></script>
|
||||
<!-- jQuery and Materialize JS -->
|
||||
<script src="blockly/ardublockly/js_libs/jquery-2.1.3.min.js"></script>
|
||||
<script src="blockly/ardublockly/materialize/materialize.js"></script>
|
||||
|
@ -320,24 +262,43 @@
|
|||
Ardublockly.init({ blocklyPath: './blockly' });
|
||||
SenseboxExtension.init();
|
||||
|
||||
window.addEventListener('message', function load(event) {
|
||||
switch (event.data) {
|
||||
/**
|
||||
* message handler for the postMessage protocol with the iframe parent (ionic BlocklyPage).
|
||||
*/
|
||||
window.addEventListener('message', function msgHandler (event) {
|
||||
switch (event.data.type) {
|
||||
case 'getSketch':
|
||||
var code = Ardublockly.generateArduino();
|
||||
event.source.postMessage({
|
||||
type: 'sketch',
|
||||
data: code,
|
||||
}, event.origin)
|
||||
event.source.postMessage({ type: 'sketch', data: code }, event.origin);
|
||||
break;
|
||||
|
||||
case 'getXml':
|
||||
var xml = Ardublockly.generateXml();
|
||||
event.source.postMessage({ type: 'xml', data: xml }, event.origin);
|
||||
break;
|
||||
|
||||
case 'setXml':
|
||||
var xml = Ardublockly.replaceBlocksfromXml(event.data.data);
|
||||
break;
|
||||
|
||||
case 'toggleView':
|
||||
document.getElementById('blocklyview').classList.toggle('hidden');
|
||||
document.querySelector('.blocklyToolboxDiv').classList.toggle('hidden');
|
||||
document.getElementById('codeview').classList.toggle('hidden');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`postMessage type ${event.data} not implemented`)
|
||||
event.source.postMessage({
|
||||
type: 'log',
|
||||
data: `postMessage type ${event.data.type} not implemented`,
|
||||
}, event.origin);
|
||||
}
|
||||
});
|
||||
|
||||
// notify parent that we are ready to receive commands.
|
||||
// @FIXME: this is not exactly correct, as the default sketch is still loading after this point!
|
||||
parent.postMessage({ type: 'ready' }, '*')
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"CANCEL": "Abbrechen",
|
||||
"MENU": {
|
||||
"TITLE": "Menü",
|
||||
"ABOUT": "Über",
|
||||
|
@ -22,13 +23,20 @@
|
|||
"BLOCKLY": {
|
||||
"TITLE": "Blockly für senseBox",
|
||||
"BTN_CODE": "Quellcode Ansicht",
|
||||
"BTN_OTA": "OTA Programmierung"
|
||||
"BTN_OTA": "OTA Upload"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Einstellungen",
|
||||
"LANGUAGE": {
|
||||
"TITLE": "Sprache"
|
||||
},
|
||||
"LOG_OPTIN": {
|
||||
"TITLE": "Sende uns automatisch Fehler-Berichte zu",
|
||||
"TITLE": "Sende Fehlerberichte",
|
||||
"TEXT": "Das hilft uns beim Beheben von Fehlern.<br/>Keine persönlichen Daten außer deinem Sketch werden übertragen."
|
||||
},
|
||||
"RESET": {
|
||||
"TITLE": "App zurücksetzen",
|
||||
"TEXT": "Dies setzt die Einstellungen zurück und löscht deine Sketche!"
|
||||
}
|
||||
},
|
||||
"OTAWIZ": {
|
||||
|
@ -58,7 +66,7 @@
|
|||
},
|
||||
"GO_ONLINE": {
|
||||
"TITLE": "Du bist offline.",
|
||||
"TEXT": "Für das Kompilieren des Coes ist eine Internetverbindung nötig. Bitte stelle eine Verbindung her."
|
||||
"TEXT": "Für das Kompilieren des Codes ist eine Internetverbindung nötig. Bitte stelle eine Verbindung her."
|
||||
},
|
||||
"DONE": {
|
||||
"TITLE": "Programm erfolgreich kompiliert."
|
||||
|
@ -70,7 +78,7 @@
|
|||
"WIFI": {
|
||||
"MANUAL": {
|
||||
"TITLE": "Verbindung zur senseBox herstellen",
|
||||
"TEXT": "Deine senseBox sollte nun ein WiFi Netzwerk erstellt haben. Bitte verbinde dich mit diesem Netzwerk im im WiFi Menü deines Geräts."
|
||||
"TEXT": "Deine senseBox sollte nun ein WiFi Netzwerk erstellt haben. Bitte verbinde dich mit diesem Netzwerk im WiFi Menü deines Geräts."
|
||||
},
|
||||
"AUTO": {
|
||||
"TITLE": "Wähle deine senseBox",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"CANCEL": "Cancel",
|
||||
"MENU": {
|
||||
"TITLE": "Menu",
|
||||
"ABOUT": "About",
|
||||
|
@ -26,9 +27,16 @@
|
|||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Settings",
|
||||
"LANGUAGE": {
|
||||
"TITLE": "Language"
|
||||
},
|
||||
"LOG_OPTIN": {
|
||||
"TITLE": "Automatically send error reports to us",
|
||||
"TITLE": "Automatically send error reports",
|
||||
"TEXT": "These reports help us in troubleshooting issues.<br/>No personal information is collected, except for your Sketch."
|
||||
},
|
||||
"RESET": {
|
||||
"TITLE": "Reset App",
|
||||
"TEXT": "This will reset your settings and delete your Sketches!"
|
||||
}
|
||||
},
|
||||
"OTAWIZ": {
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
mini
|
||||
color="light"
|
||||
[title]="'BLOCKLY.BTN_CODE' | translate"
|
||||
(click)="toggleView();"
|
||||
(click)="blockly.toggleView()"
|
||||
>
|
||||
<ion-icon name="code"></ion-icon>
|
||||
</button>
|
||||
|
|
|
@ -1,73 +1,70 @@
|
|||
import { Component, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { IonicPage, NavController, NavParams } from 'ionic-angular';
|
||||
import { IonicPage, NavController, NavParams, Platform } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OtaWizardPage } from '../ota-wizard/ota-wizard';
|
||||
import { LoggingProvider } from '../../providers/logging/logging';
|
||||
import { StorageProvider, LASTSKETCH } from '../../providers/storage/storage';
|
||||
import { BlocklyMessageProtocol } from './blockly_protocol';
|
||||
|
||||
@IonicPage()
|
||||
@Component({
|
||||
selector: 'page-blockly',
|
||||
templateUrl: 'blockly.html',
|
||||
})
|
||||
export class BlocklyPage {
|
||||
export class BlocklyPage implements OnInit {
|
||||
@ViewChild('blocklyFrame') blocklyFrame: ElementRef
|
||||
blocklyUrl: SafeResourceUrl
|
||||
blockly: BlocklyMessageProtocol
|
||||
|
||||
private messageHandler: (ev: IframePostMessageEvent) => void
|
||||
private log: LoggingProvider
|
||||
|
||||
constructor(
|
||||
public navCtrl: NavController,
|
||||
public navParams: NavParams,
|
||||
private sanitizer: DomSanitizer,
|
||||
private platform: Platform,
|
||||
private storage: StorageProvider,
|
||||
logger: LoggingProvider,
|
||||
translate: TranslateService,
|
||||
) {
|
||||
this.log = logger.createChild('BlocklyPage')
|
||||
this.blocklyUrl = this.buildBlocklyUrl(translate.currentLang)
|
||||
|
||||
// need to assign it here to keep the function reference for unsubscribing again
|
||||
// and to maintain the this scope properly
|
||||
this.messageHandler = (ev: IframePostMessageEvent) => {
|
||||
const { type, data } = ev.data;
|
||||
switch (type) {
|
||||
case 'sketch':
|
||||
this.log.debug('sketch received, launching ota wizard page', { sketch: data })
|
||||
this.navCtrl.push(OtaWizardPage, { sketch: data })
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', this.messageHandler)
|
||||
}
|
||||
|
||||
ionViewWillUnload () {
|
||||
window.removeEventListener('message', this.messageHandler)
|
||||
async ngOnInit () {
|
||||
// blocklyFrame is available from here on
|
||||
this.blockly = new BlocklyMessageProtocol(this.blocklyFrame, this.log)
|
||||
await this.blockly.ready
|
||||
// load the last sketch
|
||||
const xml = this.storage.get(LASTSKETCH)
|
||||
if (xml) this.blockly.setXml(xml)
|
||||
|
||||
// save state when exiting (on mobile & browsers separately)
|
||||
this.platform.pause.subscribe(() => this.saveBlocklyState())
|
||||
window.addEventListener('beforeunload', () => this.saveBlocklyState())
|
||||
}
|
||||
|
||||
launchOtaWizard () {
|
||||
this.log.debug('clicked launch ota')
|
||||
this.blocklyFrame.nativeElement.contentWindow.postMessage('getSketch', '*')
|
||||
async ionViewCanLeave () {
|
||||
// hold closing the page until we saved the current blockly state
|
||||
await this.saveBlocklyState()
|
||||
return true
|
||||
}
|
||||
|
||||
toggleView () {
|
||||
this.log.debug('clicked toggle view')
|
||||
this.blocklyFrame.nativeElement.contentWindow.postMessage('toggleView', '*')
|
||||
async saveBlocklyState () {
|
||||
const xml = await this.blockly.getXml()
|
||||
this.storage.set(LASTSKETCH, xml)
|
||||
}
|
||||
|
||||
buildBlocklyUrl (lang: string): SafeResourceUrl {
|
||||
async launchOtaWizard () {
|
||||
const sketch = await this.blockly.getSketch()
|
||||
this.navCtrl.push(OtaWizardPage, { sketch })
|
||||
}
|
||||
|
||||
private buildBlocklyUrl (lang: string): SafeResourceUrl {
|
||||
if (!lang) this.log.error('building url with empty language!')
|
||||
const url = `./assets/blockly.html?lang=${lang}`
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
interface IframePostMessageEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'sketch',
|
||||
data: any,
|
||||
}
|
||||
}
|
||||
|
|
96
src/pages/blockly/blockly_protocol.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { ElementRef } from '@angular/core';
|
||||
import { LoggingProvider } from '../../providers/logging/logging';
|
||||
|
||||
/**
|
||||
* this file defines & implements the message passing protocol to communicate
|
||||
* with an iframe that has src/assets/blockly.html loaded.
|
||||
* The underlying postMessage protocol is wrapped into a promise API.
|
||||
*/
|
||||
|
||||
export class BlocklyMessageProtocol {
|
||||
// resolve ready promise once the blocklyFrame is ready
|
||||
ready = new Promise(resolve => {
|
||||
window.addEventListener('message', (ev: IframePostMessageEvent) => {
|
||||
// @HACK @FIXME: timeout is required, as blockly resolves some async functions
|
||||
// after firing the `ready` event..
|
||||
if (ev.data.type === 'ready') setTimeout(resolve, 300)
|
||||
})
|
||||
})
|
||||
|
||||
static reqResPatterns: BlocklyReqPatterns = {
|
||||
'getSketch': 'sketch',
|
||||
'getXml': 'xml',
|
||||
'setXml': null,
|
||||
'toggleView': null,
|
||||
}
|
||||
|
||||
constructor (private blocklyFrame: ElementRef, private log: LoggingProvider) {
|
||||
// set up event listeners for non-request log messages
|
||||
window.addEventListener('message', (ev: IframePostMessageEvent) => {
|
||||
if (ev.data.type === 'log')
|
||||
this.log.warn('log entry from blockly:', ev.data)
|
||||
else
|
||||
this.log.debug(`received ${ev.data.type} message from blockly`, { message: ev.data })
|
||||
})
|
||||
}
|
||||
|
||||
toggleView() { this.sendRequest({ type: 'toggleView' }) }
|
||||
setXml(data: string) { this.sendRequest({ type: 'setXml', data }) }
|
||||
getXml() { return this.sendRequest({ type: 'getXml' }) }
|
||||
getSketch() { return this.sendRequest({ type: 'getSketch' }) }
|
||||
|
||||
private async sendRequest(req: BlocklyRequest): Promise<any> {
|
||||
await this.ready
|
||||
|
||||
if (
|
||||
!this.blocklyFrame ||
|
||||
!this.blocklyFrame.nativeElement ||
|
||||
!this.blocklyFrame.nativeElement.contentWindow
|
||||
) {
|
||||
throw new Error('cannot access blockly frame')
|
||||
}
|
||||
|
||||
const expectResponse = BlocklyMessageProtocol.reqResPatterns[req.type]
|
||||
this.log.debug(`sending ${req.type} message to blockly, expecting response: ${expectResponse}`, { message: req })
|
||||
|
||||
if (!expectResponse)
|
||||
return this.blocklyFrame.nativeElement.contentWindow.postMessage(req, '*')
|
||||
|
||||
// create promise waiting for the response event
|
||||
const resPromise = new Promise<any>((resolve, reject) => {
|
||||
const resHandler = ({ data: res }: IframePostMessageEvent) => {
|
||||
if (expectResponse !== res.type) return
|
||||
window.removeEventListener('message', resHandler)
|
||||
if (res.type === 'error') reject(res.data)
|
||||
else resolve(res.data)
|
||||
}
|
||||
// TODO: promise reject after timeout?
|
||||
window.addEventListener('message', resHandler)
|
||||
})
|
||||
|
||||
// send message *after* registering the response handler!
|
||||
this.blocklyFrame.nativeElement.contentWindow.postMessage(req, '*')
|
||||
return resPromise
|
||||
}
|
||||
}
|
||||
|
||||
interface IframePostMessageEvent extends MessageEvent {
|
||||
data: BlocklyResponse
|
||||
}
|
||||
|
||||
type BlocklyReqPatterns = {
|
||||
[k in BlocklyRequest['type']]: BlocklyResponse['type']
|
||||
}
|
||||
|
||||
type BlocklyRequest =
|
||||
{ type: 'getSketch' } |
|
||||
{ type: 'getXml' } |
|
||||
{ type: 'setXml', data: string } |
|
||||
{ type: 'toggleView' }
|
||||
|
||||
type BlocklyResponse =
|
||||
{ type: 'log', data: any } |
|
||||
{ type: 'error', data: any } |
|
||||
{ type: 'ready', data: undefined } |
|
||||
{ type: 'sketch', data: string } |
|
||||
{ type: 'xml', data: string }
|
|
@ -8,7 +8,21 @@
|
|||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
<ion-list inset>
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-icon name="text" item-start></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="color: black;" translate>SETTINGS.LANGUAGE.TITLE</h2>
|
||||
</ion-label>
|
||||
<ion-select
|
||||
interface="popover"
|
||||
(ngModelChange)="onSettingsChange('language', $event)"
|
||||
[ngModel]="settings.language"
|
||||
>
|
||||
<ion-option *ngFor="let lang of translate.langs" value="{{lang}}">{{ lang }}</ion-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-icon name="megaphone" item-start></ion-icon>
|
||||
<ion-label>
|
||||
|
@ -17,5 +31,13 @@
|
|||
</ion-label>
|
||||
<ion-toggle (ngModelChange)="onSettingsChange('logOptin', $event)" [ngModel]="settings.logOptin"></ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<button ion-item (click)="resetStorage()">
|
||||
<ion-icon name="refresh" item-start></ion-icon>
|
||||
<ion-label>
|
||||
<h2 translate>SETTINGS.RESET.TITLE</h2>
|
||||
</ion-label>
|
||||
</button>
|
||||
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
page-settings {
|
||||
|
||||
.error {
|
||||
color: red !important;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { IonicPage } from 'ionic-angular';
|
||||
import { IonicPage, ActionSheetController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { StorageProvider, SETTINGS } from '../../providers/storage/storage';
|
||||
import { DEFAULT_LANG } from '../../constants';
|
||||
|
||||
|
||||
@IonicPage()
|
||||
|
@ -10,14 +12,46 @@ import { StorageProvider, SETTINGS } from '../../providers/storage/storage';
|
|||
templateUrl: 'settings.html',
|
||||
})
|
||||
export class SettingsPage {
|
||||
settings = {}
|
||||
settings = {} as any
|
||||
|
||||
constructor(private storage: StorageProvider) {
|
||||
constructor(
|
||||
public actionCtrl: ActionSheetController,
|
||||
public translate: TranslateService,
|
||||
private storage: StorageProvider,
|
||||
) {
|
||||
this.settings = this.storage.get(SETTINGS)
|
||||
this.settings.language = this.translate.currentLang
|
||||
}
|
||||
|
||||
onSettingsChange (name, value) {
|
||||
this.settings[name] = value
|
||||
this.storage.set(SETTINGS, this.settings)
|
||||
if (name === 'language')
|
||||
this.translate.use(value)
|
||||
}
|
||||
|
||||
resetStorage() {
|
||||
const handler = () => {
|
||||
this.storage.reset()
|
||||
// update UI state & reset language
|
||||
this.settings = this.storage.get(SETTINGS)
|
||||
const lang = this.getPreferredLanguage()
|
||||
this.storage.get(SETTINGS).language = lang
|
||||
this.translate.use(lang)
|
||||
}
|
||||
|
||||
this.actionCtrl.create({
|
||||
title: this.translate.instant('SETTINGS.RESET.TEXT'),
|
||||
buttons: [
|
||||
{ text: this.translate.instant('CANCEL') },
|
||||
{ text: this.translate.instant('SETTINGS.RESET.TITLE'), cssClass: 'error', handler },
|
||||
]
|
||||
}).present()
|
||||
}
|
||||
|
||||
private getPreferredLanguage () {
|
||||
const langsAvailable = this.translate.getLangs()
|
||||
const lang = this.storage.get(SETTINGS).language || this.translate.getBrowserLang()
|
||||
return langsAvailable.indexOf(lang) === -1 ? DEFAULT_LANG : lang
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,13 +30,16 @@ export class LoggingProvider {
|
|||
private storage: StorageProvider,
|
||||
private translate: TranslateService,
|
||||
) {
|
||||
const { defaultFields } = this
|
||||
defaultFields.appVersion = APP_VERSION
|
||||
defaultFields.platform = this.plt.platforms().join(' ')
|
||||
defaultFields.platformVersion = this.plt.version().str
|
||||
defaultFields.lang = translate.currentLang
|
||||
if (!this.defaultFields.appVersion) {
|
||||
this.defaultFields.appVersion = APP_VERSION
|
||||
this.defaultFields.platform = this.plt.platforms().join(' ')
|
||||
this.defaultFields.platformVersion = this.plt.version().str
|
||||
this.defaultFields.lang = translate.currentLang
|
||||
}
|
||||
}
|
||||
|
||||
// construct a new logger with a new component name and default fields,
|
||||
// inheriting existing default fields
|
||||
createChild (component: string, defaultFields: object = {}) {
|
||||
const child = new LoggingProvider(this.http, this.plt, this.storage, this.translate)
|
||||
Object.assign(child.defaultFields, defaultFields, { component })
|
||||
|
@ -71,6 +74,7 @@ export class LoggingProvider {
|
|||
const logentry = { } as LogMessage
|
||||
let msg = ''
|
||||
|
||||
// fields can be strings, which are appended to the message; or objects, which are merged.
|
||||
for (const param of fields) {
|
||||
if (typeof param === 'object')
|
||||
Object.assign(logentry, param)
|
||||
|
@ -81,8 +85,10 @@ export class LoggingProvider {
|
|||
if (msg)
|
||||
logentry.msg = msg
|
||||
|
||||
// add the static and dynamic default fields
|
||||
Object.assign(logentry, this.defaultFields, {
|
||||
time: Date.now(),
|
||||
time: new Date().toISOString(),
|
||||
levelText: LogLevel[level],
|
||||
level,
|
||||
})
|
||||
|
||||
|
|
|
@ -29,12 +29,10 @@ export class OtaWifiProvider {
|
|||
return WifiStrategy.Manual
|
||||
}
|
||||
|
||||
if (
|
||||
this.platform.is('android') ||
|
||||
this.platform.is('ios') && this.platform.version().major >= 11
|
||||
) {
|
||||
// FIXME: iOS 11+ supposedly allows WiFi API queries (see WifiWizard2 docs),
|
||||
// but testing in emulator gives "not supported". might be an emulator issue?
|
||||
if (this.platform.is('android'))
|
||||
return WifiStrategy.Automatic
|
||||
}
|
||||
|
||||
return WifiStrategy.Manual
|
||||
}
|
||||
|
@ -83,7 +81,7 @@ export class OtaWifiProvider {
|
|||
}
|
||||
|
||||
export enum WifiStrategy {
|
||||
Automatic = 'Automatic', // android, iOS 11+
|
||||
Manual = 'Manual', // older iOS
|
||||
Unavailable = 'Unavailable', // browser
|
||||
Automatic = 'Automatic', // android
|
||||
Manual = 'Manual', // iOS, browser
|
||||
Unavailable = 'Unavailable', // currently unused
|
||||
}
|
||||
|
|
|
@ -1,33 +1,41 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
export const SETTINGS = 'appsettings'
|
||||
export const LASTSKETCH = 'lastsketch'
|
||||
|
||||
@Injectable()
|
||||
export class StorageProvider {
|
||||
private cache: Map<string, any> = new Map()
|
||||
|
||||
constructor () {
|
||||
this.registerKey(SETTINGS, {
|
||||
logOptin: false,
|
||||
})
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
// set up default values
|
||||
this.registerKey(SETTINGS, { logOptin: false })
|
||||
this.registerKey(LASTSKETCH, '')
|
||||
}
|
||||
|
||||
registerKey (key, defaultValue) {
|
||||
const stored = localStorage.getItem(key)
|
||||
if (!stored) {
|
||||
localStorage.setItem(key, JSON.stringify(defaultValue))
|
||||
this.cache[key] = defaultValue
|
||||
if (stored === null) {
|
||||
this.set(key, defaultValue)
|
||||
} else {
|
||||
this.cache[key] = JSON.parse(stored)
|
||||
}
|
||||
}
|
||||
|
||||
get (key) {
|
||||
return this.cache[key]
|
||||
}
|
||||
get (key) { return this.cache[key] }
|
||||
|
||||
set (key, value) {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
this.cache[key] = value
|
||||
}
|
||||
|
||||
reset () {
|
||||
localStorage.clear()
|
||||
this.cache = new Map()
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,10 @@ const requestHandler = (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
const fileStream = fs.createWriteStream(logfile, 'utf-8')
|
||||
const fileStream = fs.createWriteStream(logfile, {
|
||||
encoding: 'utf-8',
|
||||
flags: 'a', // append
|
||||
})
|
||||
const server = http.createServer(requestHandler)
|
||||
|
||||
server.listen(port, err => {
|
||||
|
|