
Vor fast einem Jahr präsentierte ich mein Lieblingsprojekt - die ThingJS IoT-Plattform. Ehrlich gesagt habe ich nicht alle Ziele erreicht, die ich mir durch die Veröffentlichung dieses Artikels gesetzt habe. Aber die Arbeit hat sich gelohnt. Ich habe etwas anderes bekommen - nützliche Kritik.
Ich habe vergangene Erfahrungen berücksichtigt. Theorie geht ohne Übung schlecht. Dieses Mal basiert die Präsentation auf einer angewandten Lösung. Jeder kann es „berühren“ und im Alltag verwenden.
Aber zuerst einige allgemeine Informationen.
Einführung
ThingJS unterscheidet sich von anderen Plattformen durch eine Architektur, mit der Sie Geräte vom Hobby- bis zum professionellen Level erstellen können.
In nur wenigen Minuten können Sie ein Haushaltsgerät für Ihre Bedürfnisse erstellen. Zu diesem Zweck verfügt ein Amateur über vorgefertigte Anwendungsvorlagen, die Möglichkeit, den Code eines anderen wiederzuverwenden, und die Integration mit den meisten IoT-Diensten über MQTT.
Der Fachmann hat die Möglichkeit, die Firmware zu ändern und so zuverlässige Geräte herzustellen. Entwickeln Sie eine hochwertige Benutzeroberfläche, die auf modernen WEB-Technologien basiert. Fertige Geräte herstellen.
, “” . — .
, . (core) (Resource Interfaces). ( ), “” UI . , , .
. IDE. : Lion, CMake, webpack, npm .. ThingJS. TDD .
“” , . “ ” . “ ”. .
runtime ESP32. .
. backend frontend — . :
- ();
- Frontend ();
- Backend ();
- ();
- ();
- ();
- ().
Backend . JavaScript — mJS.
Frontend SPA WEB-. . : UBUS Storage.

UBUS
. JSON-. . . .
, . . frontend, backend, , , .
. frontend WEBSocket.
, frontend , . — . backend.
.
, . , , .
Storage
. . frontend, backend.
. , . , . , .
. , , . .
. , . .
. , . . , .
API
API. , $res $bus.
API . , , (Resource interfaces).
Resource interfaces
, . .. , . . , launcher, vuetifyjs React.
. . , FreeRTOS , . SmartLED.
, . , . . , . .
. .
:
- ;
- .
. . , GPIO . , .
. .
. , .
, , . , .
. , , GPIO, , UART .. . , TCP .. , , .
, , , ThingJS.
ThingJS
. , . , , . .
ESP32 ESP8266. , , . 240, , 520, Bluetooth, Wifi. GPIO. : ES, SHA-2, RSA, ECC, RNG.

JavaScript VUE CLI. , frontend .
:
- — .
- (hot reload) — . . .
- dev — dev- NodeJS. , . .
:
git clone --branch beta https://github.com/rpiontik/ThingJS-front
cd ThingJS-front
npm install
, dev :
npm run dev
http://0.0.0.0:8080/. dev-. .

100%, . , . . , /config/dev.env.js IP .
, dev- “” -, , . .. “”, , , thermostat.smt.
dev thermostat . .

, src/applications/thermostat/scripts/thermostat.js. , “debugger”. .
, dev- :

“Start debugger” .

. watch . . .
, :
npm run build
dist/apps/
Thermostat
. /src/applications/. “blink”. — thermostat.
. manifest.json. . .
"name": "Thermostat",
"vendor": "rpiontik",
"version": 1,
"subversion": 0,
"patch": 0,
"description": {
"ru": "",
"en": "Thermostat"
},
, .
components
frontend .
"components": {
"thermostat-app": {
"source": "thermostat.js",
"intent_filter": [
{
"action": "thingjs.intent.action.MAIN",
"category": "thingjs.intent.category.LAUNCH"
}
]
}
},
— “thermostat-app”. “thermostat.js”. “intent_filter”. .
“blink.js” -> “thermostat.js” :
import App from './Thermostat.vue';
import Langs from './langs';
$includeLang(Langs);
$exportComponent('thermostat-app', App);
VUE “Thermostat.vue”. . . :
$exportComponent('thermostat-app', App);
? . . , “thermostat.js” VUE “thermostat-app”. . “langs.js”.
frontend “Thermostat.vue”. . .
<template>
<v-flex fill-height style="max-width: 600px">
<h1>{{ 'TITLE'|lang }}</h1>
<v-container>
<v-layout>
<v-flex xs12 md12>
{{ 'DESCRIPTION'|lang }}
</v-flex>
</v-layout>
</v-container>
<v-tabs
centered
icons-and-text
>
<v-tab href="#tab-1">
{{ 'CONTROL'|lang }}
<v-icon>dashboard</v-icon>
</v-tab>
<v-tab href="#tab-2">
{{ 'CLOUD'|lang }}
<v-icon>cloud</v-icon>
</v-tab>
<v-tab-item value="tab-1">
<v-container>
<v-layout>
<v-flex class="current-temp" xs12 md4>
<span>
<template v-if="state.temp !== null">
{{ state.temp.toFixed(1) }}°
</template>
<template v-else>
--.--
</template>
</span>
</v-flex>
<v-flex xs12 md4 style="text-align: center; padding: 12px; ">
<template v-if="state.state === 1">
<v-icon
title="Power on"
class="indicator"
>power
</v-icon>
</template>
<template v-else-if="state.state === 0">
<v-icon
title="Power off"
class="indicator"
>power_off
</v-icon>
</template>
</v-flex>
<v-flex xs12 md4 style="text-align: center; padding: 12px;">
<template v-if="!!state.connected">
<v-icon
title="Connected"
class="indicator"
>cloud
</v-icon>
</template>
<template v-else>
<v-icon
title="Disconnected"
class="indicator"
>cloud_off
</v-icon>
</template>
</v-flex>
</v-layout>
</v-container>
<v-container grid-list-xl>
<v-layout>
<v-flex xs12 md3>
<v-select
label="Mode"
:items="modes"
v-model="state.mode"
@change="onChangeMode"
></v-select>
</v-flex>
<v-flex xs12 md9>
<v-slider v-if="state.mode <= 1"
thumb-label="always"
v-model="state.target"
:disabled="!state.target"
@change="onChangeTarget"
></v-slider>
</v-flex>
</v-layout>
</v-container>
</v-tab-item>
<v-tab-item value="tab-2">
<v-container>
<p>
Android applications:
<ul>
<li><a href="https://play.google.com/store/apps/details?id=net.routix.mqttdash" target="_blank">MQTT Dash (RUS)</a></li>
<li><a href="https://play.google.com/store/apps/details?id=snr.lab.iotmqttpanel.prod" target="_blank">IoT MQTT Panel (EN)</a></li>
</ul>
</p>
<p>
Server params:
<ul>
<li>Address: mqtt.eclipse.org</li>
<li>port: 1883</li>
</ul>
</p>
<table class="topic-table">
<tr>
<th>{{ 'TOPIC'|lang }}</th>
<th>{{ 'TOPIC_DESCRIPTION'|lang }}</th>
</tr>
<tr>
<td>/thingjs/{{ state.chip_id }}/temp</td>
<td>{{ 'TOPIC_TEMP_DESC'|lang }}</td>
</tr>
<tr>
<td>/thingjs/{{ state.chip_id }}/state</td>
<td>{{ 'TOPIC_STATE_DESC'|lang }}</td>
</tr>
<tr>
<td>/thingjs/{{ state.chip_id }}/target/out</td>
<td>{{ 'TOPIC_TARGET_OUT'|lang }}</td>
</tr>
<tr>
<td>/thingjs/{{ state.chip_id }}/target/in</td>
<td>{{ 'TOPIC_TARGET_IN'|lang }}</td>
</tr>
<tr>
<td>/thingjs/{{ state.chip_id }}/mode/out</td>
<td>{{ 'TOPIC_MODE_OUT'|lang }}</td>
</tr>
<tr>
<td>/thingjs/{{ state.chip_id }}/mode/in</td>
<td>{{ 'TOPIC_MODE_IN'|lang }}</td>
</tr>
</table>
</v-container>
</v-tab-item>
</v-tabs>
</v-flex>
</template>data () {
return {
modes: [ // .
{ text: 'Less then', value: 0 },
{ text: 'More then', value: 1 },
{ text: 'On', value: 2 },
{ text: 'Off', value: 3 }
],
isHold: false, // . , .
state: { //
connected: null, // MQTT
mode: null, //
target: null, //
temp: null, //
state: null, // (/)
chip_id: null // MQTT .
}
};
}
:
isHold — . , . . , . , . “” .
chip_id — . MQTT .
mounted () {
this.$bus.$on($consts.EVENTS.UBUS_MESSAGE, (type, data) => {
if (this.isHold) return;
switch (type) {
case 'thermostat-state':
this.state = JSON.parse(data);
break;
}
});
this.refreshState();
},
. “thermostat-state”. . “isHold” .
refreshState () {
this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-refresh-state');
},
. .
flushData () {
if (this.isHold) { clearTimeout(this.isHold); }
this.isHold = setTimeout(() => {
this.isHold = null;
this.refreshState();
}, 1000);
},
. , .
onChangeTarget (val) {
this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-set-target', val);
this.flushData();
},
onChangeMode (val) {
this.$bus.$emit($consts.EVENTS.UBUS_MESSAGE, 'tmst-set-mode', val);
this.flushData();
}
.
- frontend. “” .
requires
, . “requires”.
"requires": {
"interfaces": {
"mqtt": {
"type": "mqttc",
"required": true
},
"timers": {
"type": "timers",
"required": true,
"description": {
"ru": " ",
"en": "System timers"
}
},
"ds18x20": {
"type": "DS18X20",
"required": true
},
"relay": {
"type": "bit_port",
"required": true,
"default": 2,
"description": {
"ru": "",
"en": "Relay"
}
},
"sys_info": {
"type": "sys_info",
"required": true,
"description": {
"ru": " ",
"en": "System information"
}
}
}
}
:
- mqttc — MQTT . .
- timers — . .
- DS18X20 — OneWire .
- bit_port — . .
- sys_info — . .
, . . , . "required" . .
. , “DS18X20”. , OneWire.
. . . , , “relay”, “type”. “relay” “bit_port”.
scripts
.
"scripts": {
"entry": "thermostat",
"subscriptions": ["tmst-refresh-state", "tmst-set-target", "tmst-set-mode"],
"modules": {
"thermostat": {
"hot_reload": true,
"source": "scripts/thermostat.js",
"optimize": false
}
}
},
- entry — . . . .
- subscriptions — . - . , “”. .
- modules — .
- thermostat — .
- hot_reload — “” . true, . . dev-.
- source — .
- optimize — true, webpack.
“scripts/blink.js” “scripts/thermostat.js”. .
let MQTT_SERVER = 'wss://mqtt.eclipse.org:443/mqtt';
MQTT . . . . MQTT , .
let CHIP_ID = $res.sys_info.chip_id;
. $res. . sys_info . MQTT .
let TOPIC_TEMP = '/thingjs/' + CHIP_ID + '/temp';
let TOPIC_TARGET_OUT = '/thingjs/' + CHIP_ID + '/target/out';
let TOPIC_TARGET_IN = '/thingjs/' + CHIP_ID + '/target/in';
let TOPIC_MODE_OUT = '/thingjs/' + CHIP_ID + '/mode/out';
let TOPIC_MODE_IN = '/thingjs/' + CHIP_ID + '/mode/in';
let TOPIC_MODE_STATE = '/thingjs/' + CHIP_ID + '/state';
MQTT . “out” “in”. . out — , in — .
:
// . .
let MODE_LESS = 0;
// . .
let MODE_MORE = 1;
// .
let MODE_ON = 2;
// .
let MODE_OFF = 3;
:
// MQTT
let isConnected = false;
//
let mode = MODE_LESS;
//
let target = 32;
//
let state = 0;
//
let sensor = null;
//
let temp = null;
// , . . “” .
let fakeVector = 0.5;
. OneWire. sensor .
$res.ds18x20.search(function (addr) {
if (sensor === null) {
sensor = addr;
}
});
function publishState () {
$bus.emit('thermostat-state', JSON.stringify({
connected: isConnected,
mode: mode,
target: target,
temp: temp,
state: state,
chip_id: CHIP_ID
}));
if (isConnected) {
$res.mqtt.publish(TOPIC_MODE_OUT, JSON.stringify(mode));
$res.mqtt.publish(TOPIC_TARGET_OUT, JSON.stringify(target));
$res.mqtt.publish(TOPIC_MODE_STATE, JSON.stringify(state));
$res.mqtt.publish(TOPIC_TEMP, JSON.stringify(temp));
}
}
publishState :
- UBUS. . . , . , frontend. frontend.
- MQTT . . .
MQTT :
//
$res.mqtt.onconnected = function () {
print('MQTT client is connected');
isConnected = true;
$res.mqtt.subscribe(TOPIC_TARGET_IN);
$res.mqtt.subscribe(TOPIC_MODE_IN);
publishState();
};
// online
$res.mqtt.disconnected = function () {
print('MQTT client is disconnected');
isConnected = false;
publishState();
};
// MQTT
$res.mqtt.ondata = function (topic, data) {
print('MQTT client received from topic [', topic, '] with data [', data, ']');
if (topic === TOPIC_TARGET_IN) {
target = JSON.parse(data);
} else if (topic === TOPIC_MODE_IN) {
mode = JSON.parse(data);
}
};
UBUS. , .
$bus.on(function (event, data) {
if (event === 'tmst-set-target') {
target = JSON.parse(data);
} else if (event === 'tmst-set-mode') {
mode = JSON.parse(data);
}
publishState();
}, null);
.
$res.timers.setInterval(function () {
if (sensor !== null) {
$res.ds18x20.convert_all();
temp = $res.ds18x20.get_temp_c(sensor);
} else { // Fake temperature
if (temp > 99) {
fakeVector = -0.5;
} else if (temp < 1) {
fakeVector = 0.5;
}
temp += fakeVector;
}
// Refresh sensor data
if (mode === MODE_ON) {
state = 1;
} else if (mode === MODE_OFF) {
state = 0;
} else if (mode === MODE_LESS) {
if (temp < target) {
state = 1;
} else {
state = 0;
}
} else if (mode === MODE_MORE) {
if (temp > target) {
state = 1;
} else {
state = 0;
}
}
publishState();
//
$res.relay.set(!state);
}, 1000);
:
// .
temp = 34.5;
// GPIO
$res.relay.direction($res.relay.DIR_MODE_OUTPUT);
//
publishState();
// MQTT .
$res.mqtt.connect(MQTT_SERVER);
. VUE . :
<h1>{{ 'TITLE'|lang }}</h1>
langs.js frontend .
favicon
. favicon.svg .
. .. /src/applications/. npm
npm run build
smt . thermostat.smt Thermostat. /dist/apps/

. WEB. “” “” “ ”. thermostat.smt.

, . . , , MQTT . . , OneWire UART GPIO . , GPIO .
default . , “”. .
:
- . .
- . .
- . , .
. , .
. .
: MQTT. , MQTT.

. . .
. .

Android. MQTT Dash.
, . IP . .
. MQTT .

. . . .

. — /thingjs/TJS-030BE4/temp .
. , — .
().

. . out in .
, , .
!
- IoT. , , .
, .
. - , . , . , .
.
?
- ;
- ;
- , ;
- Installieren von Anwendungen auf Mobilgeräten;
- Entwicklung und Veröffentlichung des ThingJS-Entwicklungskits für Amateure;
- Integration in beliebte IoT-Ökosysteme;
- Implementierung der Sprachsteuerung.
Links
ThingJS-Projektressourcen:
ThingJS-Projekt-Repositories:
Wo die Plattform bereits genutzt wird:
Wo wird verwendet:
Verwendete Projekte: