From 54bc04c884e80b69f64995c36d83e914db5d2229 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Mon, 6 Apr 2020 23:51:32 +0900 Subject: [PATCH 01/42] added mesh extension skelton. --- src/extension-support/extension-manager.js | 3 +- src/extensions/scratch3_mesh/index.js | 62 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/extensions/scratch3_mesh/index.js diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 7cb556c5d0d..a01d1334455 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -23,7 +23,8 @@ const builtinExtensions = { ev3: () => require('../extensions/scratch3_ev3'), makeymakey: () => require('../extensions/scratch3_makeymakey'), boost: () => require('../extensions/scratch3_boost'), - gdxfor: () => require('../extensions/scratch3_gdx_for') + gdxfor: () => require('../extensions/scratch3_gdx_for'), + mesh: () => require('../extensions/scratch3_mesh') }; /** diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js new file mode 100644 index 00000000000..d02fc7273fc --- /dev/null +++ b/src/extensions/scratch3_mesh/index.js @@ -0,0 +1,62 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const formatMessage = require('format-message'); + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV8/tCIVBzsUccjQOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi5Oik6CIl/i8ptIjx4Lgf7+497t4B/maVqWZwHFA1y8ikkkIuvyqEXhFEL8KIIi4xU58TxTQ8x9c9fHy9S/As73N/jgGlYDLAJxDPMt2wiDeIpzctnfM+cYSVJYX4nHjMoAsSP3JddvmNc8lhP8+MGNnMPHGEWCh1sdzFrGyoxFPEMUXVKN+fc1nhvMVZrdZZ+578heGCtrLMdZojSGERSxAhQEYdFVRhIUGrRoqJDO0nPfzDjl8kl0yuChg5FlCDCsnxg//B727N4uSEmxROAj0vtv0RB0K7QKth29/Htt06AQLPwJXW8deawMwn6Y2OFjsCBreBi+uOJu8BlztA9EmXDMmRAjT9xSLwfkbflAeGboH+Nbe39j5OH4AsdZW+AQ4OgdESZa97vLuvu7d/z7T7+wE5SnKQf0E1gwAAAAlwSFlzAAAN1wAADdcBQiibeAAAAAd0SU1FB+QEBg0qK1rlrBMAAAx1SURBVFjDtZh7kBXVncc/p7tv933P+wEMzPAcnoM8ZhiZCFHKOEFB1ggGXGNkddcyVVbWdbXWLS2TzSbZSu3W7hpTWuKKpaLGB8Ia1gpgcACB8BgY5CHPmWGcB3de931v3+4++8dc8DoZEaikq351uvre0/Wp7zn9O7/fV/DnucQ3/C7/Ui++1rniKsDkXxpQjHAvvgFQjgB3VaDadYDljpdCGfbsEsDXxVUrKq4DTskZFUDNuR8J0AHs7HgpZM54RUjtGsBEDpCanasBrux46XkuoJ2NDGDlhJ0DfcVlF9egmgpiCGjUAqNo8m154fLZUyzPmG+heeYj9KoyIfJcEgYdJx5zMm1Y4X10f7aLY39oJ3M8Ru9nKRBpkGYWeiRQeTWAuXAqQlGR0gXS8K58uyKhTLwZ4b1zdqkxvypf9SVSku1pWFHtYXaFTrEBsbBFa0+Gd5qj/b1xs5lI91avOHkgseXRDiABIgkyPQz0TyDFN8MJDYkO0sOqfY2k8/5m/ARP/TO3BbSqcW4KAiq2A719JnvOpOhNSZbN9FBc5OJEZ4YXd4R5pN7Hul1xc/vByFFof5sPGrcBUSAGJAEzZ+m/Avl1gLlwBkgvaw4/OrEo+ODMKndR9VidexuCdPZn6I/aOI7EoyuoLoWuPpNHXutj7jQPhyI2ZCSfPTYavyrZfjjOExv7w3393e/ydsPLwCAQHlKU9DDIEQG//BiEoiGlAdLHAyeerS7wPvDU3QVa64UUv9odZ3xAoR9BoU8hT1fwagINSUd7ipUNQe5bnIeZkbzQFOHIqSSNtX6kAwlT8uqOsHM+FPqAN+r+E+gDMSgEcSnlJchLe1JqX7u0Q3vOoz54/FFbuNfOmKSrL38SYWDQYtl0D08vLyDPq+LWIJmWtPVlCMUdnnjPonq0Tp5XIZJweOr2Aor3x5GH41SXu2gPZdBcQsEMrhCrm2LyzUXrQMply5azefNmZ1i+FNoICiogVJCG96FDtyVi7gdrKlBnFqrcems+sZjN+oNxgobCZ+cSvPDHJO19GeoLFCaVu6gp1di4M8yEUheFQY2uXhOEpL7K4P5FQVy6wt4zKf6wL6K8dFBdwR3vtPLhyo9aW1ut7F7MzZVSHSHPaSD04OrXK2Nm5T/jd0196YFiFs/yk+9VUTXBroMxms6kOfKFyfdnufnhoiD3LM4nP6iy9/MkBR6Fj86ZbD0U4xf7E0QTDrUVBlua4xhCcvMMH7OneBjtUYyPjyqF/qDv2IXmTdGcj+Vy2lFGUFAVZXONiFmxoLJUry2sUJlU5sLMSExLcvJCmt82J9F0wX/fV0pDjZ/JZTqHzyep+a9uvlfr46f3lfHivcXUFKvcNdFg2w+L2HvB5LuzfWw6EGfdx2H8hkLjPD+N84qnxQILF5I3LR/wAXpuwldGSshSm+3DW3j7qrqAu86vkjLBqwuazyZ5fmeU9Y+Usft0ioGUQ8BQ6BiweWZDiPfuKaRxbgDbkfjdCrOqfey5YHJLjZ8fLfDSdDzBk3cV8b9H4qxvilBRrLF0lkfDU7moonR8URbQyJ5MCiCUEZbYRc2tVYtHeeu/NcNLhUsSSTn0R23WbArzs2UFrL4xwPfmeHn89RCKKtjxx0EWTPeyfF6AhCmxHbAdCPpVzkdsUhnJzTV+bEfSc9Hk5YfKeHd/jFc/jfJpewbyg5M7xi+ZfCVAvrIHSyoXTyxzl0wZpVNR7GLnySSPf9DPK40Bpo01CIUtfry0kKqAwu2/7mZnp82TSwtwpBx6gQouFXR1KN/aEtwuwZob/azdHmVCkcYzf1XEk78OUTfZw9/WBdz4p90AeAF39nxXR1JwqAjQ/A3FhRpel6C+xsdPtgxSW6rRWBsknnJIZUBT4NnVJWxrSeJNWHRHHXRVkOdVCLgVXKogZUrKfCoel0BKmDvZx/3lKpv2RWg6GmNxvYdyv8KYMhd4i6qzgLkKKiMr6Lgm6LpAEaDrCgVxG+FS8OpD21UREk0RfN5l8vAcg6UNQR7bEOLfN/ZyosvE0Ib+F0s6fLvKQBXgcglOd5t0Xszw128OsGSml1+uKmLLqTQ9GUGp7i3NwrlySji0YYXnkIqOCETDFlLA3pY4d34nj9+dSHLTlAQNU9ykLYlLg3/cOMDDN/pZXhtg8QwfLzZFuOU3PczQoWaCm5NtKQak4NHXQhw+myKqCmaN0alOw8JpPgbjNpPzFJpDFhLNN6x0E8MT9ZeQCqKjJ8Op7gzH21P8eEURd852+PnGXp57oJSasW6a21Mk+0y+O28UUkLQo/BEYz5PNOZzJmSx/3yKQ8cTLK7xMb3C4K66AHPG6fQnHJb/RyehiI3HJZgyWuc3hxL0CUmOcmIkwMuXosvI8bQofm9/lC5FIc+rUl7gYvXCAP+2qZ9frSlhfVOEexqCBHWIphxUBQxNQQiYVKJRGvDy4YEYv1hVjKrAYMLBtCSZ7DkRNx2CbpXCPI3iqEWPtJLfVFFfPgN1mWkfXaRNOHAqyU3VHoSASNLmjnkB8twKT7/bx2stSdp/WXl5suWAYzoIAT5D4XhripBQsGyH3qgkbjpoqsC2JVKAzxCkM5JPWuIcCzvoeqrX/GrxKgGpjNDQ2KlE9PA4FyytdtOfHHqx5YBpSZbVBqgfb0DMYd2HvRxqSxNwKwTdCqoqEAhsCWc7Utw33SDjgOlIQKApkLElbkOhPy75+ft9dCUlS6tc2OmL53JaAzsXMPcaanAS5/e29aTjrQnJ+r1xPjwSJ+hWcBxJ2pZ0hUw2PFLCtIke5j3fw9oXumk6lUQRAq8hiCYctrRbLKp2Y2iCgFuh0KdQ4NPoHLQZ+CLN373SQ814g3vr/USSVtpuO3IyWxNmctqAy0ssc6oIa0rbttMf+2e1+Ezlxp/dWcALW8M4Dtw1389A3OFwp8XqW9xMKXexcIqHd/ZF+acNvZy34bbJbqYWqPT0W3SEHTqiKb4YsDneneHTtjRbj8SZNdbgxR+UUFWosXl3mF2fX2zl8Ja2YYWrAzjD96ANZE51nYgpld2/j/cbtbNWFmp1PyjhX38bwko7zBync1FTyPcqdIZtNAUeXpLP/YvzONOd4Vxnins3DvLQKJWNu8Ik0g5uTVCcr7G2zk++7bBsQYDxRRptFy3e2he1iZ1tFlpXRA4BpnJUlNoIPaxF176kP35kV0QvP9x0Oj3/H5Z5+Mn3S9i0N8pju6PcMVrH0BXISBKmQ8K00DXBrAqdcYUqSw8k+Pt7iigJasRNiWCo2Ojqt3guLamtMkiZkq1HEuw43HfeH9vTEhs8FwbiwxT8Sj2YkwuFkj67NcOM5Zk93f4FN1QY7jlVBjPGGVSZFs/vi6PHLQyfyphCF+7sUaZrgqff72daocpN070MJhxSpoNpSyxbsuNIjKllLirLDF7dPshPtwzGiTRvMZueagEuAgPZRuoypDqCjfFlnHzjIhNX+Tae4IaFkzyiulznTKfJrVM9WG6VH20e4NixOBmgssTFyW6L1z+J8C9rSvC4FBwJuirw6AqRhM2zv49g2/DaRwNs2BN3FNp3yA9WbMvC9WWbqGR2iR1AihG7OdBBeEDmAyXqyt2PGwXlK9atLVV6u1MEgi6+M8dHKOaw40SC7S0JDg7YdLammTfRYHmtH5cikEiiKUln2Gbb6SRlSDxBjYOn0o5pdnwq31j8FtCVjVAOoHlpD6pX08TL4//Tkplwt/beMTFhV8gxbq7SGVOsoyqCmrEGy+sCWIMZxuWr3F0fYLA/QyJqoZgOeYbgaE+GfNPh27P9nD4djV8Ind3BW0vezyoXAvqBSI56l/tj9Sr8vaHMfvyVk1QuCZn9nqJ4Wiv0KlKRqsClq+w9EWd/a5qnVxZTN8nL3Eke6qb6mDXRg7Qlb+6M0p3G/r8jF891ntq/hd/dvS0HrjenNzaHN+9XbtyH8qQBwgsyD8gP3vLshIh7bh2eUQ11lflTp5YbvuZOm/kzPDRMcpPKSOIJh3DEIhQyeeloIk14sA2r/RAdTS00P9eRVaz/ehr3P+2PhyB1wAP4gaAIjs+TrhlFTKyvYtzsufjLZhaqnjJha27HdkRCWsm0leglHTqvDRw9qX2xtz3VcWCAZFcku5Th7Hhd1sdIkGq2VjOyoN5s/3ApPMNKdXJst3RWoUQ2z12K5LCj7arNoyvYb5f9QD0Lmxt6TqnO5aT/JWQqO6aziuUWBtdsv12LganlKPd1BqY1zMQc7gvK6zEw/5wW8HAb+Kot4P8HVeGw2VaOZdIAAAAASUVORK5CYII='; + + + +/** + * Host for the Mesh-related blocks + * @param {Runtime} runtime - the runtime instantiating this block package. + * @constructor + */ +class Scratch3MeshBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'mesh', + name: formatMessage({ + id: 'mesh.categoryName', + default: 'Mesh', + description: 'Label for the mesh extension category' + }), + blockIconURI: blockIconURI, + blocks: [ + { + opcode: 'getSensorValue', + text: formatMessage({ + id: 'mesh.sensorValue', + default: 'sensor value', + description: 'Any global variables from other projects' + }), + blockType: BlockType.REPORTER + } + ] + }; + } + + /** + * @return {number} - the global variable's value from other projects + */ + getSensorValue () { + return 1; + } +} + +module.exports = Scratch3MeshBlocks; From 0b01afff2ff836da99cbec0a9469b31618b31c76 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Wed, 8 Apr 2020 01:10:54 +0900 Subject: [PATCH 02/42] select variable name. --- src/extensions/scratch3_mesh/index.js | 55 ++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index d02fc7273fc..a0257c64280 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -23,6 +23,22 @@ class Scratch3MeshBlocks { * @type {Runtime} */ this.runtime = runtime; + + /** + * Host and clients global variable name and value pair. + * @type {Object.} + */ + this.variables = {}; + + /** + * Host and clients global variable names + * @type {string[]} + */ + this.variableNames = []; + + // FIXME: for debug + this._setVariable('var1', 10); + this._setVariable('var2', 20); } /** @@ -42,20 +58,47 @@ class Scratch3MeshBlocks { opcode: 'getSensorValue', text: formatMessage({ id: 'mesh.sensorValue', - default: 'sensor value', + default: '[NAME] sensor value', description: 'Any global variables from other projects' }), - blockType: BlockType.REPORTER + blockType: BlockType.REPORTER, + arguments: { + NAME: { + type: ArgumentType.STRING, + menu: 'variableNames', + defaultValue: '' + } + } + } + ], + menus: { + variableNames: { + acceptReporters: true, + items: '_getVariableNamesMenuItems' } - ] + } }; } /** - * @return {number} - the global variable's value from other projects + * @return {Object} - the global variable's value from other projects */ - getSensorValue () { - return 1; + getSensorValue (args) { + if (!this.variableNames.includes(args.NAME)) { + return ''; + } + return this.variables[args.NAME]; + } + + _setVariable (name, value) { + if (!this.variableNames.includes(name)) { + this.variableNames.push(name); + } + this.variables[name] = value; + } + + _getVariableNamesMenuItems () { + return [''].concat(this.variableNames); } } From c7192e9d4c77688d164f6dd2d58ec87190871bed Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 11 Apr 2020 10:48:26 +0900 Subject: [PATCH 03/42] connect WebRTC and send broadcast handly. --- src/extensions/scratch3_mesh/index.js | 260 +++++++++++++++++++++++++- 1 file changed, 258 insertions(+), 2 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index a0257c64280..52d86f4b04c 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -36,9 +36,35 @@ class Scratch3MeshBlocks { */ this.variableNames = []; + /** + * WebRTC connections + * @type {RTCPeerConnection[]} + */ + this.rtcConnections = []; + + /** + * WebRTC data channels + * @type {RTCDataChannel[]} + */ + this.rtcDataChannels = []; + + /** + * Host or not + * @type {bool} + */ + this.isHost = false; + + this.eventBroadcast = this.runtime.getOpcodeFunction('event_broadcast'); + this.runtime._primitives['event_broadcast'] = this._broadcast.bind(this); + + this.eventBroadcastandwait = this.runtime.getOpcodeFunction('event_broadcastandwait'); + this.runtime._primitives['event_broadcastandwait'] = this._broadcastAndWait.bind(this); + // FIXME: for debug - this._setVariable('var1', 10); - this._setVariable('var2', 20); + this._setVariable('message', 'Hello'); + this._setVariable('answer1', 'apple'); + this._setVariable('messageA', 'How are you?'); + this._setVariable('messageB', "I'm fine,and you?"); } /** @@ -53,6 +79,7 @@ class Scratch3MeshBlocks { description: 'Label for the mesh extension category' }), blockIconURI: blockIconURI, + showStatusButton: true, blocks: [ { opcode: 'getSensorValue', @@ -69,6 +96,35 @@ class Scratch3MeshBlocks { defaultValue: '' } } + }, + '---', + { + opcode: 'createHostOffer', + text: 'Host: create offer', + blockType: BlockType.COMMAND + }, + { + opcode: 'connectClient', + text: 'Host: connect client [CLIENT_DESC_JSON]', + blockType: BlockType.COMMAND, + arguments: { + CLIENT_DESC_JSON: { + type: ArgumentType.STRING, + defaultValue: ' ' + } + } + }, + '---', + { + opcode: 'createClientAnswer', + text: 'Client: create answer [HOST_DESC_JSON]', + blockType: BlockType.COMMAND, + arguments: { + HOST_DESC_JSON: { + type: ArgumentType.STRING, + defaultValue: ' ' + } + } } ], menus: { @@ -90,6 +146,160 @@ class Scratch3MeshBlocks { return this.variables[args.NAME]; } + _receivedMessageFromClient (message) { + console.log(`Host: received message: type=<${message.type}> data=<${message.data}>`); + switch (message.type) { + case 'broadcast': + console.log(`received broadcast: ${message.data}`); + break; + default: + console.error(`invalid message type: ${message.type}`); + break; + } + } + + _receivedMessageFromHost (message) { + console.log(`Client: received message: type=<${message.type}> data=<${message.data}>`); + switch (message.type) { + case 'broadcast': + console.log(`received broadcast: ${message.data}`); + break; + default: + console.error(`invalid message type: ${message.type}`); + break; + } + } + + createHostOffer (_) { + try { + this.isHost = true; + + if (this.rtcConnections.length == 0) { + const connection = new RTCPeerConnection(null); + connection.onicecandidate = e => { + if (!e.candidate) { + // NOTE: これをクライアントへコピーする + const hostDescJSON = JSON.stringify(connection.localDescription); + + console.log(`Host: connection.onicecandidate: offer\n${hostDescJSON}`); + if (navigator.clipboard) { + navigator.clipboard.writeText(hostDescJSON); + console.log('Host: copied offer to clipboard'); + } + } + }; + + const dataChannel = connection.createDataChannel('dataChannel'); + dataChannel.onopen = (e => { + console.log(`Host: data channel onopen: readyState=<${dataChannel.readyState}>`); + }).bind(this); + dataChannel.onmessage = (e => { + this._receivedMessageFromClient(JSON.parse(e.data)); + }).bind(this); + dataChannel.onclose = (e => { + console.log(`Host: data channel onclose: readyState=<${dataChannel.readyState}>`); + }).bind(this); + + connection.createOffer().then( + (hostDesc) => { + this.rtcConnections.push(connection); + this.rtcDataChannels.push(dataChannel); + + connection.setLocalDescription(hostDesc); + }, + (error) => { + // TODO: エラー処理 + console.error(`Host: error ${error}`); + } + ); + } + } + catch (e) { + console.error(`Host: ${e}`); + } + } + + connectClient (args) { + try { + const clientDescJson = args.CLIENT_DESC_JSON; + if (clientDescJson.length > 0 && this.rtcConnections.length > 0) { + const clientDesc = JSON.parse(clientDescJson); + const connection = this.rtcConnections[0]; + connection.setRemoteDescription(new RTCSessionDescription(clientDesc)); + + console.log('Host: connect Client'); + } + } + catch (e) { + console.error(`Host: ${e}`); + } + } + + createClientAnswer (args) { + try { + const hostDescJson = args.HOST_DESC_JSON; + if (hostDescJson.length > 0 && this.rtcConnections.length == 0) { + const hostDesc = JSON.parse(hostDescJson); + + const connection = new RTCPeerConnection(null); + connection.onicecandidate = e => { + if (e.candidate) { + console.log(`Client: connection.onicecandidate:\n${JSON.stringify(e.candidate)}`); + connection.addIceCandidate(e.candidate).then( + () => { + console.log(`Client: connection.onicecandidate: success`); + }, + (error) => { + console.error(`Client: connection.onicecandidate: error=<${error}>`); + } + ); + } + else { + // NOTE: これをクライアントへコピーする + const clientDescJSON = JSON.stringify(connection.localDescription); + + console.log(`Client: connection.onicecandidate: answer\n${clientDescJSON}`); + if (navigator.clipboard) { + navigator.clipboard.writeText(clientDescJSON); + console.log('Client: copied answer to clipboard'); + } + } + }; + connection.ondatachannel = (event => { + const dataChannel = event.channel; + + dataChannel.onopen = (e => { + console.log(`Client: data channel onopen: readyState=<${dataChannel.readyState}>`); + }).bind(this); + dataChannel.onmessage = (e => { + this._receivedMessageFromHost(JSON.parse(e.data)); + }).bind(this); + dataChannel.onclose = (e => { + console.log(`Client: data channel onclose: readyState=<${dataChannel.readyState}>`); + }).bind(this); + + this.rtcDataChannels.push(dataChannel); + }).bind(this); + + connection.setRemoteDescription(new RTCSessionDescription(hostDesc)); + connection.createAnswer().then( + (clientDesc) => { + this.rtcConnections.push(connection); + + connection.setLocalDescription(clientDesc); + }, + (error) => { + // TODO: エラー処理 + console.error(`Client: error ${error}`); + } + ); + } + } + catch (e) { + console.error(`Client: ${e}`); + } + } + _setVariable (name, value) { if (!this.variableNames.includes(name)) { this.variableNames.push(name); @@ -100,6 +310,52 @@ class Scratch3MeshBlocks { _getVariableNamesMenuItems () { return [''].concat(this.variableNames); } + + _broadcast (args, util) { + console.log('event_broadcast in mesh'); + this._sendBroadcast(args); + this.eventBroadcast(args, util); + } + + _broadcastAndWait (args, util) { + console.log('event_broadcastandwait in mesh'); + this._sendBroadcast(args); + this.eventBroadcastandwait(args, util); + } + + _sendBroadcast (args) { + try { + const broadcastName = args.BROADCAST_OPTION.name; + if (this.isHost) { + this._sendBroadcastToClients(broadcastName); + } + else { + this._sendBroadcastToHost(broadcastName); + } + } + catch (e) { + // TODO: エラー処理 + console.log(e); + } + } + + _sendBroadcastToClients (broadcastName) { + this.rtcDataChannels.forEach(channel => { + channel.send(JSON.stringify({ + type: 'broadcast', + data: broadcastName + })); + }); + } + + _sendBroadcastToHost (broadcastName) { + this.rtcDataChannels.forEach(channel => { + channel.send(JSON.stringify({ + type: 'broadcast', + data: broadcastName + })); + }); + } } module.exports = Scratch3MeshBlocks; From a6c32e2b083bdb4d64dbf348ee7d4feb2b2704be Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 11 Apr 2020 12:23:52 +0900 Subject: [PATCH 04/42] broadcast on Client when received broadcast message from Host. --- src/engine/execute.js | 5 ++++- src/engine/runtime.js | 2 +- src/engine/sequencer.js | 2 +- src/extensions/scratch3_mesh/index.js | 25 +++++++++++++------------ 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 51aee706425..b7ee4254adc 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -587,4 +587,7 @@ const execute = function (sequencer, thread) { } }; -module.exports = execute; +module.exports = { + execute, + blockUtility +}; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 908a30821d7..b6a6d7b843d 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -7,7 +7,7 @@ const BlocksRuntimeCache = require('./blocks-runtime-cache'); const BlockType = require('../extension-support/block-type'); const Profiler = require('./profiler'); const Sequencer = require('./sequencer'); -const execute = require('./execute.js'); +const {execute} = require('./execute.js'); const ScratchBlocksConstants = require('./scratch-blocks-constants'); const TargetType = require('../extension-support/target-type'); const Thread = require('./thread'); diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 504a3e078d1..864167e7fbf 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -1,6 +1,6 @@ const Timer = require('../util/timer'); const Thread = require('./thread'); -const execute = require('./execute.js'); +const {execute} = require('./execute.js'); /** * Profiler frame name for stepping a single thread. diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 52d86f4b04c..cca3786a25e 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -1,6 +1,7 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const formatMessage = require('format-message'); +const {blockUtility} = require('../../engine/execute'); /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. @@ -163,6 +164,13 @@ class Scratch3MeshBlocks { switch (message.type) { case 'broadcast': console.log(`received broadcast: ${message.data}`); + const args = { + BROADCAST_OPTION: { + id: null, + name: message.data + } + }; + this.eventBroadcast(args, blockUtility); break; default: console.error(`invalid message type: ${message.type}`); @@ -206,6 +214,9 @@ class Scratch3MeshBlocks { this.rtcDataChannels.push(dataChannel); connection.setLocalDescription(hostDesc); + + console.log(`Host: localDescription in createOffer:\n${JSON.stringify(connection.localDescription)}`); + }, (error) => { // TODO: エラー処理 @@ -243,18 +254,7 @@ class Scratch3MeshBlocks { const connection = new RTCPeerConnection(null); connection.onicecandidate = e => { - if (e.candidate) { - console.log(`Client: connection.onicecandidate:\n${JSON.stringify(e.candidate)}`); - connection.addIceCandidate(e.candidate).then( - () => { - console.log(`Client: connection.onicecandidate: success`); - }, - (error) => { - console.error(`Client: connection.onicecandidate: error=<${error}>`); - } - ); - } - else { + if (!e.candidate) { // NOTE: これをクライアントへコピーする const clientDescJSON = JSON.stringify(connection.localDescription); @@ -287,6 +287,7 @@ class Scratch3MeshBlocks { this.rtcConnections.push(connection); connection.setLocalDescription(clientDesc); + console.log(`Client: localDescription in createAnswer:\n${JSON.stringify(connection.localDescription)}`); }, (error) => { // TODO: エラー処理 From 14ad24b8eb66c03a0785919e7e36bacc01ccbe39 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 11 Apr 2020 14:59:52 +0900 Subject: [PATCH 05/42] receive broadcast from Client. --- package-lock.json | 17 ++++-- package.json | 1 + src/extensions/scratch3_mesh/index.js | 85 +++++++++++++++++++-------- 3 files changed, 74 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index cee94a5fa35..0e63ffcac8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4866,6 +4866,11 @@ } } }, + "fingerprintjs2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fingerprintjs2/-/fingerprintjs2-2.1.0.tgz", + "integrity": "sha512-H1k/ESTD2rJ3liupyqWBPjZC+LKfCGixQzz/NDN4dkgbmG1bVFyMOh7luKSkVDoyfhgvRm62pviNMPI+eJTZcQ==" + }, "flat-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", @@ -12013,9 +12018,9 @@ } }, "scratch-blocks": { - "version": "0.1.0-prerelease.1583868812", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1583868812.tgz", - "integrity": "sha512-9IMt3EwBaZZW7QBUB3jhanBsrUq8JjZKTlDSln/rnrrPjyzZnYT26X0mvt2rGBPT3eda2Af7iPfa/y/nqYzuzQ==", + "version": "0.1.0-prerelease.1586372076", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1586372076.tgz", + "integrity": "sha512-RWVYBsF2HXUfy74vk04qbCzD73Bt6v7Z/OEsfZn/oUObCTtc+NhUX3YFpgsN5eqgRFJVxG+lXHJ/Xq+lGuk9Xw==", "dev": true, "requires": { "exports-loader": "0.6.3", @@ -12062,9 +12067,9 @@ } }, "scratch-render": { - "version": "0.1.0-prerelease.20200228152431", - "resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20200228152431.tgz", - "integrity": "sha512-rI/IVwzAHVhq6IoFOgsqwwwwl9acizjJBVxOHKyP9F+/vs34uhbxr+mVUdpO82tSWpjDR+xuw3XfF7oH9U6WnQ==", + "version": "0.1.0-prerelease.20200409163640", + "resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20200409163640.tgz", + "integrity": "sha512-0CkRTpS71tYAOmWk1QvlTOqu7ZWEddRx9fEOia4wJDDs5XRew653j1V5NsJt68FTFHczXgE+WGI5pAwU/cQw7Q==", "dev": true, "requires": { "grapheme-breaker": "0.3.2", diff --git a/package.json b/package.json index b6df8959520..9870e2d4bb5 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "canvas-toBlob": "1.0.0", "decode-html": "2.0.0", "diff-match-patch": "1.0.4", + "fingerprintjs2": "2.1.0", "format-message": "6.2.1", "htmlparser2": "3.10.0", "immutable": "3.8.1", diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index cca3786a25e..6a62599bf63 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -1,7 +1,8 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const formatMessage = require('format-message'); -const {blockUtility} = require('../../engine/execute'); +const {blockUtility} = require('../../engine/execute.js'); +const Fingerprint2 = require('fingerprintjs2'); /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. @@ -55,6 +56,13 @@ class Scratch3MeshBlocks { */ this.isHost = false; + /** + * Fingerprint + * @type {string} + */ + this.fingerprint = null; + this._setFingerprint(); + this.eventBroadcast = this.runtime.getOpcodeFunction('event_broadcast'); this.runtime._primitives['event_broadcast'] = this._broadcast.bind(this); @@ -147,11 +155,29 @@ class Scratch3MeshBlocks { return this.variables[args.NAME]; } + _setFingerprint () { + if (!this.fingerprint) { + Fingerprint2.get(components => { + console.log(`Fingerprint2: componets:\n<${JSON.stringify(components)}>`); + this.fingerprint = Fingerprint2.x64hash128(components.map(c => c.value).join(''), 31); + console.log(`Fingerprint2: fingerprint=<${this.fingerprint}>`); + }); + } + } + _receivedMessageFromClient (message) { - console.log(`Host: received message: type=<${message.type}> data=<${message.data}>`); + console.log(`Host: received message: fingerprint=<${message.fingerprint}> type=<${message.type}> data=<${message.data}>`); switch (message.type) { case 'broadcast': console.log(`received broadcast: ${message.data}`); + this._sendMessageToClients(message); + const args = { + BROADCAST_OPTION: { + id: null, + name: message.data + } + }; + this.eventBroadcast(args, blockUtility); break; default: console.error(`invalid message type: ${message.type}`); @@ -160,17 +186,21 @@ class Scratch3MeshBlocks { } _receivedMessageFromHost (message) { - console.log(`Client: received message: type=<${message.type}> data=<${message.data}>`); + console.log(`Client: received message: fingerprint=<${message.fingerprint}> type=<${message.type}> data=<${message.data}>`); switch (message.type) { case 'broadcast': console.log(`received broadcast: ${message.data}`); - const args = { - BROADCAST_OPTION: { - id: null, - name: message.data - } - }; - this.eventBroadcast(args, blockUtility); + if (message.fingerprint == this.fingerprint) { + console.log('ignore broadcast: reason='); + } else { + const args = { + BROADCAST_OPTION: { + id: null, + name: message.data + } + }; + this.eventBroadcast(args, blockUtility); + } break; default: console.error(`invalid message type: ${message.type}`); @@ -187,7 +217,11 @@ class Scratch3MeshBlocks { connection.onicecandidate = e => { if (!e.candidate) { // NOTE: これをクライアントへコピーする - const hostDescJSON = JSON.stringify(connection.localDescription); + const data = { + fingerprint: this.fingerprint, + description: connection.localDescription + }; + const hostDescJSON = JSON.stringify(data); console.log(`Host: connection.onicecandidate: offer\n${hostDescJSON}`); if (navigator.clipboard) { @@ -236,7 +270,7 @@ class Scratch3MeshBlocks { if (clientDescJson.length > 0 && this.rtcConnections.length > 0) { const clientDesc = JSON.parse(clientDescJson); const connection = this.rtcConnections[0]; - connection.setRemoteDescription(new RTCSessionDescription(clientDesc)); + connection.setRemoteDescription(new RTCSessionDescription(clientDesc.description)); console.log('Host: connect Client'); } @@ -251,12 +285,17 @@ class Scratch3MeshBlocks { const hostDescJson = args.HOST_DESC_JSON; if (hostDescJson.length > 0 && this.rtcConnections.length == 0) { const hostDesc = JSON.parse(hostDescJson); + console.log(`Client: Host desc: <${JSON.stringify(hostDesc)}>`); const connection = new RTCPeerConnection(null); connection.onicecandidate = e => { if (!e.candidate) { + const data = { + fingerprint: this.fingerprint, + description: connection.localDescription + }; // NOTE: これをクライアントへコピーする - const clientDescJSON = JSON.stringify(connection.localDescription); + const clientDescJSON = JSON.stringify(data); console.log(`Client: connection.onicecandidate: answer\n${clientDescJSON}`); if (navigator.clipboard) { @@ -281,7 +320,7 @@ class Scratch3MeshBlocks { this.rtcDataChannels.push(dataChannel); }).bind(this); - connection.setRemoteDescription(new RTCSessionDescription(hostDesc)); + connection.setRemoteDescription(new RTCSessionDescription(hostDesc.description)); connection.createAnswer().then( (clientDesc) => { this.rtcConnections.push(connection); @@ -341,20 +380,20 @@ class Scratch3MeshBlocks { } _sendBroadcastToClients (broadcastName) { - this.rtcDataChannels.forEach(channel => { - channel.send(JSON.stringify({ - type: 'broadcast', - data: broadcastName - })); + this._sendMessageToClients({ + fingerprint: this.fingerprint, + type: 'broadcast', + data: broadcastName }); } _sendBroadcastToHost (broadcastName) { + this._sendBroadcastToClients(broadcastName); + } + + _sendMessageToClients (message) { this.rtcDataChannels.forEach(channel => { - channel.send(JSON.stringify({ - type: 'broadcast', - data: broadcastName - })); + channel.send(JSON.stringify(message)); }); } } From accec066915feaf3dee6450db9788bb8b30ac1ea Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 11 Apr 2020 16:24:59 +0900 Subject: [PATCH 06/42] fix send broadcast from Client. --- package-lock.json | 30 ++++++++++---- package.json | 2 +- src/extensions/scratch3_mesh/index.js | 58 ++++++++------------------- 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e63ffcac8d..9a69b1901e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4866,11 +4866,6 @@ } } }, - "fingerprintjs2": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fingerprintjs2/-/fingerprintjs2-2.1.0.tgz", - "integrity": "sha512-H1k/ESTD2rJ3liupyqWBPjZC+LKfCGixQzz/NDN4dkgbmG1bVFyMOh7luKSkVDoyfhgvRm62pviNMPI+eJTZcQ==" - }, "flat-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", @@ -11797,6 +11792,11 @@ "version": "6.3.2", "resolved": "http://registry.npmjs.org/qs/-/qs-6.3.2.tgz", "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -12567,6 +12567,14 @@ "requires": { "faye-websocket": "^0.10.0", "uuid": "^3.0.1" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "sockjs-client": { @@ -13932,9 +13940,9 @@ "dev": true }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" }, "v8-compile-cache": { "version": "2.0.2", @@ -14425,6 +14433,12 @@ "requires": { "has-flag": "^3.0.0" } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, diff --git a/package.json b/package.json index 9870e2d4bb5..86f044ff7d3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "canvas-toBlob": "1.0.0", "decode-html": "2.0.0", "diff-match-patch": "1.0.4", - "fingerprintjs2": "2.1.0", "format-message": "6.2.1", "htmlparser2": "3.10.0", "immutable": "3.8.1", @@ -47,6 +46,7 @@ "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "0.0.20191118205314", "text-encoding": "0.7.0", + "uuid": "^7.0.3", "worker-loader": "^1.1.1" }, "devDependencies": { diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 6a62599bf63..51125e2ce79 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -2,7 +2,7 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const formatMessage = require('format-message'); const {blockUtility} = require('../../engine/execute.js'); -const Fingerprint2 = require('fingerprintjs2'); +const {v4: uuidv4} = require('uuid'); /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. @@ -57,11 +57,10 @@ class Scratch3MeshBlocks { this.isHost = false; /** - * Fingerprint + * ID * @type {string} */ - this.fingerprint = null; - this._setFingerprint(); + this.id = uuidv4(); this.eventBroadcast = this.runtime.getOpcodeFunction('event_broadcast'); this.runtime._primitives['event_broadcast'] = this._broadcast.bind(this); @@ -155,44 +154,21 @@ class Scratch3MeshBlocks { return this.variables[args.NAME]; } - _setFingerprint () { - if (!this.fingerprint) { - Fingerprint2.get(components => { - console.log(`Fingerprint2: componets:\n<${JSON.stringify(components)}>`); - this.fingerprint = Fingerprint2.x64hash128(components.map(c => c.value).join(''), 31); - console.log(`Fingerprint2: fingerprint=<${this.fingerprint}>`); - }); - } - } - - _receivedMessageFromClient (message) { - console.log(`Host: received message: fingerprint=<${message.fingerprint}> type=<${message.type}> data=<${message.data}>`); + _onRtcMessage (message) { + console.log(`received message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}> data=<${message.data}>`); switch (message.type) { case 'broadcast': console.log(`received broadcast: ${message.data}`); - this._sendMessageToClients(message); - const args = { - BROADCAST_OPTION: { - id: null, - name: message.data - } - }; - this.eventBroadcast(args, blockUtility); - break; - default: - console.error(`invalid message type: ${message.type}`); - break; - } - } + if (this.isHost) { + console.log('broadcast all clients'); - _receivedMessageFromHost (message) { - console.log(`Client: received message: fingerprint=<${message.fingerprint}> type=<${message.type}> data=<${message.data}>`); - switch (message.type) { - case 'broadcast': - console.log(`received broadcast: ${message.data}`); - if (message.fingerprint == this.fingerprint) { + this._sendMessageToClients(message); + } + if (this.id == message.owner) { console.log('ignore broadcast: reason='); } else { + console.log('broadcast me'); + const args = { BROADCAST_OPTION: { id: null, @@ -218,7 +194,7 @@ class Scratch3MeshBlocks { if (!e.candidate) { // NOTE: これをクライアントへコピーする const data = { - fingerprint: this.fingerprint, + id: this.id, description: connection.localDescription }; const hostDescJSON = JSON.stringify(data); @@ -236,7 +212,7 @@ class Scratch3MeshBlocks { console.log(`Host: data channel onopen: readyState=<${dataChannel.readyState}>`); }).bind(this); dataChannel.onmessage = (e => { - this._receivedMessageFromClient(JSON.parse(e.data)); + this._onRtcMessage(JSON.parse(e.data)); }).bind(this); dataChannel.onclose = (e => { console.log(`Host: data channel onclose: readyState=<${dataChannel.readyState}>`); @@ -291,7 +267,7 @@ class Scratch3MeshBlocks { connection.onicecandidate = e => { if (!e.candidate) { const data = { - fingerprint: this.fingerprint, + id: this.id, description: connection.localDescription }; // NOTE: これをクライアントへコピーする @@ -311,7 +287,7 @@ class Scratch3MeshBlocks { console.log(`Client: data channel onopen: readyState=<${dataChannel.readyState}>`); }).bind(this); dataChannel.onmessage = (e => { - this._receivedMessageFromHost(JSON.parse(e.data)); + this._onRtcMessage(JSON.parse(e.data)); }).bind(this); dataChannel.onclose = (e => { console.log(`Client: data channel onclose: readyState=<${dataChannel.readyState}>`); @@ -381,7 +357,7 @@ class Scratch3MeshBlocks { _sendBroadcastToClients (broadcastName) { this._sendMessageToClients({ - fingerprint: this.fingerprint, + owner: this.id, type: 'broadcast', data: broadcastName }); From 1e5004cf6cb44ca4b2f7c1dc731102aa210c584c Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 11 Apr 2020 16:30:21 +0900 Subject: [PATCH 07/42] ignore error. --- src/extensions/scratch3_mesh/index.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 51125e2ce79..dd61b563e3a 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -368,9 +368,15 @@ class Scratch3MeshBlocks { } _sendMessageToClients (message) { - this.rtcDataChannels.forEach(channel => { - channel.send(JSON.stringify(message)); - }); + try { + this.rtcDataChannels.forEach(channel => { + channel.send(JSON.stringify(message)); + }); + } + catch (e) { + // TODO: エラー処理 + console.log(e); + } } } From 01ad63822c5dd1ed9243370d60671d041e36d0f1 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 11 Apr 2020 20:33:01 +0900 Subject: [PATCH 08/42] get all global variables on Host. --- src/extensions/scratch3_mesh/index.js | 183 +++++++++++++++++++------- 1 file changed, 135 insertions(+), 48 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index dd61b563e3a..fd6af803419 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -3,6 +3,7 @@ const BlockType = require('../../extension-support/block-type'); const formatMessage = require('format-message'); const {blockUtility} = require('../../engine/execute.js'); const {v4: uuidv4} = require('uuid'); +const Variable = require('../../engine/variable'); /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. @@ -68,11 +69,11 @@ class Scratch3MeshBlocks { this.eventBroadcastandwait = this.runtime.getOpcodeFunction('event_broadcastandwait'); this.runtime._primitives['event_broadcastandwait'] = this._broadcastAndWait.bind(this); - // FIXME: for debug - this._setVariable('message', 'Hello'); - this._setVariable('answer1', 'apple'); - this._setVariable('messageA', 'How are you?'); - this._setVariable('messageB', "I'm fine,and you?"); + this.dataSetvariableto = this.runtime.getOpcodeFunction('data_setvariableto'); + this.runtime._primitives['data_setvariableto'] = this._setVariableTo.bind(this); + + this.dataChangevariableby = this.runtime.getOpcodeFunction('data_changevariableby'); + this.runtime._primitives['data_changevariableby'] = this._changeVariableBy.bind(this); } /** @@ -148,45 +149,106 @@ class Scratch3MeshBlocks { * @return {Object} - the global variable's value from other projects */ getSensorValue (args) { - if (!this.variableNames.includes(args.NAME)) { + return this._getVariable(args.NAME); + } + + _getVariable (name) { + if (!this.variableNames.includes(name)) { return ''; } - return this.variables[args.NAME]; + return this.variables[name]; + } + + _getGlobalVariables () { + const variables = {}; + const stage = this.runtime.getTargetForStage(); + for (const varId in stage.variables) { + const currVar = stage.variables[varId]; + if (currVar.type === Variable.SCALAR_TYPE) { + variables[currVar.name] = currVar.value; + } + } + return variables; + } + + _onRtcOpen (connection, dataChannel) { + console.log(`data channel open`); + let variables; + if (this.isHost) { + variables = this.variables; + } else { + variables = this._getGlobalVariables(); + } + Object.keys(variables).forEach(name => { + const value = variables[name]; + + const message = { + owner: this.id, + type: 'variable', + data: { + name: name, + value: value + } + }; + dataChannel.send(JSON.stringify(message)); + console.log(`send variable: name=<${name}> value=<${value}>`); + }); } _onRtcMessage (message) { - console.log(`received message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}> data=<${message.data}>`); + console.log(`received message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}> data=<${JSON.stringify(message.data)}>`); switch (message.type) { case 'broadcast': - console.log(`received broadcast: ${message.data}`); + const broadcastName = message.data; + console.log(`received broadcast: ${broadcastName}`); if (this.isHost) { - console.log('broadcast all clients'); + console.log('send broadcast all clients'); - this._sendMessageToClients(message); + this._sendMessage(message); } if (this.id == message.owner) { console.log('ignore broadcast: reason='); } else { - console.log('broadcast me'); + console.log('process broadcast'); const args = { BROADCAST_OPTION: { id: null, - name: message.data + name: broadcastName } }; this.eventBroadcast(args, blockUtility); } break; + case 'variable': + const variable = message.data; + console.log(`received variable: name=<${variable.name}> value=<${variable.value}>`); + if (this.isHost) { + console.log('send variable all clients'); + + this._sendMessage(message); + } + if (this.id == message.owner) { + console.log('ignore variable: reason='); + } else { + console.log(`update variable: name=<${variable.name}> from=<${this._getVariable(variable.name)}> to=<${variable.value}>`); + this._setVariable(variable.name, variable.value); + } + break; default: console.error(`invalid message type: ${message.type}`); break; } } + _onRtcClose (connection, dataChannel) { + console.log(`data channel close`); + } + createHostOffer (_) { try { this.isHost = true; + this.variables = this._getGlobalVariables(); if (this.rtcConnections.length == 0) { const connection = new RTCPeerConnection(null); @@ -208,15 +270,15 @@ class Scratch3MeshBlocks { }; const dataChannel = connection.createDataChannel('dataChannel'); - dataChannel.onopen = (e => { - console.log(`Host: data channel onopen: readyState=<${dataChannel.readyState}>`); - }).bind(this); - dataChannel.onmessage = (e => { + dataChannel.onopen = e => { + this._onRtcOpen(connection, dataChannel); + }; + dataChannel.onmessage = e => { this._onRtcMessage(JSON.parse(e.data)); - }).bind(this); - dataChannel.onclose = (e => { - console.log(`Host: data channel onclose: readyState=<${dataChannel.readyState}>`); - }).bind(this); + }; + dataChannel.onclose = e => { + this._onRtcClose(connection, dataChannel); + }; connection.createOffer().then( (hostDesc) => { @@ -280,21 +342,21 @@ class Scratch3MeshBlocks { } } }; - connection.ondatachannel = (event => { + connection.ondatachannel = event => { const dataChannel = event.channel; - dataChannel.onopen = (e => { - console.log(`Client: data channel onopen: readyState=<${dataChannel.readyState}>`); - }).bind(this); - dataChannel.onmessage = (e => { + dataChannel.onopen = e => { + this._onRtcOpen(connection, dataChannel); + }; + dataChannel.onmessage = e => { this._onRtcMessage(JSON.parse(e.data)); - }).bind(this); - dataChannel.onclose = (e => { - console.log(`Client: data channel onclose: readyState=<${dataChannel.readyState}>`); - }).bind(this); + }; + dataChannel.onclose = e => { + this._onRtcClose(connection, dataChannel); + }; this.rtcDataChannels.push(dataChannel); - }).bind(this); + }; connection.setRemoteDescription(new RTCSessionDescription(hostDesc.description)); connection.createAnswer().then( @@ -327,6 +389,18 @@ class Scratch3MeshBlocks { return [''].concat(this.variableNames); } + _sendMessage (message) { + try { + this.rtcDataChannels.forEach(channel => { + channel.send(JSON.stringify(message)); + }); + } + catch (e) { + // TODO: エラー処理 + console.log(e); + } + } + _broadcast (args, util) { console.log('event_broadcast in mesh'); this._sendBroadcast(args); @@ -342,12 +416,11 @@ class Scratch3MeshBlocks { _sendBroadcast (args) { try { const broadcastName = args.BROADCAST_OPTION.name; - if (this.isHost) { - this._sendBroadcastToClients(broadcastName); - } - else { - this._sendBroadcastToHost(broadcastName); - } + this._sendMessage({ + owner: this.id, + type: 'broadcast', + data: broadcastName + }); } catch (e) { // TODO: エラー処理 @@ -355,22 +428,36 @@ class Scratch3MeshBlocks { } } - _sendBroadcastToClients (broadcastName) { - this._sendMessageToClients({ - owner: this.id, - type: 'broadcast', - data: broadcastName - }); + _setVariableTo (args, util) { + console.log('data_setvariableto in mesh'); + this.dataSetvariableto(args, util); + this._sendVariable(args, util); } - _sendBroadcastToHost (broadcastName) { - this._sendBroadcastToClients(broadcastName); + _changeVariableBy (args, util) { + console.log('data_changevariableby in mesh'); + this.dataChangevariableby(args, util); + this._sendVariable(args, util); } - _sendMessageToClients (message) { + _sendVariable (args, util) { try { - this.rtcDataChannels.forEach(channel => { - channel.send(JSON.stringify(message)); + const stage = this.runtime.getTargetForStage(); + let variable = stage.lookupVariableById(args.VARIABLE.id); + if (!variable) { + variable = stage.lookupVariableByNameAndType(args.VARIABLE.name, Variable.SCALAR_TYPE); + } + if (!variable) { + return; + } + + this._sendMessage({ + owner: this.id, + type: 'variable', + data: { + name: variable.name, + value: variable.value + } }); } catch (e) { From c9f9e1b27e1853e6dde80c2297bb18d1e5881218 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 11 Apr 2020 23:46:47 +0900 Subject: [PATCH 09/42] create and rename global variable. --- src/extensions/scratch3_mesh/index.js | 97 ++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index fd6af803419..b1843456148 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -74,6 +74,8 @@ class Scratch3MeshBlocks { this.dataChangevariableby = this.runtime.getOpcodeFunction('data_changevariableby'); this.runtime._primitives['data_changevariableby'] = this._changeVariableBy.bind(this); + + this._setVariableFunctionHOC(); } /** @@ -173,11 +175,9 @@ class Scratch3MeshBlocks { _onRtcOpen (connection, dataChannel) { console.log(`data channel open`); - let variables; + let variables = this._getGlobalVariables(); if (this.isHost) { - variables = this.variables; - } else { - variables = this._getGlobalVariables(); + variables = Object.assign(variables, this.variables); } Object.keys(variables).forEach(name => { const value = variables[name]; @@ -248,7 +248,6 @@ class Scratch3MeshBlocks { createHostOffer (_) { try { this.isHost = true; - this.variables = this._getGlobalVariables(); if (this.rtcConnections.length == 0) { const connection = new RTCPeerConnection(null); @@ -431,16 +430,16 @@ class Scratch3MeshBlocks { _setVariableTo (args, util) { console.log('data_setvariableto in mesh'); this.dataSetvariableto(args, util); - this._sendVariable(args, util); + this._sendVariableByOpcodeFunction(args, util); } _changeVariableBy (args, util) { console.log('data_changevariableby in mesh'); this.dataChangevariableby(args, util); - this._sendVariable(args, util); + this._sendVariableByOpcodeFunction(args, util); } - _sendVariable (args, util) { + _sendVariableByOpcodeFunction (args, util) { try { const stage = this.runtime.getTargetForStage(); let variable = stage.lookupVariableById(args.VARIABLE.id); @@ -451,12 +450,22 @@ class Scratch3MeshBlocks { return; } + this._sendVariable(variable.name, variable.value); + } + catch (e) { + // TODO: エラー処理 + console.log(e); + } + } + + _sendVariable (name, value) { + try { this._sendMessage({ owner: this.id, type: 'variable', data: { - name: variable.name, - value: variable.value + name: name, + value: value } }); } @@ -465,6 +474,74 @@ class Scratch3MeshBlocks { console.log(e); } } + + _setVariableFunctionHOC () { + const stage = this.runtime.getTargetForStage(); + this._variableFunctions = { + runtime: { + createNewGlobalVariable: this.runtime.createNewGlobalVariable.bind(this.runtime) + }, + stage: { + lookupOrCreateVariable: stage.lookupOrCreateVariable.bind(stage), + createVariable: stage.createVariable.bind(stage), + renameVariable: stage.renameVariable.bind(stage) + } + }; + + this.runtime.createNewGlobalVariable = this._createNewGlobalVariable.bind(this); + + stage.lookupOrCreateVariable = this._lookupOrCreateVariable.bind(this); + stage.createVariable = this._createVariable.bind(this); + stage.renameVariable = this._renameVariable.bind(this); + } + + _createNewGlobalVariable (variableName, optVarId, optVarType) { + console.log('runtime.createNewGlobalVariable in mesh'); + const variable = this._variableFunctions.runtime.createNewGlobalVariable(variableName, optVarId, optVarType); + this._sendVariable(variable.name, variable.value); + return variable; + } + + _lookupOrCreateVariable (id, name) { + console.log('stage.lookupOrCreateVariable in mesh'); + + const stage = this.runtime.getTargetForStage(); + let variable = stage.lookupVariableById(id); + if (variable) return variable; + + variable = stage.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE); + if (variable) return variable; + + // No variable with this name exists - create it locally. + const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false); + stage.variables[id] = newVariable; + this._sendVariable(newVariable.name, newVariable.value); + return newVariable; + } + + _createVariable (id, name, type, isCloud) { + console.log('stage.createVariable in mesh'); + + const stage = this.runtime.getTargetForStage(); + if (!stage.variables.hasOwnProperty(id)) { + this._variableFunctions.stage.createVariable(id, name, type, isCloud); + const variable = stage.variables[id]; + this._sendVariable(variable.name, variable.value); + } + } + + _renameVariable (id, newName) { + console.log('stage.renameVariable in mesh'); + + const stage = this.runtime.getTargetForStage(); + if (stage.variables.hasOwnProperty(id)) { + const variable = stage.variables[id]; + if (variable.id === id) { + this._variableFunctions.stage.renameVariable(id, newName); + this._sendVariable(variable.name, variable.value); + } + } + } } module.exports = Scratch3MeshBlocks; From 834e1559e99c6b3e2955c931537965dd7878fd31 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 00:10:09 +0900 Subject: [PATCH 10/42] use uid() instead of uuid(). --- package-lock.json | 5 ----- package.json | 1 - src/extensions/scratch3_mesh/index.js | 4 ++-- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a69b1901e3..8b682e721c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13939,11 +13939,6 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, - "uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" - }, "v8-compile-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz", diff --git a/package.json b/package.json index 86f044ff7d3..b6df8959520 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "0.0.20191118205314", "text-encoding": "0.7.0", - "uuid": "^7.0.3", "worker-loader": "^1.1.1" }, "devDependencies": { diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index b1843456148..2106c52e936 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -2,7 +2,7 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const formatMessage = require('format-message'); const {blockUtility} = require('../../engine/execute.js'); -const {v4: uuidv4} = require('uuid'); +const uid = require('../../util/uid'); const Variable = require('../../engine/variable'); /** @@ -61,7 +61,7 @@ class Scratch3MeshBlocks { * ID * @type {string} */ - this.id = uuidv4(); + this.id = uid(); this.eventBroadcast = this.runtime.getOpcodeFunction('event_broadcast'); this.runtime._primitives['event_broadcast'] = this._broadcast.bind(this); From e3f799b19eb864a796057b70c1f8600d01a7a404 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 00:11:35 +0900 Subject: [PATCH 11/42] revert package-lock.json. --- package-lock.json | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b682e721c3..cee94a5fa35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11792,11 +11792,6 @@ "version": "6.3.2", "resolved": "http://registry.npmjs.org/qs/-/qs-6.3.2.tgz", "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -12018,9 +12013,9 @@ } }, "scratch-blocks": { - "version": "0.1.0-prerelease.1586372076", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1586372076.tgz", - "integrity": "sha512-RWVYBsF2HXUfy74vk04qbCzD73Bt6v7Z/OEsfZn/oUObCTtc+NhUX3YFpgsN5eqgRFJVxG+lXHJ/Xq+lGuk9Xw==", + "version": "0.1.0-prerelease.1583868812", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1583868812.tgz", + "integrity": "sha512-9IMt3EwBaZZW7QBUB3jhanBsrUq8JjZKTlDSln/rnrrPjyzZnYT26X0mvt2rGBPT3eda2Af7iPfa/y/nqYzuzQ==", "dev": true, "requires": { "exports-loader": "0.6.3", @@ -12067,9 +12062,9 @@ } }, "scratch-render": { - "version": "0.1.0-prerelease.20200409163640", - "resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20200409163640.tgz", - "integrity": "sha512-0CkRTpS71tYAOmWk1QvlTOqu7ZWEddRx9fEOia4wJDDs5XRew653j1V5NsJt68FTFHczXgE+WGI5pAwU/cQw7Q==", + "version": "0.1.0-prerelease.20200228152431", + "resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20200228152431.tgz", + "integrity": "sha512-rI/IVwzAHVhq6IoFOgsqwwwwl9acizjJBVxOHKyP9F+/vs34uhbxr+mVUdpO82tSWpjDR+xuw3XfF7oH9U6WnQ==", "dev": true, "requires": { "grapheme-breaker": "0.3.2", @@ -12567,14 +12562,6 @@ "requires": { "faye-websocket": "^0.10.0", "uuid": "^3.0.1" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } } }, "sockjs-client": { @@ -13939,6 +13926,11 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" + }, "v8-compile-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz", @@ -14428,12 +14420,6 @@ "requires": { "has-flag": "^3.0.0" } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true } } }, From 18b5c145b8d9f169500905d399238c73bf35fb1d Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 00:39:49 +0900 Subject: [PATCH 12/42] change variable value by slider. --- src/engine/target.js | 14 ++++++++++++++ src/extensions/scratch3_mesh/index.js | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/engine/target.js b/src/engine/target.js index 6ed5747b561..0f2467e9ae8 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -305,6 +305,20 @@ class Target extends EventEmitter { } } + /** + * Sets the variable value with the given id to newValue. + * @param {string} id Id of variable to set value. + * @param {object} newValue New value for the variable. + */ + setVariableValue (id, newValue) { + if (this.variables.hasOwnProperty(id)) { + const variable = this.variables[id]; + if (variable.id === id) { + variable.value = newValue; + } + } + } + /** * Renames the variable with the given id to newName. * @param {string} id Id of variable to rename. diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 2106c52e936..5d0c16ab639 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -484,6 +484,7 @@ class Scratch3MeshBlocks { stage: { lookupOrCreateVariable: stage.lookupOrCreateVariable.bind(stage), createVariable: stage.createVariable.bind(stage), + setVariableValue: stage.setVariableValue.bind(stage), renameVariable: stage.renameVariable.bind(stage) } }; @@ -492,6 +493,7 @@ class Scratch3MeshBlocks { stage.lookupOrCreateVariable = this._lookupOrCreateVariable.bind(this); stage.createVariable = this._createVariable.bind(this); + stage.setVariableValue = this._setVariableValue.bind(this); stage.renameVariable = this._renameVariable.bind(this); } @@ -530,6 +532,19 @@ class Scratch3MeshBlocks { } } + _setVariableValue (id, newValue) { + console.log('stage.setVariableValue in mesh'); + + const stage = this.runtime.getTargetForStage(); + if (stage.variables.hasOwnProperty(id)) { + const variable = stage.variables[id]; + if (variable.id === id) { + this._variableFunctions.stage.setVariableValue(id, newValue); + this._sendVariable(variable.name, variable.value); + } + } + } + _renameVariable (id, newName) { console.log('stage.renameVariable in mesh'); From 3251a3bc4c08de2e7ae695f39b006a4dc181eaf2 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 01:14:54 +0900 Subject: [PATCH 13/42] refactor. --- src/engine/block-utility.js | 8 ++++++++ src/engine/execute.js | 5 +---- src/engine/runtime.js | 2 +- src/engine/sequencer.js | 2 +- src/extensions/scratch3_mesh/index.js | 4 ++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/engine/block-utility.js b/src/engine/block-utility.js index cfe18a22735..923e3862b99 100644 --- a/src/engine/block-utility.js +++ b/src/engine/block-utility.js @@ -7,6 +7,8 @@ const Timer = require('../util/timer'); * runtime, thread, target, and convenient methods. */ +let lastInstance; + class BlockUtility { constructor (sequencer = null, thread = null) { /** @@ -25,6 +27,8 @@ class BlockUtility { this._nowObj = { now: () => this.sequencer.runtime.currentMSecs }; + + lastInstance = this; } /** @@ -235,6 +239,10 @@ class BlockUtility { return devObject[func].apply(devObject, args); } } + + static lastInstance () { + return lastInstance; + } } module.exports = BlockUtility; diff --git a/src/engine/execute.js b/src/engine/execute.js index b7ee4254adc..51aee706425 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -587,7 +587,4 @@ const execute = function (sequencer, thread) { } }; -module.exports = { - execute, - blockUtility -}; +module.exports = execute; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index b6a6d7b843d..908a30821d7 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -7,7 +7,7 @@ const BlocksRuntimeCache = require('./blocks-runtime-cache'); const BlockType = require('../extension-support/block-type'); const Profiler = require('./profiler'); const Sequencer = require('./sequencer'); -const {execute} = require('./execute.js'); +const execute = require('./execute.js'); const ScratchBlocksConstants = require('./scratch-blocks-constants'); const TargetType = require('../extension-support/target-type'); const Thread = require('./thread'); diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 864167e7fbf..504a3e078d1 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -1,6 +1,6 @@ const Timer = require('../util/timer'); const Thread = require('./thread'); -const {execute} = require('./execute.js'); +const execute = require('./execute.js'); /** * Profiler frame name for stepping a single thread. diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 5d0c16ab639..5bd251555fd 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -1,7 +1,7 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const formatMessage = require('format-message'); -const {blockUtility} = require('../../engine/execute.js'); +const BlockUtility = require('../../engine/block-utility'); const uid = require('../../util/uid'); const Variable = require('../../engine/variable'); @@ -217,7 +217,7 @@ class Scratch3MeshBlocks { name: broadcastName } }; - this.eventBroadcast(args, blockUtility); + this.eventBroadcast(args, BlockUtility.lastInstance()); } break; case 'variable': From 122418d94a97303308fca1ed9925adfff1108622 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 18:08:23 +0900 Subject: [PATCH 14/42] use signaling server. --- src/extensions/scratch3_mesh/index.js | 185 +++++++++++++++++++++++++- 1 file changed, 182 insertions(+), 3 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 5bd251555fd..b6b4a202392 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -76,6 +76,8 @@ class Scratch3MeshBlocks { this.runtime._primitives['data_changevariableby'] = this._changeVariableBy.bind(this); this._setVariableFunctionHOC(); + + this._hostIds = []; } /** @@ -109,6 +111,42 @@ class Scratch3MeshBlocks { } }, '---', + { + opcode: 'connectSignalingServer', + text: 'H&C: connect [WSS_URL]', + blockType: BlockType.COMMAND, + arguments: { + WSS_URL: { + type: ArgumentType.STRING, + defaultValue: 'ws://localhost:8080' + } + } + }, + '---', + { + opcode: 'registerHost', + text: 'Host: register', + blockType: BlockType.COMMAND + }, + '---', + { + opcode: 'listHosts', + text: 'Client: list Hosts', + blockType: BlockType.COMMAND + }, + { + opcode: 'offerToHost', + text: 'Client: offer to [HOST_ID]', + blockType: BlockType.COMMAND, + arguments: { + HOST_ID: { + type: ArgumentType.STRING, + menu: 'hostIds', + defaultValue: ' ' + } + } + }, + '---', { opcode: 'createHostOffer', text: 'Host: create offer', @@ -142,7 +180,11 @@ class Scratch3MeshBlocks { variableNames: { acceptReporters: true, items: '_getVariableNamesMenuItems' - } + }, + hostIds: { + acceptReporters: true, + items: '_getHostIdsMenuItems' + }, } }; } @@ -161,6 +203,103 @@ class Scratch3MeshBlocks { return this.variables[name]; } + connectSignalingServer (args) { + try { + if (this._websocket) { + console.error('WebSocket already opened'); + } else { + const websocket = new WebSocket(args.WSS_URL); + websocket.onopen = e => { + this._websocket = websocket; + }; + websocket.onmessage = e => { + try { + this._onWebSocketMessage(JSON.parse(e.data)); + } + catch (error) { + console.error(`Error in WebSocket.onmessage: ${error}`); + } + }; + websocket.onclose = e => { + this._websocket = null; + }; + websocket.onerror = e => { + console.error(`Error in WebSocket: ${e}`); + }; + } + } + catch (e) { + console.error(`Error in connectSignalingServer: ${e}`); + } + } + + registerHost (args) { + try { + if (!this._websocket) { + console.error('WebSocket is not opened'); + return; + } + + this._websocket.send(JSON.stringify({ + service: 'mesh', + action: 'register', + data: { + id: this.id + } + })); + this.isHost = true; + } + catch (e) { + console.error(`Error in registerHost: ${e}`); + } + } + + listHosts (args) { + try { + if (!this._websocket) { + console.error('WebSocket is not opened'); + return; + } + + this._websocket.send(JSON.stringify({ + service: 'mesh', + action: 'list', + data: { + id: this.id + } + })); + } + catch (e) { + console.error(`Error in listHosts: ${e}`); + } + } + + offerToHost (args) { + try { + if (!args.HOST_ID || args.HOST_ID.trim() === '') { + console.error('Not select HOST_ID'); + return; + } + if (!this._websocket) { + console.error('WebSocket is not opened'); + return; + } + + this._websocket.send(JSON.stringify({ + service: 'mesh', + action: 'offer', + data: { + id: this.id, + hostId: args.HOST_ID, + clientDescription: 'Client Description' // TODO: make description + } + })); + } + catch (e) { + console.error(`Error in offerToHost: ${e}`); + } + } + _getGlobalVariables () { const variables = {}; const stage = this.runtime.getTargetForStage(); @@ -195,8 +334,44 @@ class Scratch3MeshBlocks { }); } + _onWebSocketMessage (message) { + console.log(`received WebSocket message: isHost=<${this.isHost}> message=<${JSON.stringify(message)}>`); + + if (message.hasOwnProperty('result') && !message.result) { + console.error(`failed action: action=<${message.action}> error=<${message.data.error}>`); + return; + } + + const {action, data} = message; + + switch (action) { + case 'list': + this._hostIds = data.hostIds; + break; + case 'offer': + if (this.isHost) { + if (data.hostId === this.id) { + this._websocket.send(JSON.stringify({ + service: 'mesh', + action: 'answer', + data: { + id: this.id, + clientId: data.id, + hostDescription: 'Host description' // TODO: make description + } + })); + } else { + console.error(`failed action: action=<${message.action}> reason=`); + } + } else { + console.error(`failed action: action=<${message.action}> reason=`); + } + break; + } + } + _onRtcMessage (message) { - console.log(`received message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}> data=<${JSON.stringify(message.data)}>`); + console.log(`received WebRTC message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}> data=<${JSON.stringify(message.data)}>`); switch (message.type) { case 'broadcast': const broadcastName = message.data; @@ -385,7 +560,11 @@ class Scratch3MeshBlocks { } _getVariableNamesMenuItems () { - return [''].concat(this.variableNames); + return [' '].concat(this.variableNames); + } + + _getHostIdsMenuItems () { + return [' '].concat(this._hostIds); } _sendMessage (message) { From aa82af50c9569c9c172443f8afeb7b4bae99824b Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 19:23:49 +0900 Subject: [PATCH 15/42] use signaling server. --- src/extensions/scratch3_mesh/index.js | 314 +++++++++++--------------- 1 file changed, 136 insertions(+), 178 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index b6b4a202392..de22e2ccd4d 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -145,35 +145,6 @@ class Scratch3MeshBlocks { defaultValue: ' ' } } - }, - '---', - { - opcode: 'createHostOffer', - text: 'Host: create offer', - blockType: BlockType.COMMAND - }, - { - opcode: 'connectClient', - text: 'Host: connect client [CLIENT_DESC_JSON]', - blockType: BlockType.COMMAND, - arguments: { - CLIENT_DESC_JSON: { - type: ArgumentType.STRING, - defaultValue: ' ' - } - } - }, - '---', - { - opcode: 'createClientAnswer', - text: 'Client: create answer [HOST_DESC_JSON]', - blockType: BlockType.COMMAND, - arguments: { - HOST_DESC_JSON: { - type: ArgumentType.STRING, - defaultValue: ' ' - } - } } ], menus: { @@ -284,16 +255,65 @@ class Scratch3MeshBlocks { console.error('WebSocket is not opened'); return; } + if (this.isHost) { + console.error('I am host'); + return; + } + if (this.rtcConnections.length > 0) { + console.error('Already WebRTC connected'); + return; + } - this._websocket.send(JSON.stringify({ - service: 'mesh', - action: 'offer', - data: { - id: this.id, - hostId: args.HOST_ID, - clientDescription: 'Client Description' // TODO: make description + const connection = new RTCPeerConnection(null); + this.rtcConnections.push(connection); + + connection.onconnectionstatechange = e => { + console.log(`Client: onconnectionstatechange: ${connection.connectionState}`); + + switch (connection.connectionState) { + case 'disconnected': + case 'failed': + case 'closed': + // TODO: this.rtcConnectionsから削除する + break; } - })); + }; + connection.onicecandidate = e => { + if (!e.candidate) { + console.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); + + this._websocket.send(JSON.stringify({ + service: 'mesh', + action: 'offer', + data: { + id: this.id, + hostId: args.HOST_ID, + clientDescription: connection.localDescription + } + })); + } + }; + + const dataChannel = connection.createDataChannel('dataChannel'); + dataChannel.onopen = e => { + this._onRtcOpen(connection, dataChannel); + }; + dataChannel.onmessage = e => { + this._onRtcMessage(JSON.parse(e.data)); + }; + dataChannel.onclose = e => { + this._onRtcClose(connection, dataChannel); + }; + + connection.createOffer().then( + (desc) => { + connection.setLocalDescription(desc); + }, + (error) => { + // TODO: エラー処理 + console.error(`Client: Error in createOffer: ${error}`); + } + ); } catch (e) { console.error(`Error in offerToHost: ${e}`); @@ -314,6 +334,9 @@ class Scratch3MeshBlocks { _onRtcOpen (connection, dataChannel) { console.log(`data channel open`); + + this.rtcDataChannels.push(dataChannel); + let variables = this._getGlobalVariables(); if (this.isHost) { variables = Object.assign(variables, this.variables); @@ -342,30 +365,97 @@ class Scratch3MeshBlocks { return; } - const {action, data} = message; + let connection; + const {action, data} = message; switch (action) { case 'list': this._hostIds = data.hostIds; break; case 'offer': - if (this.isHost) { - if (data.hostId === this.id) { + if (!this.isHost) { + console.error(`failed action: action=<${message.action}> reason=`); + return; + } + if (data.hostId !== this.id) { + console.error(`failed action: action=<${message.action}> reason= actual=<${data.hostId}> expected=<${this.id}>`); + } + + connection = new RTCPeerConnection(null); + this.rtcConnections.push(connection); + + connection.onconnectionstatechange = e => { + console.log(`Host: onconnectionstatechange: ${connection.connectionState}`); + + switch (connection.connectionState) { + case 'disconnected': + case 'failed': + case 'closed': + // TODO: this.rtcConnectionsから削除する + break; + } + }; + connection.onicecandidate = e => { + if (!e.candidate) { + console.log(`Host: answer to Client:\n${JSON.stringify(connection.localDescription)}`); + this._websocket.send(JSON.stringify({ service: 'mesh', action: 'answer', data: { id: this.id, clientId: data.id, - hostDescription: 'Host description' // TODO: make description + hostDescription: connection.localDescription } })); - } else { - console.error(`failed action: action=<${message.action}> reason=`); } - } else { - console.error(`failed action: action=<${message.action}> reason=`); + }; + connection.ondatachannel = event => { + const dataChannel = event.channel; + + dataChannel.onopen = e => { + this._onRtcOpen(connection, dataChannel); + }; + dataChannel.onmessage = e => { + this._onRtcMessage(JSON.parse(e.data)); + }; + dataChannel.onclose = e => { + this._onRtcClose(connection, dataChannel); + }; + }; + + connection.setRemoteDescription(new RTCSessionDescription(data.clientDescription)); + connection.createAnswer().then( + (desc) => { + connection.setLocalDescription(desc); + }, + (error) => { + // TODO: エラー処理 + console.error(`Host: Error in createAnswer: ${error}`); + } + ); + break; + case 'answer': + if (this.isHost) { + console.error(`failed action: action=<${message.action}> reason=`); + return; + } + if (data.clientId !== this.id) { + console.error(`failed action: action=<${message.action}> reason= actual=<${data.clientId}> expected=<${this.id}>`); + return; + } + + if (this.rtcConnections.length == 0) { + console.error(`failed action: action=<${message.action}> reason=`); + return; } + + console.log('Client: set Host description'); + + connection = this.rtcConnections[0]; + connection.setRemoteDescription(new RTCSessionDescription(data.hostDescription)); + + this._websocket.close(); break; } } @@ -404,7 +494,7 @@ class Scratch3MeshBlocks { this._sendMessage(message); } if (this.id == message.owner) { - console.log('ignore variable: reason='); + console.log('ignore variable: reason='); } else { console.log(`update variable: name=<${variable.name}> from=<${this._getVariable(variable.name)}> to=<${variable.value}>`); this._setVariable(variable.name, variable.value); @@ -420,138 +510,6 @@ class Scratch3MeshBlocks { console.log(`data channel close`); } - createHostOffer (_) { - try { - this.isHost = true; - - if (this.rtcConnections.length == 0) { - const connection = new RTCPeerConnection(null); - connection.onicecandidate = e => { - if (!e.candidate) { - // NOTE: これをクライアントへコピーする - const data = { - id: this.id, - description: connection.localDescription - }; - const hostDescJSON = JSON.stringify(data); - - console.log(`Host: connection.onicecandidate: offer\n${hostDescJSON}`); - if (navigator.clipboard) { - navigator.clipboard.writeText(hostDescJSON); - console.log('Host: copied offer to clipboard'); - } - } - }; - - const dataChannel = connection.createDataChannel('dataChannel'); - dataChannel.onopen = e => { - this._onRtcOpen(connection, dataChannel); - }; - dataChannel.onmessage = e => { - this._onRtcMessage(JSON.parse(e.data)); - }; - dataChannel.onclose = e => { - this._onRtcClose(connection, dataChannel); - }; - - connection.createOffer().then( - (hostDesc) => { - this.rtcConnections.push(connection); - this.rtcDataChannels.push(dataChannel); - - connection.setLocalDescription(hostDesc); - - console.log(`Host: localDescription in createOffer:\n${JSON.stringify(connection.localDescription)}`); - - }, - (error) => { - // TODO: エラー処理 - console.error(`Host: error ${error}`); - } - ); - } - } - catch (e) { - console.error(`Host: ${e}`); - } - } - - connectClient (args) { - try { - const clientDescJson = args.CLIENT_DESC_JSON; - if (clientDescJson.length > 0 && this.rtcConnections.length > 0) { - const clientDesc = JSON.parse(clientDescJson); - const connection = this.rtcConnections[0]; - connection.setRemoteDescription(new RTCSessionDescription(clientDesc.description)); - - console.log('Host: connect Client'); - } - } - catch (e) { - console.error(`Host: ${e}`); - } - } - - createClientAnswer (args) { - try { - const hostDescJson = args.HOST_DESC_JSON; - if (hostDescJson.length > 0 && this.rtcConnections.length == 0) { - const hostDesc = JSON.parse(hostDescJson); - console.log(`Client: Host desc: <${JSON.stringify(hostDesc)}>`); - - const connection = new RTCPeerConnection(null); - connection.onicecandidate = e => { - if (!e.candidate) { - const data = { - id: this.id, - description: connection.localDescription - }; - // NOTE: これをクライアントへコピーする - const clientDescJSON = JSON.stringify(data); - - console.log(`Client: connection.onicecandidate: answer\n${clientDescJSON}`); - if (navigator.clipboard) { - navigator.clipboard.writeText(clientDescJSON); - console.log('Client: copied answer to clipboard'); - } - } - }; - connection.ondatachannel = event => { - const dataChannel = event.channel; - - dataChannel.onopen = e => { - this._onRtcOpen(connection, dataChannel); - }; - dataChannel.onmessage = e => { - this._onRtcMessage(JSON.parse(e.data)); - }; - dataChannel.onclose = e => { - this._onRtcClose(connection, dataChannel); - }; - - this.rtcDataChannels.push(dataChannel); - }; - - connection.setRemoteDescription(new RTCSessionDescription(hostDesc.description)); - connection.createAnswer().then( - (clientDesc) => { - this.rtcConnections.push(connection); - - connection.setLocalDescription(clientDesc); - console.log(`Client: localDescription in createAnswer:\n${JSON.stringify(connection.localDescription)}`); - }, - (error) => { - // TODO: エラー処理 - console.error(`Client: error ${error}`); - } - ); - } - } - catch (e) { - console.error(`Client: ${e}`); - } - } - _setVariable (name, value) { if (!this.variableNames.includes(name)) { this.variableNames.push(name); From efc902ae047ebf16e70a79f9749e3306f69456f3 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 19:37:41 +0900 Subject: [PATCH 16/42] check variable type. --- src/extensions/scratch3_mesh/index.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index de22e2ccd4d..9134a233a33 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -637,7 +637,9 @@ class Scratch3MeshBlocks { _createNewGlobalVariable (variableName, optVarId, optVarType) { console.log('runtime.createNewGlobalVariable in mesh'); const variable = this._variableFunctions.runtime.createNewGlobalVariable(variableName, optVarId, optVarType); - this._sendVariable(variable.name, variable.value); + if (variable.type === Variable.SCALAR_TYPE) { + this._sendVariable(variable.name, variable.value); + } return variable; } @@ -664,8 +666,10 @@ class Scratch3MeshBlocks { const stage = this.runtime.getTargetForStage(); if (!stage.variables.hasOwnProperty(id)) { this._variableFunctions.stage.createVariable(id, name, type, isCloud); - const variable = stage.variables[id]; - this._sendVariable(variable.name, variable.value); + if (type === Variable.SCALAR_TYPE) { + const variable = stage.variables[id]; + this._sendVariable(variable.name, variable.value); + } } } @@ -677,7 +681,9 @@ class Scratch3MeshBlocks { const variable = stage.variables[id]; if (variable.id === id) { this._variableFunctions.stage.setVariableValue(id, newValue); - this._sendVariable(variable.name, variable.value); + if (variable.type === Variable.SCALAR_TYPE) { + this._sendVariable(variable.name, variable.value); + } } } } @@ -690,7 +696,9 @@ class Scratch3MeshBlocks { const variable = stage.variables[id]; if (variable.id === id) { this._variableFunctions.stage.renameVariable(id, newName); - this._sendVariable(variable.name, variable.value); + if (variable.type === Variable.SCALAR_TYPE) { + this._sendVariable(variable.name, variable.value); + } } } } From 6b2f083fafae186717dd1d367932f8af2ff1bd25 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 20:12:47 +0900 Subject: [PATCH 17/42] refactor and scan hook. --- src/extensions/scratch3_mesh/index.js | 96 +++++++++++++++++++++------ 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 9134a233a33..f0fb4dcd71c 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -20,6 +20,20 @@ const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYA * @constructor */ class Scratch3MeshBlocks { + /** + * @return {string} - the name of this extension. + */ + static get EXTENSION_NAME () { + return 'Mesh'; + } + + /** + * @return {string} - the ID of this extension. + */ + static get EXTENSION_ID () { + return 'mesh'; + } + constructor (runtime) { /** * The runtime instantiating this block package. @@ -63,21 +77,51 @@ class Scratch3MeshBlocks { */ this.id = uid(); - this.eventBroadcast = this.runtime.getOpcodeFunction('event_broadcast'); - this.runtime._primitives['event_broadcast'] = this._broadcast.bind(this); + this._setOpcodeFunctionHOC(); + this._setVariableFunctionHOC(); - this.eventBroadcastandwait = this.runtime.getOpcodeFunction('event_broadcastandwait'); - this.runtime._primitives['event_broadcastandwait'] = this._broadcastAndWait.bind(this); + this._hostIds = []; - this.dataSetvariableto = this.runtime.getOpcodeFunction('data_setvariableto'); - this.runtime._primitives['data_setvariableto'] = this._setVariableTo.bind(this); + this.runtime.registerPeripheralExtension(Scratch3MeshBlocks.EXTENSION_ID, this); + } - this.dataChangevariableby = this.runtime.getOpcodeFunction('data_changevariableby'); - this.runtime._primitives['data_changevariableby'] = this._changeVariableBy.bind(this); + /** + * Called by the runtime when user wants to scan for a peripheral. + */ + scan () { + console.log('scan in mesh'); + } - this._setVariableFunctionHOC(); + /** + * Called by the runtime when user wants to connect to a certain peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + console.log(`connect in mesh: ${id}`); + } - this._hostIds = []; + /** + * Disconnect from the micro:bit. + */ + disconnect () { + console.log(`disconnect in mesh`); + } + + /** + * Reset all the state and timeout/interval ids. + */ + reset () { + console.log(`reset in mesh`); + } + + /** + * Return true if connected to the micro:bit. + * @return {boolean} - whether the micro:bit is connected. + */ + isConnected () { + console.log(`isConnected in mesh`); + + return false; } /** @@ -85,12 +129,8 @@ class Scratch3MeshBlocks { */ getInfo () { return { - id: 'mesh', - name: formatMessage({ - id: 'mesh.categoryName', - default: 'Mesh', - description: 'Label for the mesh extension category' - }), + id: Scratch3MeshBlocks.EXTENSION_ID, + name: Scratch3MeshBlocks.EXTENSION_NAME, blockIconURI: blockIconURI, showStatusButton: true, blocks: [ @@ -482,7 +522,7 @@ class Scratch3MeshBlocks { name: broadcastName } }; - this.eventBroadcast(args, BlockUtility.lastInstance()); + this._opcodeFunctions['event_broadcast'](args, BlockUtility.lastInstance()); } break; case 'variable': @@ -540,13 +580,13 @@ class Scratch3MeshBlocks { _broadcast (args, util) { console.log('event_broadcast in mesh'); this._sendBroadcast(args); - this.eventBroadcast(args, util); + this._opcodeFunctions['event_broadcast'](args, util); } _broadcastAndWait (args, util) { console.log('event_broadcastandwait in mesh'); this._sendBroadcast(args); - this.eventBroadcastandwait(args, util); + this._opcodeFunctions['event_broadcastandwait'](args, util); } _sendBroadcast (args) { @@ -566,13 +606,13 @@ class Scratch3MeshBlocks { _setVariableTo (args, util) { console.log('data_setvariableto in mesh'); - this.dataSetvariableto(args, util); + this._opcodeFunctions['data_setvariableto'](args, util); this._sendVariableByOpcodeFunction(args, util); } _changeVariableBy (args, util) { console.log('data_changevariableby in mesh'); - this.dataChangevariableby(args, util); + this._opcodeFunctions['data_changevariableby'](args, util); this._sendVariableByOpcodeFunction(args, util); } @@ -612,6 +652,20 @@ class Scratch3MeshBlocks { } } + _setOpcodeFunctionHOC () { + this._opcodeFunctions = { + event_broadcast: this.runtime.getOpcodeFunction('event_broadcast'), + event_broadcastandwait: this.runtime.getOpcodeFunction('event_broadcastandwait'), + data_setvariableto: this.runtime.getOpcodeFunction('data_setvariableto'), + data_changevariableby: this.runtime.getOpcodeFunction('data_changevariableby') + }; + + this.runtime._primitives['event_broadcast'] = this._broadcast.bind(this); + this.runtime._primitives['event_broadcastandwait'] = this._broadcastAndWait.bind(this); + this.runtime._primitives['data_setvariableto'] = this._setVariableTo.bind(this); + this.runtime._primitives['data_changevariableby'] = this._changeVariableBy.bind(this); + } + _setVariableFunctionHOC () { const stage = this.runtime.getTargetForStage(); this._variableFunctions = { From aa2b3d9a79501ee443e35ae2b22675b78528c2e2 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 21:46:21 +0900 Subject: [PATCH 18/42] scan. --- src/extensions/scratch3_mesh/index.js | 96 ++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index f0fb4dcd71c..deb7d6e1749 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -12,7 +12,8 @@ const Variable = require('../../engine/variable'); // eslint-disable-next-line max-len const blockIconURI = 'data:image/png;base64,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'; - +const MESH_HOST_PERIPHERAL_ID = 'mesh_host'; +const MESH_WSS_URL = 'ws://localhost:8080'; /** * Host for the Mesh-related blocks @@ -81,6 +82,7 @@ class Scratch3MeshBlocks { this._setVariableFunctionHOC(); this._hostIds = []; + this._availablePeripherals = {}; this.runtime.registerPeripheralExtension(Scratch3MeshBlocks.EXTENSION_ID, this); } @@ -90,6 +92,67 @@ class Scratch3MeshBlocks { */ scan () { console.log('scan in mesh'); + + try { + this._availablePeripherals[MESH_HOST_PERIPHERAL_ID] = { + name: formatMessage({ + id: 'mesh.hostPeripheralName', + default: 'Host Mesh', + description: 'label for "Host Mesh" in connect modal for Mesh extension' + }) + ` [${this.id.slice(0, 8)}]`, + periperalId: MESH_HOST_PERIPHERAL_ID, + rssi: 0 + }; + + if (this._websocket) { + const websocket = this._websocket; + + websocket.send(JSON.stringify({ + service: 'mesh', + action: 'list', + data: { + id: this.id + } + })); + } else { + this._websocket = new WebSocket(MESH_WSS_URL); + const websocket = this._websocket; + websocket.onopen = e => { + this.runtime.emit( + this.runtime.constructor.PERIPHERAL_LIST_UPDATE, + this._availablePeripherals + ); + + websocket.send(JSON.stringify({ + service: 'mesh', + action: 'list', + data: { + id: this.id + } + })); + }; + websocket.onmessage = e => { + try { + this._onWebSocketMessage(JSON.parse(e.data)); + } + catch (error) { + console.error(`Error in WebSocket.onmessage: ${error}`); + } + }; + websocket.onclose = e => { + this._websocket = null; + }; + websocket.onerror = e => { + console.error(`Error in WebSocket: ${e}`); + }; + } + } + catch (e) { + console.error(`Error in connectSignalingServer: ${e}`); + } + + setTimeout(() => { + }, 500); } /** @@ -410,7 +473,36 @@ class Scratch3MeshBlocks { const {action, data} = message; switch (action) { case 'list': - this._hostIds = data.hostIds; + const now = new Date(); + data.hostIds.forEach(hostId => { + const t = now - Date.parse(hostId.updatedAt); + let rssi; + if (t <= 1 * 60 * 1000) { + rssi = 0; + } else if (t <= 3 * 60 * 1000) { + rssi = -20; + } else if (t <= 15 * 60 * 1000) { + rssi = -40; + } else if (t <= 30 * 60 * 1000) { + rssi = -60; + } else { + rssi = -80; + } + this._availablePeripherals[hostId.id] = { + name: formatMessage({ + id: 'mesh.clientPeripheralName', + default: 'Join Mesh', + description: 'label for "Join Mesh" in connect modal for Mesh extension' + }) + ` [${hostId.id.slice(0, 8)}]`, + periperalId: hostId.id, + rssi: rssi + }; + }); + + this.runtime.emit( + this.runtime.constructor.PERIPHERAL_LIST_UPDATE, + this._availablePeripherals + ); break; case 'offer': if (!this.isHost) { From 2fd0c580c83755db7c85ff30928b185e1dbc0304 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 21:49:31 +0900 Subject: [PATCH 19/42] fixed scan. --- src/extensions/scratch3_mesh/index.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index deb7d6e1749..2dd6dd8d8d4 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -100,7 +100,7 @@ class Scratch3MeshBlocks { default: 'Host Mesh', description: 'label for "Host Mesh" in connect modal for Mesh extension' }) + ` [${this.id.slice(0, 8)}]`, - periperalId: MESH_HOST_PERIPHERAL_ID, + peripheralId: MESH_HOST_PERIPHERAL_ID, rssi: 0 }; @@ -148,11 +148,8 @@ class Scratch3MeshBlocks { } } catch (e) { - console.error(`Error in connectSignalingServer: ${e}`); + console.error(`Error in scan: ${e}`); } - - setTimeout(() => { - }, 500); } /** @@ -494,7 +491,7 @@ class Scratch3MeshBlocks { default: 'Join Mesh', description: 'label for "Join Mesh" in connect modal for Mesh extension' }) + ` [${hostId.id.slice(0, 8)}]`, - periperalId: hostId.id, + peripheralId: hostId.id, rssi: rssi }; }); From 462ecdeb42fe0d5416f0b0fc800d89a474bde524 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 22:22:26 +0900 Subject: [PATCH 20/42] connect. --- src/extensions/scratch3_mesh/index.js | 126 ++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 6 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 2dd6dd8d8d4..5f27fd5d20b 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -157,7 +157,104 @@ class Scratch3MeshBlocks { * @param {number} id - the id of the peripheral to connect to. */ connect (id) { - console.log(`connect in mesh: ${id}`); + try { + console.log(`connect in mesh: ${id}`); + + this._connected = false; + + if (id === MESH_HOST_PERIPHERAL_ID) { + if (!this._websocket) { + console.error('WebSocket is not opened'); + return; + } + + this._websocket.send(JSON.stringify({ + service: 'mesh', + action: 'register', + data: { + id: this.id + } + })); + + this.isHost = true; + } else { + if (!id || id.trim() === '') { + console.error('Not select id'); + return; + } + if (!this._websocket) { + console.error('WebSocket is not opened'); + return; + } + if (this.isHost) { + console.error('I am not Client'); + return; + } + if (this.rtcConnections.length > 0) { + console.error('Already WebRTC connected'); + return; + } + + const connection = new RTCPeerConnection(null); + this.rtcConnections.push(connection); + + connection.onconnectionstatechange = e => { + console.log(`Client: onconnectionstatechange: ${connection.connectionState}`); + + switch (connection.connectionState) { + case 'disconnected': + case 'failed': + case 'closed': + // TODO: this.rtcConnectionsから削除する + break; + } + }; + connection.onicecandidate = e => { + if (!e.candidate) { + console.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); + + this._websocket.send(JSON.stringify({ + service: 'mesh', + action: 'offer', + data: { + id: this.id, + hostId: id, + clientDescription: connection.localDescription + } + })); + } + }; + + const dataChannel = connection.createDataChannel('dataChannel'); + dataChannel.onopen = e => { + this._onRtcOpen(connection, dataChannel); + }; + dataChannel.onmessage = e => { + try { + this._onRtcMessage(JSON.parse(e.data)); + } + catch (error) { + console.error(`Client: Error in dataChannel.onmessage: ${error}`); + } + }; + dataChannel.onclose = e => { + this._onRtcClose(connection, dataChannel); + }; + + connection.createOffer().then( + (desc) => { + connection.setLocalDescription(desc); + }, + (error) => { + // TODO: エラー処理 + console.error(`Client: Error in createOffer: ${error}`); + } + ); + } + } + catch (e) { + console.error(`Error in connect: ${e}`); + } } /** @@ -175,13 +272,13 @@ class Scratch3MeshBlocks { } /** - * Return true if connected to the micro:bit. - * @return {boolean} - whether the micro:bit is connected. + * Return true if connected to the Mesh + * @return {boolean} - whether the Mesh is connected. */ isConnected () { - console.log(`isConnected in mesh`); + console.log(`isConnected in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); - return false; + return this._connected; } /** @@ -399,7 +496,12 @@ class Scratch3MeshBlocks { this._onRtcOpen(connection, dataChannel); }; dataChannel.onmessage = e => { - this._onRtcMessage(JSON.parse(e.data)); + try { + this._onRtcMessage(JSON.parse(e.data)); + } + catch (error) { + console.error(`Client: Error in dataChannel.onmessage: ${error}`); + } }; dataChannel.onclose = e => { this._onRtcClose(connection, dataChannel); @@ -469,6 +571,15 @@ class Scratch3MeshBlocks { const {action, data} = message; switch (action) { + case 'register': + if (!this.isHost) { + console.error(`failed action: action=<${message.action}> reason=`); + return; + } + + this._connected = true; + this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); + break; case 'list': const now = new Date(); data.hostIds.forEach(hostId => { @@ -585,6 +696,9 @@ class Scratch3MeshBlocks { connection.setRemoteDescription(new RTCSessionDescription(data.hostDescription)); this._websocket.close(); + + this._connected = true; + this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); break; } } From f33128db2f5a8f44b31dc4d17253f736038f6712 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 22:24:49 +0900 Subject: [PATCH 21/42] removed unused block. --- src/extensions/scratch3_mesh/index.js | 198 +------------------------- 1 file changed, 1 insertion(+), 197 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 5f27fd5d20b..58f4867ec90 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -81,7 +81,6 @@ class Scratch3MeshBlocks { this._setOpcodeFunctionHOC(); this._setVariableFunctionHOC(); - this._hostIds = []; this._availablePeripherals = {}; this.runtime.registerPeripheralExtension(Scratch3MeshBlocks.EXTENSION_ID, this); @@ -306,53 +305,13 @@ class Scratch3MeshBlocks { defaultValue: '' } } - }, - '---', - { - opcode: 'connectSignalingServer', - text: 'H&C: connect [WSS_URL]', - blockType: BlockType.COMMAND, - arguments: { - WSS_URL: { - type: ArgumentType.STRING, - defaultValue: 'ws://localhost:8080' - } - } - }, - '---', - { - opcode: 'registerHost', - text: 'Host: register', - blockType: BlockType.COMMAND - }, - '---', - { - opcode: 'listHosts', - text: 'Client: list Hosts', - blockType: BlockType.COMMAND - }, - { - opcode: 'offerToHost', - text: 'Client: offer to [HOST_ID]', - blockType: BlockType.COMMAND, - arguments: { - HOST_ID: { - type: ArgumentType.STRING, - menu: 'hostIds', - defaultValue: ' ' - } - } } ], menus: { variableNames: { acceptReporters: true, items: '_getVariableNamesMenuItems' - }, - hostIds: { - acceptReporters: true, - items: '_getHostIdsMenuItems' - }, + } } }; } @@ -371,157 +330,6 @@ class Scratch3MeshBlocks { return this.variables[name]; } - connectSignalingServer (args) { - try { - if (this._websocket) { - console.error('WebSocket already opened'); - } else { - const websocket = new WebSocket(args.WSS_URL); - websocket.onopen = e => { - this._websocket = websocket; - }; - websocket.onmessage = e => { - try { - this._onWebSocketMessage(JSON.parse(e.data)); - } - catch (error) { - console.error(`Error in WebSocket.onmessage: ${error}`); - } - }; - websocket.onclose = e => { - this._websocket = null; - }; - websocket.onerror = e => { - console.error(`Error in WebSocket: ${e}`); - }; - } - } - catch (e) { - console.error(`Error in connectSignalingServer: ${e}`); - } - } - - registerHost (args) { - try { - if (!this._websocket) { - console.error('WebSocket is not opened'); - return; - } - - this._websocket.send(JSON.stringify({ - service: 'mesh', - action: 'register', - data: { - id: this.id - } - })); - this.isHost = true; - } - catch (e) { - console.error(`Error in registerHost: ${e}`); - } - } - - listHosts (args) { - try { - if (!this._websocket) { - console.error('WebSocket is not opened'); - return; - } - - this._websocket.send(JSON.stringify({ - service: 'mesh', - action: 'list', - data: { - id: this.id - } - })); - } - catch (e) { - console.error(`Error in listHosts: ${e}`); - } - } - - offerToHost (args) { - try { - if (!args.HOST_ID || args.HOST_ID.trim() === '') { - console.error('Not select HOST_ID'); - return; - } - if (!this._websocket) { - console.error('WebSocket is not opened'); - return; - } - if (this.isHost) { - console.error('I am host'); - return; - } - if (this.rtcConnections.length > 0) { - console.error('Already WebRTC connected'); - return; - } - - const connection = new RTCPeerConnection(null); - this.rtcConnections.push(connection); - - connection.onconnectionstatechange = e => { - console.log(`Client: onconnectionstatechange: ${connection.connectionState}`); - - switch (connection.connectionState) { - case 'disconnected': - case 'failed': - case 'closed': - // TODO: this.rtcConnectionsから削除する - break; - } - }; - connection.onicecandidate = e => { - if (!e.candidate) { - console.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); - - this._websocket.send(JSON.stringify({ - service: 'mesh', - action: 'offer', - data: { - id: this.id, - hostId: args.HOST_ID, - clientDescription: connection.localDescription - } - })); - } - }; - - const dataChannel = connection.createDataChannel('dataChannel'); - dataChannel.onopen = e => { - this._onRtcOpen(connection, dataChannel); - }; - dataChannel.onmessage = e => { - try { - this._onRtcMessage(JSON.parse(e.data)); - } - catch (error) { - console.error(`Client: Error in dataChannel.onmessage: ${error}`); - } - }; - dataChannel.onclose = e => { - this._onRtcClose(connection, dataChannel); - }; - - connection.createOffer().then( - (desc) => { - connection.setLocalDescription(desc); - }, - (error) => { - // TODO: エラー処理 - console.error(`Client: Error in createOffer: ${error}`); - } - ); - } - catch (e) { - console.error(`Error in offerToHost: ${e}`); - } - } - _getGlobalVariables () { const variables = {}; const stage = this.runtime.getTargetForStage(); @@ -764,10 +572,6 @@ class Scratch3MeshBlocks { return [' '].concat(this.variableNames); } - _getHostIdsMenuItems () { - return [' '].concat(this._hostIds); - } - _sendMessage (message) { try { this.rtcDataChannels.forEach(channel => { From 777309079dfccea9206300ae30614857590d07b8 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 12 Apr 2020 22:44:40 +0900 Subject: [PATCH 22/42] disconnect. --- src/extensions/scratch3_mesh/index.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 58f4867ec90..1b2f4ea8c19 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -93,6 +93,7 @@ class Scratch3MeshBlocks { console.log('scan in mesh'); try { + this._availablePeripherals = {}; this._availablePeripherals[MESH_HOST_PERIPHERAL_ID] = { name: formatMessage({ id: 'mesh.hostPeripheralName', @@ -257,10 +258,31 @@ class Scratch3MeshBlocks { } /** - * Disconnect from the micro:bit. + * Disconnect from the Mesh. */ disconnect () { - console.log(`disconnect in mesh`); + console.log(`disconnect in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); + + if (!this._connected) { + console.warn('Mesh: Already disconnected'); + return; + } + + if (this._websocket) { + this._websocket.close(); + } + this.variables = {}; + this.variableNames = []; + this.rtcConnections.forEach(connection => { + connection.close(); + }); + this.rtcConnections = []; + this.rtcDataChannels = []; + this.isHost = false; + + this._connected = false; + + this.runtime.emit(this.runtime.constructor.PERIPHERAL_DISCONNECTED); } /** From f327eeaaf756cfe77e8227214929506e7e9ee1bd Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Mon, 13 Apr 2020 00:29:17 +0900 Subject: [PATCH 23/42] connected message. --- src/engine/runtime.js | 14 +++++++++++ src/extensions/scratch3_mesh/index.js | 36 +++++++++++++++++++++------ src/virtual-machine.js | 9 +++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 908a30821d7..14e3978cc1f 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1488,6 +1488,20 @@ class Runtime extends EventEmitter { return isConnected; } + /** + * Returns the connected message. + * @param {string} extensionId - the id of the extension. + * @return {string|null} - the connected message. + */ + getPeripheralConnectedMessage (extensionId) { + if (this.getPeripheralIsConnected(extensionId) && + this.peripheralExtensions[extensionId] && + this.peripheralExtensions[extensionId].connectedMessage) { + return this.peripheralExtensions[extensionId].connectedMessage(); + } + return null; + } + /** * Emit an event to indicate that the microphone is being used to stream audio. * @param {boolean} listening - true if the microphone is currently listening. diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 1b2f4ea8c19..5697a3aedce 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -279,19 +279,13 @@ class Scratch3MeshBlocks { this.rtcConnections = []; this.rtcDataChannels = []; this.isHost = false; + this._hostId = null; this._connected = false; this.runtime.emit(this.runtime.constructor.PERIPHERAL_DISCONNECTED); } - /** - * Reset all the state and timeout/interval ids. - */ - reset () { - console.log(`reset in mesh`); - } - /** * Return true if connected to the Mesh * @return {boolean} - whether the Mesh is connected. @@ -302,6 +296,33 @@ class Scratch3MeshBlocks { return this._connected; } + /** + * Return connected message if connected to the Mesh + * @return {string} - connected message. + */ + connectedMessage () { + console.log(`connectedMessage in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); + + let message; + let id; + if (this.isHost) { + message = formatMessage({ + id: 'mesh.registeredHost', + default: 'Registered Host Mesh [[ID]]', + description: 'label for registered Host Mesh in connect modal for Mesh extension' + }); + id = this.id; + } else { + message = formatMessage({ + id: 'mesh.joinedMesh', + default: 'Joined Mesh [[ID]]', + description: 'label for joined Mesh in connect modal for Mesh extension' + }); + id = this._hostId; + } + return message.replace('[ID]', id.slice(0, 8)); + } + /** * @returns {object} metadata for this extension and its blocks. */ @@ -527,6 +548,7 @@ class Scratch3MeshBlocks { this._websocket.close(); + this._hostId = data.id; this._connected = true; this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); break; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index a36fe767ca9..ecd80830c5b 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -289,6 +289,15 @@ class VirtualMachine extends EventEmitter { return this.runtime.getPeripheralIsConnected(extensionId); } + /** + * Returns connected message. + * @param {string} extensionId - the id of the extension. + * @return {string} - connected message. + */ + getPeripheralConnectedMessage (extensionId) { + return this.runtime.getPeripheralConnectedMessage(extensionId); + } + /** * Load a Scratch project from a .sb, .sb2, .sb3 or json string. * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load. From b54d74542f67e45ac38cabc696a6b0c460877711 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Mon, 13 Apr 2020 01:08:49 +0900 Subject: [PATCH 24/42] fixed register own variable when reconnect client. --- src/extensions/scratch3_mesh/index.js | 35 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 5697a3aedce..fc7dd553d21 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -370,7 +370,7 @@ class Scratch3MeshBlocks { if (!this.variableNames.includes(name)) { return ''; } - return this.variables[name]; + return this.variables[name].value; } _getGlobalVariables () { @@ -379,7 +379,11 @@ class Scratch3MeshBlocks { for (const varId in stage.variables) { const currVar = stage.variables[varId]; if (currVar.type === Variable.SCALAR_TYPE) { - variables[currVar.name] = currVar.value; + variables[currVar.name] = { + name: currVar.name, + value: currVar.value, + owner: this.id + }; } } return variables; @@ -390,23 +394,26 @@ class Scratch3MeshBlocks { this.rtcDataChannels.push(dataChannel); - let variables = this._getGlobalVariables(); + this._sendVariablesTo(this._getGlobalVariables(), dataChannel); if (this.isHost) { - variables = Object.assign(variables, this.variables); + this._sendVariablesTo(this.variables, dataChannel); } + } + + _sendVariablesTo (variables, dataChannel) { Object.keys(variables).forEach(name => { - const value = variables[name]; + const variable = variables[name]; const message = { - owner: this.id, + owner: variable.owner, type: 'variable', data: { - name: name, - value: value + name: variable.name, + value: variable.value } }; dataChannel.send(JSON.stringify(message)); - console.log(`send variable: name=<${name}> value=<${value}>`); + console.log(`send variable: name=<${variable.name}> value=<${variable.value}> owner=<${variable.owner}>`); }); } @@ -592,7 +599,7 @@ class Scratch3MeshBlocks { console.log('ignore variable: reason='); } else { console.log(`update variable: name=<${variable.name}> from=<${this._getVariable(variable.name)}> to=<${variable.value}>`); - this._setVariable(variable.name, variable.value); + this._setVariable(variable.name, variable.value, message.owner); } break; default: @@ -605,11 +612,15 @@ class Scratch3MeshBlocks { console.log(`data channel close`); } - _setVariable (name, value) { + _setVariable (name, value, owner) { if (!this.variableNames.includes(name)) { this.variableNames.push(name); } - this.variables[name] = value; + this.variables[name] = { + name: name, + value: value, + owner: owner + }; } _getVariableNamesMenuItems () { From 2ae5127b6331cb15e5f05726ccb2d07dd5366bd9 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Mon, 13 Apr 2020 02:22:30 +0900 Subject: [PATCH 25/42] error handling. --- src/extensions/scratch3_mesh/index.js | 70 +++++++++++++++++---------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index fc7dd553d21..e832f8c8f08 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -199,15 +199,7 @@ class Scratch3MeshBlocks { this.rtcConnections.push(connection); connection.onconnectionstatechange = e => { - console.log(`Client: onconnectionstatechange: ${connection.connectionState}`); - - switch (connection.connectionState) { - case 'disconnected': - case 'failed': - case 'closed': - // TODO: this.rtcConnectionsから削除する - break; - } + this._onRtcConnectionStateChange(connection); }; connection.onicecandidate = e => { if (!e.candidate) { @@ -263,11 +255,6 @@ class Scratch3MeshBlocks { disconnect () { console.log(`disconnect in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); - if (!this._connected) { - console.warn('Mesh: Already disconnected'); - return; - } - if (this._websocket) { this._websocket.close(); } @@ -284,6 +271,8 @@ class Scratch3MeshBlocks { this._connected = false; this.runtime.emit(this.runtime.constructor.PERIPHERAL_DISCONNECTED); + + console.log(`disconnected in mesh`); } /** @@ -420,14 +409,27 @@ class Scratch3MeshBlocks { _onWebSocketMessage (message) { console.log(`received WebSocket message: isHost=<${this.isHost}> message=<${JSON.stringify(message)}>`); - if (message.hasOwnProperty('result') && !message.result) { - console.error(`failed action: action=<${message.action}> error=<${message.data.error}>`); + const {action, result, data} = message; + + if (message.hasOwnProperty('result') && !result) { + console.error(`failed action: action=<${action}> error=<${data.error}>`); + + switch (action) { + case 'offer': + if (!this.isHost) { + this.disconnect(); + + this.runtime.emit(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR, { + extensionId: Scratch3MeshBlocks.EXTENSION_ID + }); + } + break; + } return; } let connection; - const {action, data} = message; switch (action) { case 'register': if (!this.isHost) { @@ -483,15 +485,7 @@ class Scratch3MeshBlocks { this.rtcConnections.push(connection); connection.onconnectionstatechange = e => { - console.log(`Host: onconnectionstatechange: ${connection.connectionState}`); - - switch (connection.connectionState) { - case 'disconnected': - case 'failed': - case 'closed': - // TODO: this.rtcConnectionsから削除する - break; - } + this._onRtcConnectionStateChange(connection); }; connection.onicecandidate = e => { if (!e.candidate) { @@ -562,6 +556,17 @@ class Scratch3MeshBlocks { } } + _onRtcConnectionStateChange (connection) { + console.log(`onRtcConnectionStateChange: ${connection.connectionState}`); + + switch (connection.connectionState) { + case 'disconnected': + case 'failed': + connection.close(); + break; + } + } + _onRtcMessage (message) { console.log(`received WebRTC message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}> data=<${JSON.stringify(message.data)}>`); switch (message.type) { @@ -610,6 +615,19 @@ class Scratch3MeshBlocks { _onRtcClose (connection, dataChannel) { console.log(`data channel close`); + + this.rtcConnections = this.rtcConnections.filter(c => c !== connection); + this.rtcDataChannels = this.rtcDataChannels.filter(c => c !== dataChannel); + + if (!this.isHost && this._connected) { + this.disconnect(); + + this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, { + extensionId: Scratch3MeshBlocks.EXTENSION_ID + }); + } + + console.log(`data channel closed`); } _setVariable (name, value, owner) { From 18ad33c8af4117de4f6bbe382f52a3e65b01fa03 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Tue, 14 Apr 2020 11:27:09 +0900 Subject: [PATCH 26/42] use aws. --- src/extensions/scratch3_mesh/index.js | 96 +++++++++++++-------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index e832f8c8f08..799b63910d0 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -13,7 +13,7 @@ const Variable = require('../../engine/variable'); const blockIconURI = 'data:image/png;base64,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'; const MESH_HOST_PERIPHERAL_ID = 'mesh_host'; -const MESH_WSS_URL = 'ws://localhost:8080'; +const MESH_WSS_URL = 'wss://3rnikeqwld.execute-api.ap-northeast-1.amazonaws.com/dev'; /** * Host for the Mesh-related blocks @@ -73,10 +73,10 @@ class Scratch3MeshBlocks { this.isHost = false; /** - * ID + * Mesh ID * @type {string} */ - this.id = uid(); + this.meshId = uid(); this._setOpcodeFunctionHOC(); this._setVariableFunctionHOC(); @@ -99,7 +99,7 @@ class Scratch3MeshBlocks { id: 'mesh.hostPeripheralName', default: 'Host Mesh', description: 'label for "Host Mesh" in connect modal for Mesh extension' - }) + ` [${this.id.slice(0, 8)}]`, + }) + ` [${this.meshId.slice(0, 8)}]`, peripheralId: MESH_HOST_PERIPHERAL_ID, rssi: 0 }; @@ -108,10 +108,9 @@ class Scratch3MeshBlocks { const websocket = this._websocket; websocket.send(JSON.stringify({ - service: 'mesh', action: 'list', data: { - id: this.id + meshId: this.meshId } })); } else { @@ -124,10 +123,9 @@ class Scratch3MeshBlocks { ); websocket.send(JSON.stringify({ - service: 'mesh', action: 'list', data: { - id: this.id + meshId: this.meshId } })); }; @@ -154,32 +152,31 @@ class Scratch3MeshBlocks { /** * Called by the runtime when user wants to connect to a certain peripheral. - * @param {number} id - the id of the peripheral to connect to. + * @param {string} meshId - the Mesh ID of the peripheral to connect to. */ - connect (id) { + connect (meshId) { try { - console.log(`connect in mesh: ${id}`); + console.log(`connect in mesh: ${meshId}`); this._connected = false; - if (id === MESH_HOST_PERIPHERAL_ID) { + if (meshId === MESH_HOST_PERIPHERAL_ID) { if (!this._websocket) { console.error('WebSocket is not opened'); return; } this._websocket.send(JSON.stringify({ - service: 'mesh', action: 'register', data: { - id: this.id + meshId: this.meshId } })); this.isHost = true; } else { - if (!id || id.trim() === '') { - console.error('Not select id'); + if (!meshId || meshId.trim() === '') { + console.error('Not select Mesh ID'); return; } if (!this._websocket) { @@ -206,11 +203,10 @@ class Scratch3MeshBlocks { console.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); this._websocket.send(JSON.stringify({ - service: 'mesh', action: 'offer', data: { - id: this.id, - hostId: id, + meshId: this.meshId, + hostMeshId: meshId, clientDescription: connection.localDescription } })); @@ -266,7 +262,7 @@ class Scratch3MeshBlocks { this.rtcConnections = []; this.rtcDataChannels = []; this.isHost = false; - this._hostId = null; + this._hostMeshId = null; this._connected = false; @@ -293,23 +289,23 @@ class Scratch3MeshBlocks { console.log(`connectedMessage in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); let message; - let id; + let meshId; if (this.isHost) { message = formatMessage({ id: 'mesh.registeredHost', - default: 'Registered Host Mesh [[ID]]', + default: 'Registered Host Mesh [[MESH_ID]]', description: 'label for registered Host Mesh in connect modal for Mesh extension' }); - id = this.id; + meshId = this.meshId; } else { message = formatMessage({ id: 'mesh.joinedMesh', - default: 'Joined Mesh [[ID]]', + default: 'Joined Mesh [[MESH_ID]]', description: 'label for joined Mesh in connect modal for Mesh extension' }); - id = this._hostId; + meshId = this._hostMeshId; } - return message.replace('[ID]', id.slice(0, 8)); + return message.replace('[MESH_ID]', meshId.slice(0, 8)); } /** @@ -371,7 +367,7 @@ class Scratch3MeshBlocks { variables[currVar.name] = { name: currVar.name, value: currVar.value, - owner: this.id + owner: this.meshId }; } } @@ -441,28 +437,28 @@ class Scratch3MeshBlocks { this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); break; case 'list': - const now = new Date(); - data.hostIds.forEach(hostId => { - const t = now - Date.parse(hostId.updatedAt); + const now = Math.floor(Date.now() / 1000); + data.hosts.forEach(host => { + const t = host.ttl - now; let rssi; - if (t <= 1 * 60 * 1000) { + if (t >= 4 * 60) { rssi = 0; - } else if (t <= 3 * 60 * 1000) { + } else if (t >= 3 * 60) { rssi = -20; - } else if (t <= 15 * 60 * 1000) { + } else if (t >= 2 * 60) { rssi = -40; - } else if (t <= 30 * 60 * 1000) { + } else if (t >= 1 * 60) { rssi = -60; } else { rssi = -80; } - this._availablePeripherals[hostId.id] = { + this._availablePeripherals[host.meshId] = { name: formatMessage({ id: 'mesh.clientPeripheralName', default: 'Join Mesh', description: 'label for "Join Mesh" in connect modal for Mesh extension' - }) + ` [${hostId.id.slice(0, 8)}]`, - peripheralId: hostId.id, + }) + ` [${host.meshId.slice(0, 8)}]`, + peripheralId: host.meshId, rssi: rssi }; }); @@ -477,8 +473,8 @@ class Scratch3MeshBlocks { console.error(`failed action: action=<${message.action}> reason=`); return; } - if (data.hostId !== this.id) { - console.error(`failed action: action=<${message.action}> reason= actual=<${data.hostId}> expected=<${this.id}>`); + if (data.hostMeshId !== this.meshId) { + console.error(`failed action: action=<${message.action}> reason= actual=<${data.hostMeshId}> expected=<${this.meshId}>`); } connection = new RTCPeerConnection(null); @@ -492,11 +488,10 @@ class Scratch3MeshBlocks { console.log(`Host: answer to Client:\n${JSON.stringify(connection.localDescription)}`); this._websocket.send(JSON.stringify({ - service: 'mesh', action: 'answer', data: { - id: this.id, - clientId: data.id, + meshId: this.meshId, + clientMeshId: data.meshId, hostDescription: connection.localDescription } })); @@ -532,12 +527,12 @@ class Scratch3MeshBlocks { console.error(`failed action: action=<${message.action}> reason=`); return; } - if (data.clientId !== this.id) { - console.error(`failed action: action=<${message.action}> reason= actual=<${data.clientId}> expected=<${this.id}>`); + if (data.clientMeshId !== this.meshId) { + console.error(`failed action: action=<${message.action}> reason= actual=<${data.clientMeshId}> expected=<${this.meshId}>`); return; } - if (this.rtcConnections.length == 0) { + if (this.rtcConnections.length === 0) { console.error(`failed action: action=<${message.action}> reason=`); return; } @@ -549,7 +544,7 @@ class Scratch3MeshBlocks { this._websocket.close(); - this._hostId = data.id; + this._hostMeshId = data.meshId; this._connected = true; this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); break; @@ -578,7 +573,7 @@ class Scratch3MeshBlocks { this._sendMessage(message); } - if (this.id == message.owner) { + if (this.meshId === message.owner) { console.log('ignore broadcast: reason='); } else { console.log('process broadcast'); @@ -589,6 +584,7 @@ class Scratch3MeshBlocks { name: broadcastName } }; + // TODO: 1度もブロックを実行していないときに BlockUtility.lastInstance が null であるため期待通りに動作しない。 this._opcodeFunctions['event_broadcast'](args, BlockUtility.lastInstance()); } break; @@ -600,7 +596,7 @@ class Scratch3MeshBlocks { this._sendMessage(message); } - if (this.id == message.owner) { + if (this.meshId === message.owner) { console.log('ignore variable: reason='); } else { console.log(`update variable: name=<${variable.name}> from=<${this._getVariable(variable.name)}> to=<${variable.value}>`); @@ -673,7 +669,7 @@ class Scratch3MeshBlocks { try { const broadcastName = args.BROADCAST_OPTION.name; this._sendMessage({ - owner: this.id, + owner: this.meshId, type: 'broadcast', data: broadcastName }); @@ -718,7 +714,7 @@ class Scratch3MeshBlocks { _sendVariable (name, value) { try { this._sendMessage({ - owner: this.id, + owner: this.meshId, type: 'variable', data: { name: name, From fca2dd0cc2f556be28b6e6c8dcf4c7e83db13f20 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Tue, 14 Apr 2020 14:03:49 +0900 Subject: [PATCH 27/42] ignore offer and answer response. --- src/extensions/scratch3_mesh/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 799b63910d0..a9903a66848 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -470,9 +470,9 @@ class Scratch3MeshBlocks { break; case 'offer': if (!this.isHost) { - console.error(`failed action: action=<${message.action}> reason=`); return; } + if (data.hostMeshId !== this.meshId) { console.error(`failed action: action=<${message.action}> reason= actual=<${data.hostMeshId}> expected=<${this.meshId}>`); } @@ -524,9 +524,9 @@ class Scratch3MeshBlocks { break; case 'answer': if (this.isHost) { - console.error(`failed action: action=<${message.action}> reason=`); return; } + if (data.clientMeshId !== this.meshId) { console.error(`failed action: action=<${message.action}> reason= actual=<${data.clientMeshId}> expected=<${this.meshId}>`); return; From e1f3aa5b760b6e349bd92b971f5efcd29ffc6faa Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Tue, 14 Apr 2020 14:52:17 +0900 Subject: [PATCH 28/42] fixed when received broadcast from Mesh never run blocks. --- src/engine/block-utility.js | 4 ++++ src/extensions/scratch3_mesh/index.js | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/engine/block-utility.js b/src/engine/block-utility.js index 923e3862b99..7ad9c61ce9b 100644 --- a/src/engine/block-utility.js +++ b/src/engine/block-utility.js @@ -240,6 +240,10 @@ class BlockUtility { } } + /** + * Returns BlockUtility instance to use same instance in runtime and extension. + * @return {BlockUtility} The BlockUtility instance. + */ static lastInstance () { return lastInstance; } diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index a9903a66848..84702d11f27 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -1,7 +1,7 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const formatMessage = require('format-message'); -const BlockUtility = require('../../engine/block-utility'); +const BlockUtility = require('../../engine/block-utility.js'); const uid = require('../../util/uid'); const Variable = require('../../engine/variable'); @@ -584,8 +584,11 @@ class Scratch3MeshBlocks { name: broadcastName } }; - // TODO: 1度もブロックを実行していないときに BlockUtility.lastInstance が null であるため期待通りに動作しない。 - this._opcodeFunctions['event_broadcast'](args, BlockUtility.lastInstance()); + const util = BlockUtility.lastInstance(); + if (!util.sequencer) { + util.sequencer = this.runtime.sequencer; + } + this._opcodeFunctions['event_broadcast'](args, util); } break; case 'variable': From f2ed1c5ea5c75f0ed8fdaf5ea379a27393a4c099 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Tue, 14 Apr 2020 19:53:39 +0900 Subject: [PATCH 29/42] use formatMessage format. --- src/extensions/scratch3_mesh/index.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 84702d11f27..5acf2504249 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -97,9 +97,9 @@ class Scratch3MeshBlocks { this._availablePeripherals[MESH_HOST_PERIPHERAL_ID] = { name: formatMessage({ id: 'mesh.hostPeripheralName', - default: 'Host Mesh', + default: 'Host Mesh [{ MESH_ID }]', description: 'label for "Host Mesh" in connect modal for Mesh extension' - }) + ` [${this.meshId.slice(0, 8)}]`, + }, { MESH_ID: this._makeMeshIdLabel(this.meshId) }), peripheralId: MESH_HOST_PERIPHERAL_ID, rssi: 0 }; @@ -293,19 +293,21 @@ class Scratch3MeshBlocks { if (this.isHost) { message = formatMessage({ id: 'mesh.registeredHost', - default: 'Registered Host Mesh [[MESH_ID]]', + default: 'Registered Host Mesh [{ MESH_ID }]', description: 'label for registered Host Mesh in connect modal for Mesh extension' - }); - meshId = this.meshId; + }, { MESH_ID: this._makeMeshIdLabel(this.meshId) }); } else { message = formatMessage({ id: 'mesh.joinedMesh', - default: 'Joined Mesh [[MESH_ID]]', + default: 'Joined Mesh [{ MESH_ID }]', description: 'label for joined Mesh in connect modal for Mesh extension' - }); - meshId = this._hostMeshId; + }, { MESH_ID: this._makeMeshIdLabel(this._hostMeshId) }); } - return message.replace('[MESH_ID]', meshId.slice(0, 8)); + return message; + } + + _makeMeshIdLabel (meshId) { + return meshId.slice(0, 6); } /** @@ -455,9 +457,9 @@ class Scratch3MeshBlocks { this._availablePeripherals[host.meshId] = { name: formatMessage({ id: 'mesh.clientPeripheralName', - default: 'Join Mesh', + default: 'Join Mesh [{ MESH_ID }]', description: 'label for "Join Mesh" in connect modal for Mesh extension' - }) + ` [${host.meshId.slice(0, 8)}]`, + }, { MESH_ID: this._makeMeshIdLabel(host.meshId) }), peripheralId: host.meshId, rssi: rssi }; From 0ee001fe29e9134155208a055b8b2971fe38dec2 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Tue, 14 Apr 2020 21:32:58 +0900 Subject: [PATCH 30/42] fixed lint --- src/extensions/scratch3_mesh/index.js | 60 ++++++++++++--------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 5acf2504249..9023624dc24 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -99,7 +99,7 @@ class Scratch3MeshBlocks { id: 'mesh.hostPeripheralName', default: 'Host Mesh [{ MESH_ID }]', description: 'label for "Host Mesh" in connect modal for Mesh extension' - }, { MESH_ID: this._makeMeshIdLabel(this.meshId) }), + }, {MESH_ID: this._makeMeshIdLabel(this.meshId)}), peripheralId: MESH_HOST_PERIPHERAL_ID, rssi: 0 }; @@ -132,8 +132,7 @@ class Scratch3MeshBlocks { websocket.onmessage = e => { try { this._onWebSocketMessage(JSON.parse(e.data)); - } - catch (error) { + } catch (error) { console.error(`Error in WebSocket.onmessage: ${error}`); } }; @@ -144,8 +143,7 @@ class Scratch3MeshBlocks { console.error(`Error in WebSocket: ${e}`); }; } - } - catch (e) { + } catch (e) { console.error(`Error in scan: ${e}`); } } @@ -220,8 +218,7 @@ class Scratch3MeshBlocks { dataChannel.onmessage = e => { try { this._onRtcMessage(JSON.parse(e.data)); - } - catch (error) { + } catch (error) { console.error(`Client: Error in dataChannel.onmessage: ${error}`); } }; @@ -230,17 +227,16 @@ class Scratch3MeshBlocks { }; connection.createOffer().then( - (desc) => { + desc => { connection.setLocalDescription(desc); }, - (error) => { + error => { // TODO: エラー処理 console.error(`Client: Error in createOffer: ${error}`); } ); } - } - catch (e) { + } catch (e) { console.error(`Error in connect: ${e}`); } } @@ -295,13 +291,13 @@ class Scratch3MeshBlocks { id: 'mesh.registeredHost', default: 'Registered Host Mesh [{ MESH_ID }]', description: 'label for registered Host Mesh in connect modal for Mesh extension' - }, { MESH_ID: this._makeMeshIdLabel(this.meshId) }); + }, {MESH_ID: this._makeMeshIdLabel(this.meshId)}); } else { message = formatMessage({ id: 'mesh.joinedMesh', default: 'Joined Mesh [{ MESH_ID }]', description: 'label for joined Mesh in connect modal for Mesh extension' - }, { MESH_ID: this._makeMeshIdLabel(this._hostMeshId) }); + }, {MESH_ID: this._makeMeshIdLabel(this._hostMeshId)}); } return message; } @@ -347,7 +343,7 @@ class Scratch3MeshBlocks { } /** - * @return {Object} - the global variable's value from other projects + * @return {object} - the global variable's value from other projects */ getSensorValue (args) { return this._getVariable(args.NAME); @@ -459,7 +455,7 @@ class Scratch3MeshBlocks { id: 'mesh.clientPeripheralName', default: 'Join Mesh [{ MESH_ID }]', description: 'label for "Join Mesh" in connect modal for Mesh extension' - }, { MESH_ID: this._makeMeshIdLabel(host.meshId) }), + }, {MESH_ID: this._makeMeshIdLabel(host.meshId)}), peripheralId: host.meshId, rssi: rssi }; @@ -515,10 +511,10 @@ class Scratch3MeshBlocks { connection.setRemoteDescription(new RTCSessionDescription(data.clientDescription)); connection.createAnswer().then( - (desc) => { + desc => { connection.setLocalDescription(desc); }, - (error) => { + error => { // TODO: エラー処理 console.error(`Host: Error in createAnswer: ${error}`); } @@ -590,7 +586,7 @@ class Scratch3MeshBlocks { if (!util.sequencer) { util.sequencer = this.runtime.sequencer; } - this._opcodeFunctions['event_broadcast'](args, util); + this._opcodeFunctions.event_broadcast(args, util); } break; case 'variable': @@ -651,8 +647,7 @@ class Scratch3MeshBlocks { this.rtcDataChannels.forEach(channel => { channel.send(JSON.stringify(message)); }); - } - catch (e) { + } catch (e) { // TODO: エラー処理 console.log(e); } @@ -661,13 +656,13 @@ class Scratch3MeshBlocks { _broadcast (args, util) { console.log('event_broadcast in mesh'); this._sendBroadcast(args); - this._opcodeFunctions['event_broadcast'](args, util); + this._opcodeFunctions.event_broadcast(args, util); } _broadcastAndWait (args, util) { console.log('event_broadcastandwait in mesh'); this._sendBroadcast(args); - this._opcodeFunctions['event_broadcastandwait'](args, util); + this._opcodeFunctions.event_broadcastandwait(args, util); } _sendBroadcast (args) { @@ -678,8 +673,7 @@ class Scratch3MeshBlocks { type: 'broadcast', data: broadcastName }); - } - catch (e) { + } catch (e) { // TODO: エラー処理 console.log(e); } @@ -687,13 +681,13 @@ class Scratch3MeshBlocks { _setVariableTo (args, util) { console.log('data_setvariableto in mesh'); - this._opcodeFunctions['data_setvariableto'](args, util); + this._opcodeFunctions.data_setvariableto(args, util); this._sendVariableByOpcodeFunction(args, util); } _changeVariableBy (args, util) { console.log('data_changevariableby in mesh'); - this._opcodeFunctions['data_changevariableby'](args, util); + this._opcodeFunctions.data_changevariableby(args, util); this._sendVariableByOpcodeFunction(args, util); } @@ -709,8 +703,7 @@ class Scratch3MeshBlocks { } this._sendVariable(variable.name, variable.value); - } - catch (e) { + } catch (e) { // TODO: エラー処理 console.log(e); } @@ -726,8 +719,7 @@ class Scratch3MeshBlocks { value: value } }); - } - catch (e) { + } catch (e) { // TODO: エラー処理 console.log(e); } @@ -741,10 +733,10 @@ class Scratch3MeshBlocks { data_changevariableby: this.runtime.getOpcodeFunction('data_changevariableby') }; - this.runtime._primitives['event_broadcast'] = this._broadcast.bind(this); - this.runtime._primitives['event_broadcastandwait'] = this._broadcastAndWait.bind(this); - this.runtime._primitives['data_setvariableto'] = this._setVariableTo.bind(this); - this.runtime._primitives['data_changevariableby'] = this._changeVariableBy.bind(this); + this.runtime._primitives.event_broadcast = this._broadcast.bind(this); + this.runtime._primitives.event_broadcastandwait = this._broadcastAndWait.bind(this); + this.runtime._primitives.data_setvariableto = this._setVariableTo.bind(this); + this.runtime._primitives.data_changevariableby = this._changeVariableBy.bind(this); } _setVariableFunctionHOC () { From 735a10308b481db45048982a53e5a64f36c8cc7d Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Tue, 14 Apr 2020 22:23:28 +0900 Subject: [PATCH 31/42] fixed lint. --- src/extensions/scratch3_mesh/index.js | 246 ++++++++++++++------------ 1 file changed, 133 insertions(+), 113 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 9023624dc24..2ab0552b3a4 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -1,5 +1,6 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); +const log = require('../../util/log'); const formatMessage = require('format-message'); const BlockUtility = require('../../engine/block-utility.js'); const uid = require('../../util/uid'); @@ -90,7 +91,7 @@ class Scratch3MeshBlocks { * Called by the runtime when user wants to scan for a peripheral. */ scan () { - console.log('scan in mesh'); + log.log('scan in mesh'); try { this._availablePeripherals = {}; @@ -116,7 +117,7 @@ class Scratch3MeshBlocks { } else { this._websocket = new WebSocket(MESH_WSS_URL); const websocket = this._websocket; - websocket.onopen = e => { + websocket.onopen = () => { this.runtime.emit( this.runtime.constructor.PERIPHERAL_LIST_UPDATE, this._availablePeripherals @@ -133,18 +134,18 @@ class Scratch3MeshBlocks { try { this._onWebSocketMessage(JSON.parse(e.data)); } catch (error) { - console.error(`Error in WebSocket.onmessage: ${error}`); + log.error(`Error in WebSocket.onmessage: ${error}`); } }; - websocket.onclose = e => { + websocket.onclose = () => { this._websocket = null; }; websocket.onerror = e => { - console.error(`Error in WebSocket: ${e}`); + log.error(`Error in WebSocket: ${e}`); }; } } catch (e) { - console.error(`Error in scan: ${e}`); + log.error(`Error in scan: ${e}`); } } @@ -154,13 +155,13 @@ class Scratch3MeshBlocks { */ connect (meshId) { try { - console.log(`connect in mesh: ${meshId}`); + log.log(`connect in mesh: ${meshId}`); this._connected = false; if (meshId === MESH_HOST_PERIPHERAL_ID) { if (!this._websocket) { - console.error('WebSocket is not opened'); + log.error('WebSocket is not opened'); return; } @@ -174,31 +175,31 @@ class Scratch3MeshBlocks { this.isHost = true; } else { if (!meshId || meshId.trim() === '') { - console.error('Not select Mesh ID'); + log.error('Not select Mesh ID'); return; } if (!this._websocket) { - console.error('WebSocket is not opened'); + log.error('WebSocket is not opened'); return; } if (this.isHost) { - console.error('I am not Client'); + log.error('I am not Client'); return; } if (this.rtcConnections.length > 0) { - console.error('Already WebRTC connected'); + log.error('Already WebRTC connected'); return; } const connection = new RTCPeerConnection(null); this.rtcConnections.push(connection); - connection.onconnectionstatechange = e => { + connection.onconnectionstatechange = () => { this._onRtcConnectionStateChange(connection); }; connection.onicecandidate = e => { if (!e.candidate) { - console.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); + log.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); this._websocket.send(JSON.stringify({ action: 'offer', @@ -212,17 +213,13 @@ class Scratch3MeshBlocks { }; const dataChannel = connection.createDataChannel('dataChannel'); - dataChannel.onopen = e => { + dataChannel.onopen = () => { this._onRtcOpen(connection, dataChannel); }; dataChannel.onmessage = e => { - try { - this._onRtcMessage(JSON.parse(e.data)); - } catch (error) { - console.error(`Client: Error in dataChannel.onmessage: ${error}`); - } + this._onRtcMessage(e.data); }; - dataChannel.onclose = e => { + dataChannel.onclose = () => { this._onRtcClose(connection, dataChannel); }; @@ -231,13 +228,17 @@ class Scratch3MeshBlocks { connection.setLocalDescription(desc); }, error => { - // TODO: エラー処理 - console.error(`Client: Error in createOffer: ${error}`); + log.error(`Client: Error in createOffer: ${error}`); + + this.disconnect(); + this.runtime.emit(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR, { + extensionId: Scratch3MeshBlocks.EXTENSION_ID + }); } ); } } catch (e) { - console.error(`Error in connect: ${e}`); + log.error(`Error in connect: ${e}`); } } @@ -245,7 +246,7 @@ class Scratch3MeshBlocks { * Disconnect from the Mesh. */ disconnect () { - console.log(`disconnect in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); + log.log(`disconnect in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); if (this._websocket) { this._websocket.close(); @@ -264,7 +265,7 @@ class Scratch3MeshBlocks { this.runtime.emit(this.runtime.constructor.PERIPHERAL_DISCONNECTED); - console.log(`disconnected in mesh`); + log.log(`disconnected in mesh`); } /** @@ -272,7 +273,7 @@ class Scratch3MeshBlocks { * @return {boolean} - whether the Mesh is connected. */ isConnected () { - console.log(`isConnected in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); + log.log(`isConnected in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); return this._connected; } @@ -282,10 +283,9 @@ class Scratch3MeshBlocks { * @return {string} - connected message. */ connectedMessage () { - console.log(`connectedMessage in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); + log.log(`connectedMessage in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); let message; - let meshId; if (this.isHost) { message = formatMessage({ id: 'mesh.registeredHost', @@ -342,9 +342,6 @@ class Scratch3MeshBlocks { }; } - /** - * @return {object} - the global variable's value from other projects - */ getSensorValue (args) { return this._getVariable(args.NAME); } @@ -373,7 +370,7 @@ class Scratch3MeshBlocks { } _onRtcOpen (connection, dataChannel) { - console.log(`data channel open`); + log.log(`data channel open`); this.rtcDataChannels.push(dataChannel); @@ -396,17 +393,17 @@ class Scratch3MeshBlocks { } }; dataChannel.send(JSON.stringify(message)); - console.log(`send variable: name=<${variable.name}> value=<${variable.value}> owner=<${variable.owner}>`); + log.log(`send variable: name=<${variable.name}> value=<${variable.value}> owner=<${variable.owner}>`); }); } _onWebSocketMessage (message) { - console.log(`received WebSocket message: isHost=<${this.isHost}> message=<${JSON.stringify(message)}>`); + log.log(`received WebSocket message: isHost=<${this.isHost}> message=<${JSON.stringify(message)}>`); const {action, result, data} = message; if (message.hasOwnProperty('result') && !result) { - console.error(`failed action: action=<${action}> error=<${data.error}>`); + log.error(`failed action: action=<${action}> error=<${data.error}>`); switch (action) { case 'offer': @@ -423,11 +420,12 @@ class Scratch3MeshBlocks { } let connection; + let now; switch (action) { case 'register': if (!this.isHost) { - console.error(`failed action: action=<${message.action}> reason=`); + log.error(`failed action: action=<${message.action}> reason=`); return; } @@ -435,7 +433,7 @@ class Scratch3MeshBlocks { this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); break; case 'list': - const now = Math.floor(Date.now() / 1000); + now = Math.floor(Date.now() / 1000); data.hosts.forEach(host => { const t = host.ttl - now; let rssi; @@ -472,18 +470,19 @@ class Scratch3MeshBlocks { } if (data.hostMeshId !== this.meshId) { - console.error(`failed action: action=<${message.action}> reason= actual=<${data.hostMeshId}> expected=<${this.meshId}>`); + log.error(`failed action: action=<${message.action}> reason= ` + + `actual=<${data.hostMeshId}> expected=<${this.meshId}>`); } connection = new RTCPeerConnection(null); this.rtcConnections.push(connection); - connection.onconnectionstatechange = e => { + connection.onconnectionstatechange = () => { this._onRtcConnectionStateChange(connection); }; connection.onicecandidate = e => { if (!e.candidate) { - console.log(`Host: answer to Client:\n${JSON.stringify(connection.localDescription)}`); + log.log(`Host: answer to Client:\n${JSON.stringify(connection.localDescription)}`); this._websocket.send(JSON.stringify({ action: 'answer', @@ -498,13 +497,13 @@ class Scratch3MeshBlocks { connection.ondatachannel = event => { const dataChannel = event.channel; - dataChannel.onopen = e => { + dataChannel.onopen = () => { this._onRtcOpen(connection, dataChannel); }; dataChannel.onmessage = e => { - this._onRtcMessage(JSON.parse(e.data)); + this._onRtcMessage(e.data); }; - dataChannel.onclose = e => { + dataChannel.onclose = () => { this._onRtcClose(connection, dataChannel); }; }; @@ -515,8 +514,9 @@ class Scratch3MeshBlocks { connection.setLocalDescription(desc); }, error => { - // TODO: エラー処理 - console.error(`Host: Error in createAnswer: ${error}`); + log.error(`Host: Error in createAnswer: ${error}`); + connection.close(); + this.rtcConnections = this.rtcConnections.filter(c => c !== connection); } ); break; @@ -526,16 +526,17 @@ class Scratch3MeshBlocks { } if (data.clientMeshId !== this.meshId) { - console.error(`failed action: action=<${message.action}> reason= actual=<${data.clientMeshId}> expected=<${this.meshId}>`); + log.error(`failed action: action=<${message.action}> reason=` + + ` actual=<${data.clientMeshId}> expected=<${this.meshId}>`); return; } if (this.rtcConnections.length === 0) { - console.error(`failed action: action=<${message.action}> reason=`); + log.error(`failed action: action=<${message.action}> reason=`); return; } - console.log('Client: set Host description'); + log.log('Client: set Host description'); connection = this.rtcConnections[0]; connection.setRemoteDescription(new RTCSessionDescription(data.hostDescription)); @@ -550,7 +551,7 @@ class Scratch3MeshBlocks { } _onRtcConnectionStateChange (connection) { - console.log(`onRtcConnectionStateChange: ${connection.connectionState}`); + log.log(`onRtcConnectionStateChange: ${connection.connectionState}`); switch (connection.connectionState) { case 'disconnected': @@ -560,21 +561,34 @@ class Scratch3MeshBlocks { } } - _onRtcMessage (message) { - console.log(`received WebRTC message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}> data=<${JSON.stringify(message.data)}>`); + _onRtcMessage (data) { + let message; + try { + message = JSON.parse(data); + } catch (error) { + log.error(`Invalid WebRTC message format: ${error}`); + return; + } + + log.log(`received WebRTC message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}>` + + ` data=<${JSON.stringify(message.data)}>`); + + let broadcastName; + let variable; switch (message.type) { case 'broadcast': - const broadcastName = message.data; - console.log(`received broadcast: ${broadcastName}`); + broadcastName = message.data; + log.log(`received broadcast: ${broadcastName}`); + if (this.isHost) { - console.log('send broadcast all clients'); + log.log('send broadcast all clients'); this._sendMessage(message); } if (this.meshId === message.owner) { - console.log('ignore broadcast: reason='); + log.log('ignore broadcast: reason='); } else { - console.log('process broadcast'); + log.log('process broadcast'); const args = { BROADCAST_OPTION: { @@ -590,28 +604,30 @@ class Scratch3MeshBlocks { } break; case 'variable': - const variable = message.data; - console.log(`received variable: name=<${variable.name}> value=<${variable.value}>`); + variable = message.data; + log.log(`received variable: name=<${variable.name}> value=<${variable.value}>`); + if (this.isHost) { - console.log('send variable all clients'); + log.log('send variable all clients'); this._sendMessage(message); } if (this.meshId === message.owner) { - console.log('ignore variable: reason='); + log.log('ignore variable: reason='); } else { - console.log(`update variable: name=<${variable.name}> from=<${this._getVariable(variable.name)}> to=<${variable.value}>`); + log.log(`update variable: name=<${variable.name}>` + + ` from=<${this._getVariable(variable.name)}> to=<${variable.value}>`); this._setVariable(variable.name, variable.value, message.owner); } break; default: - console.error(`invalid message type: ${message.type}`); + log.error(`invalid message type: ${message.type}`); break; } } _onRtcClose (connection, dataChannel) { - console.log(`data channel close`); + log.log(`data channel close`); this.rtcConnections = this.rtcConnections.filter(c => c !== connection); this.rtcDataChannels = this.rtcDataChannels.filter(c => c !== dataChannel); @@ -624,7 +640,7 @@ class Scratch3MeshBlocks { }); } - console.log(`data channel closed`); + log.log(`data channel closed`); } _setVariable (name, value, owner) { @@ -648,65 +664,70 @@ class Scratch3MeshBlocks { channel.send(JSON.stringify(message)); }); } catch (e) { - // TODO: エラー処理 - console.log(e); + log.error(`Failed to send WebRTC message: error=<${e}> message=<${JSON.stringify(message)}>`); } } _broadcast (args, util) { - console.log('event_broadcast in mesh'); - this._sendBroadcast(args); - this._opcodeFunctions.event_broadcast(args, util); + try { + log.log('event_broadcast in mesh'); + this._sendBroadcast(args); + this._opcodeFunctions.event_broadcast(args, util); + } catch (error) { + log.error(`Failed to execute event_broadcast: ${error}`); + } } _broadcastAndWait (args, util) { - console.log('event_broadcastandwait in mesh'); - this._sendBroadcast(args); - this._opcodeFunctions.event_broadcastandwait(args, util); + try { + log.log('event_broadcastandwait in mesh'); + this._sendBroadcast(args); + this._opcodeFunctions.event_broadcastandwait(args, util); + } catch (error) { + log.error(`Failed to execute event_broadcastandwait: ${error}`); + } } _sendBroadcast (args) { - try { - const broadcastName = args.BROADCAST_OPTION.name; - this._sendMessage({ - owner: this.meshId, - type: 'broadcast', - data: broadcastName - }); - } catch (e) { - // TODO: エラー処理 - console.log(e); - } + const broadcastName = args.BROADCAST_OPTION.name; + this._sendMessage({ + owner: this.meshId, + type: 'broadcast', + data: broadcastName + }); } _setVariableTo (args, util) { - console.log('data_setvariableto in mesh'); - this._opcodeFunctions.data_setvariableto(args, util); - this._sendVariableByOpcodeFunction(args, util); + try { + log.log('data_setvariableto in mesh'); + this._opcodeFunctions.data_setvariableto(args, util); + this._sendVariableByOpcodeFunction(args); + } catch (error) { + log.error(`Failed to execute data_setvariableto: ${error}`); + } } _changeVariableBy (args, util) { - console.log('data_changevariableby in mesh'); - this._opcodeFunctions.data_changevariableby(args, util); - this._sendVariableByOpcodeFunction(args, util); - } - - _sendVariableByOpcodeFunction (args, util) { try { - const stage = this.runtime.getTargetForStage(); - let variable = stage.lookupVariableById(args.VARIABLE.id); - if (!variable) { - variable = stage.lookupVariableByNameAndType(args.VARIABLE.name, Variable.SCALAR_TYPE); - } - if (!variable) { - return; - } + log.log('data_changevariableby in mesh'); + this._opcodeFunctions.data_changevariableby(args, util); + this._sendVariableByOpcodeFunction(args); + } catch (error) { + log.error(`Failed to execute data_changevariableby: ${error}`); + } + } - this._sendVariable(variable.name, variable.value); - } catch (e) { - // TODO: エラー処理 - console.log(e); + _sendVariableByOpcodeFunction (args) { + const stage = this.runtime.getTargetForStage(); + let variable = stage.lookupVariableById(args.VARIABLE.id); + if (!variable) { + variable = stage.lookupVariableByNameAndType(args.VARIABLE.name, Variable.SCALAR_TYPE); + } + if (!variable) { + return; } + + this._sendVariable(variable.name, variable.value); } _sendVariable (name, value) { @@ -719,9 +740,8 @@ class Scratch3MeshBlocks { value: value } }); - } catch (e) { - // TODO: エラー処理 - console.log(e); + } catch (error) { + log.error(`Failed to send variable: error=<${error}> name=<${name}> value=<${value}>`); } } @@ -762,7 +782,7 @@ class Scratch3MeshBlocks { } _createNewGlobalVariable (variableName, optVarId, optVarType) { - console.log('runtime.createNewGlobalVariable in mesh'); + log.log('runtime.createNewGlobalVariable in mesh'); const variable = this._variableFunctions.runtime.createNewGlobalVariable(variableName, optVarId, optVarType); if (variable.type === Variable.SCALAR_TYPE) { this._sendVariable(variable.name, variable.value); @@ -771,7 +791,7 @@ class Scratch3MeshBlocks { } _lookupOrCreateVariable (id, name) { - console.log('stage.lookupOrCreateVariable in mesh'); + log.log('stage.lookupOrCreateVariable in mesh'); const stage = this.runtime.getTargetForStage(); let variable = stage.lookupVariableById(id); @@ -788,7 +808,7 @@ class Scratch3MeshBlocks { } _createVariable (id, name, type, isCloud) { - console.log('stage.createVariable in mesh'); + log.log('stage.createVariable in mesh'); const stage = this.runtime.getTargetForStage(); if (!stage.variables.hasOwnProperty(id)) { @@ -801,7 +821,7 @@ class Scratch3MeshBlocks { } _setVariableValue (id, newValue) { - console.log('stage.setVariableValue in mesh'); + log.log('stage.setVariableValue in mesh'); const stage = this.runtime.getTargetForStage(); if (stage.variables.hasOwnProperty(id)) { @@ -816,7 +836,7 @@ class Scratch3MeshBlocks { } _renameVariable (id, newName) { - console.log('stage.renameVariable in mesh'); + log.log('stage.renameVariable in mesh'); const stage = this.runtime.getTargetForStage(); if (stage.variables.hasOwnProperty(id)) { From 2dc8a05d404f68be4ff9497517bb6980c3985c7d Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Tue, 14 Apr 2020 22:29:08 +0900 Subject: [PATCH 32/42] fixed for lint. --- src/util/maybe-format-message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/maybe-format-message.js b/src/util/maybe-format-message.js index acb5eef133f..7309876caf3 100644 --- a/src/util/maybe-format-message.js +++ b/src/util/maybe-format-message.js @@ -10,7 +10,7 @@ const formatMessage = require('format-message'); */ const maybeFormatMessage = function (maybeMessage, args, locale) { if (maybeMessage && maybeMessage.id && maybeMessage.default) { - return formatMessage(maybeMessage, args, locale); + return formatMessage(maybeMessage, args, locale); // eslint-disable-line format-message } return maybeMessage; }; From bc99e5617b147de0fcd533c92975190954d3c902 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Wed, 15 Apr 2020 18:28:04 +0900 Subject: [PATCH 33/42] use stun servers. --- src/extensions/scratch3_mesh/index.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 2ab0552b3a4..45cd1272860 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -15,6 +15,17 @@ const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYA const MESH_HOST_PERIPHERAL_ID = 'mesh_host'; const MESH_WSS_URL = 'wss://3rnikeqwld.execute-api.ap-northeast-1.amazonaws.com/dev'; +const ICE_SERVERS = [ + { + urls: [ + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302" + ] + } +]; /** * Host for the Mesh-related blocks @@ -191,14 +202,16 @@ class Scratch3MeshBlocks { return; } - const connection = new RTCPeerConnection(null); + const connection = new RTCPeerConnection({iceServers: ICE_SERVERS}); this.rtcConnections.push(connection); connection.onconnectionstatechange = () => { this._onRtcConnectionStateChange(connection); }; connection.onicecandidate = e => { - if (!e.candidate) { + if (e.candidate) { + log.log('ICE candidate', JSON.stringify(e.candidate, null, 2)); + } else { log.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); this._websocket.send(JSON.stringify({ @@ -474,14 +487,16 @@ class Scratch3MeshBlocks { `actual=<${data.hostMeshId}> expected=<${this.meshId}>`); } - connection = new RTCPeerConnection(null); + connection = new RTCPeerConnection({iceServers: ICE_SERVERS}); this.rtcConnections.push(connection); connection.onconnectionstatechange = () => { this._onRtcConnectionStateChange(connection); }; connection.onicecandidate = e => { - if (!e.candidate) { + if (e.candidate) { + log.log('ICE candidate', JSON.stringify(e.candidate, null, 2)); + } else { log.log(`Host: answer to Client:\n${JSON.stringify(connection.localDescription)}`); this._websocket.send(JSON.stringify({ From 409d2e3f9d67aeb5f86bc90a4aee9d24b370684a Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Wed, 15 Apr 2020 19:28:21 +0900 Subject: [PATCH 34/42] use uuid instead uid. --- src/extensions/scratch3_mesh/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 45cd1272860..ecefb7917b4 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -3,7 +3,8 @@ const BlockType = require('../../extension-support/block-type'); const log = require('../../util/log'); const formatMessage = require('format-message'); const BlockUtility = require('../../engine/block-utility.js'); -const uid = require('../../util/uid'); +const uuidv4 = require('uuid/v4'); +uuidv4(); const Variable = require('../../engine/variable'); /** @@ -88,7 +89,7 @@ class Scratch3MeshBlocks { * Mesh ID * @type {string} */ - this.meshId = uid(); + this.meshId = uuidv4(); this._setOpcodeFunctionHOC(); this._setVariableFunctionHOC(); From 55f50c4fe7b1d12116414e9da0f56bcca12ab91c Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 16 Apr 2020 20:50:19 +0900 Subject: [PATCH 35/42] updated WSS_URL. --- src/extensions/scratch3_mesh/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index ecefb7917b4..37646c9ba83 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -15,7 +15,7 @@ const Variable = require('../../engine/variable'); const blockIconURI = 'data:image/png;base64,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'; const MESH_HOST_PERIPHERAL_ID = 'mesh_host'; -const MESH_WSS_URL = 'wss://3rnikeqwld.execute-api.ap-northeast-1.amazonaws.com/dev'; +const MESH_WSS_URL = 'wss://8czqql1pqe.execute-api.ap-northeast-1.amazonaws.com/dev'; const ICE_SERVERS = [ { urls: [ From 75b806749429588816eebac9b415192db134536e Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Fri, 17 Apr 2020 02:46:08 +0900 Subject: [PATCH 36/42] use custom domain. --- src/extensions/scratch3_mesh/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 37646c9ba83..dd4dca2a596 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -15,7 +15,7 @@ const Variable = require('../../engine/variable'); const blockIconURI = 'data:image/png;base64,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'; const MESH_HOST_PERIPHERAL_ID = 'mesh_host'; -const MESH_WSS_URL = 'wss://8czqql1pqe.execute-api.ap-northeast-1.amazonaws.com/dev'; +const MESH_WSS_URL = 'wss://api.smalruby.jp/mesh-signaling'; const ICE_SERVERS = [ { urls: [ From b6b4baa419fce6af639cf006a03daca02a97c6ee Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 18 Apr 2020 16:56:01 +0900 Subject: [PATCH 37/42] use Mesh Chrome Extension. --- src/extensions/scratch3_mesh/index.js | 44 ++++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index dd4dca2a596..433f5f42cac 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -16,17 +16,8 @@ const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYA const MESH_HOST_PERIPHERAL_ID = 'mesh_host'; const MESH_WSS_URL = 'wss://api.smalruby.jp/mesh-signaling'; -const ICE_SERVERS = [ - { - urls: [ - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - "stun:stun2.l.google.com:19302", - "stun:stun3.l.google.com:19302", - "stun:stun4.l.google.com:19302" - ] - } -]; +const ICE_SERVERS = []; +const CHROME_MESH_EXTENSION_ID = 'ioaoebnfpgnbehdolokpdddomfnhpckn'; /** * Host for the Mesh-related blocks @@ -203,6 +194,8 @@ class Scratch3MeshBlocks { return; } + this._changeWebRTCIPHandlingPolicy(); + const connection = new RTCPeerConnection({iceServers: ICE_SERVERS}); this.rtcConnections.push(connection); @@ -320,6 +313,31 @@ class Scratch3MeshBlocks { return meshId.slice(0, 6); } + _sendMessageToChromeMeshExtension (action) { + try { + log.log(`Send message to Chrome mesh extension: action=<${action}>`); + + chrome.runtime.sendMessage(CHROME_MESH_EXTENSION_ID, {action: action}, null, (response) => { + if (chrome.runtime.lastError === undefined) { + log.log(`Succeeded sending message to Chrome mesh extension: response=<${JSON.stringify(response)}>`); + } else { + log.error(`Failed to send message to Chrome extension: lastError=<${JSON.stringify(chrome.runtime.lastError)}>`); + } + }); + } + catch (error) { + log.warn(`Failed to send message to Chrome extension: ${error}`); + } + } + + _changeWebRTCIPHandlingPolicy () { + this._sendMessageToChromeMeshExtension('change'); + } + + _revertWebRTCIPHandlingPolicy () { + this._sendMessageToChromeMeshExtension('revert'); + } + /** * @returns {object} metadata for this extension and its blocks. */ @@ -392,6 +410,8 @@ class Scratch3MeshBlocks { if (this.isHost) { this._sendVariablesTo(this.variables, dataChannel); } + + this._revertWebRTCIPHandlingPolicy(); } _sendVariablesTo (variables, dataChannel) { @@ -488,6 +508,8 @@ class Scratch3MeshBlocks { `actual=<${data.hostMeshId}> expected=<${this.meshId}>`); } + this._changeWebRTCIPHandlingPolicy(); + connection = new RTCPeerConnection({iceServers: ICE_SERVERS}); this.rtcConnections.push(connection); From 382db3c7fc7c92d42b01b21dc07b74e965327ca1 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 19 Apr 2020 13:13:16 +0900 Subject: [PATCH 38/42] refactor. --- src/extensions/scratch3_mesh/index.js | 781 +++---------------- src/extensions/scratch3_mesh/mesh-host.js | 151 ++++ src/extensions/scratch3_mesh/mesh-peer.js | 171 ++++ src/extensions/scratch3_mesh/mesh-service.js | 512 ++++++++++++ 4 files changed, 948 insertions(+), 667 deletions(-) create mode 100644 src/extensions/scratch3_mesh/mesh-host.js create mode 100644 src/extensions/scratch3_mesh/mesh-peer.js create mode 100644 src/extensions/scratch3_mesh/mesh-service.js diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 433f5f42cac..4e7f64c5041 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -2,10 +2,12 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const log = require('../../util/log'); const formatMessage = require('format-message'); -const BlockUtility = require('../../engine/block-utility.js'); const uuidv4 = require('uuid/v4'); uuidv4(); const Variable = require('../../engine/variable'); +const MeshService = require('./mesh-service'); +const MeshHost = require('./mesh-host'); +const MeshPeer = require('./mesh-peer'); /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. @@ -15,9 +17,6 @@ const Variable = require('../../engine/variable'); const blockIconURI = 'data:image/png;base64,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'; const MESH_HOST_PERIPHERAL_ID = 'mesh_host'; -const MESH_WSS_URL = 'wss://api.smalruby.jp/mesh-signaling'; -const ICE_SERVERS = []; -const CHROME_MESH_EXTENSION_ID = 'ioaoebnfpgnbehdolokpdddomfnhpckn'; /** * Host for the Mesh-related blocks @@ -46,298 +45,24 @@ class Scratch3MeshBlocks { */ this.runtime = runtime; - /** - * Host and clients global variable name and value pair. - * @type {Object.} - */ - this.variables = {}; - - /** - * Host and clients global variable names - * @type {string[]} - */ - this.variableNames = []; - - /** - * WebRTC connections - * @type {RTCPeerConnection[]} - */ - this.rtcConnections = []; - - /** - * WebRTC data channels - * @type {RTCDataChannel[]} - */ - this.rtcDataChannels = []; - - /** - * Host or not - * @type {bool} - */ - this.isHost = false; - /** * Mesh ID * @type {string} */ this.meshId = uuidv4(); - this._setOpcodeFunctionHOC(); - this._setVariableFunctionHOC(); + /** + * Mesh Object + * @type {MeshHost|MeshPeer} + */ + this.meshService = new MeshService(this, this.meshId, null); - this._availablePeripherals = {}; + this.setOpcodeFunctionHOC(); + this.setVariableFunctionHOC(); this.runtime.registerPeripheralExtension(Scratch3MeshBlocks.EXTENSION_ID, this); } - /** - * Called by the runtime when user wants to scan for a peripheral. - */ - scan () { - log.log('scan in mesh'); - - try { - this._availablePeripherals = {}; - this._availablePeripherals[MESH_HOST_PERIPHERAL_ID] = { - name: formatMessage({ - id: 'mesh.hostPeripheralName', - default: 'Host Mesh [{ MESH_ID }]', - description: 'label for "Host Mesh" in connect modal for Mesh extension' - }, {MESH_ID: this._makeMeshIdLabel(this.meshId)}), - peripheralId: MESH_HOST_PERIPHERAL_ID, - rssi: 0 - }; - - if (this._websocket) { - const websocket = this._websocket; - - websocket.send(JSON.stringify({ - action: 'list', - data: { - meshId: this.meshId - } - })); - } else { - this._websocket = new WebSocket(MESH_WSS_URL); - const websocket = this._websocket; - websocket.onopen = () => { - this.runtime.emit( - this.runtime.constructor.PERIPHERAL_LIST_UPDATE, - this._availablePeripherals - ); - - websocket.send(JSON.stringify({ - action: 'list', - data: { - meshId: this.meshId - } - })); - }; - websocket.onmessage = e => { - try { - this._onWebSocketMessage(JSON.parse(e.data)); - } catch (error) { - log.error(`Error in WebSocket.onmessage: ${error}`); - } - }; - websocket.onclose = () => { - this._websocket = null; - }; - websocket.onerror = e => { - log.error(`Error in WebSocket: ${e}`); - }; - } - } catch (e) { - log.error(`Error in scan: ${e}`); - } - } - - /** - * Called by the runtime when user wants to connect to a certain peripheral. - * @param {string} meshId - the Mesh ID of the peripheral to connect to. - */ - connect (meshId) { - try { - log.log(`connect in mesh: ${meshId}`); - - this._connected = false; - - if (meshId === MESH_HOST_PERIPHERAL_ID) { - if (!this._websocket) { - log.error('WebSocket is not opened'); - return; - } - - this._websocket.send(JSON.stringify({ - action: 'register', - data: { - meshId: this.meshId - } - })); - - this.isHost = true; - } else { - if (!meshId || meshId.trim() === '') { - log.error('Not select Mesh ID'); - return; - } - if (!this._websocket) { - log.error('WebSocket is not opened'); - return; - } - if (this.isHost) { - log.error('I am not Client'); - return; - } - if (this.rtcConnections.length > 0) { - log.error('Already WebRTC connected'); - return; - } - - this._changeWebRTCIPHandlingPolicy(); - - const connection = new RTCPeerConnection({iceServers: ICE_SERVERS}); - this.rtcConnections.push(connection); - - connection.onconnectionstatechange = () => { - this._onRtcConnectionStateChange(connection); - }; - connection.onicecandidate = e => { - if (e.candidate) { - log.log('ICE candidate', JSON.stringify(e.candidate, null, 2)); - } else { - log.log(`Client: offer to Host\n${JSON.stringify(connection.localDescription)}`); - - this._websocket.send(JSON.stringify({ - action: 'offer', - data: { - meshId: this.meshId, - hostMeshId: meshId, - clientDescription: connection.localDescription - } - })); - } - }; - - const dataChannel = connection.createDataChannel('dataChannel'); - dataChannel.onopen = () => { - this._onRtcOpen(connection, dataChannel); - }; - dataChannel.onmessage = e => { - this._onRtcMessage(e.data); - }; - dataChannel.onclose = () => { - this._onRtcClose(connection, dataChannel); - }; - - connection.createOffer().then( - desc => { - connection.setLocalDescription(desc); - }, - error => { - log.error(`Client: Error in createOffer: ${error}`); - - this.disconnect(); - this.runtime.emit(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR, { - extensionId: Scratch3MeshBlocks.EXTENSION_ID - }); - } - ); - } - } catch (e) { - log.error(`Error in connect: ${e}`); - } - } - - /** - * Disconnect from the Mesh. - */ - disconnect () { - log.log(`disconnect in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); - - if (this._websocket) { - this._websocket.close(); - } - this.variables = {}; - this.variableNames = []; - this.rtcConnections.forEach(connection => { - connection.close(); - }); - this.rtcConnections = []; - this.rtcDataChannels = []; - this.isHost = false; - this._hostMeshId = null; - - this._connected = false; - - this.runtime.emit(this.runtime.constructor.PERIPHERAL_DISCONNECTED); - - log.log(`disconnected in mesh`); - } - - /** - * Return true if connected to the Mesh - * @return {boolean} - whether the Mesh is connected. - */ - isConnected () { - log.log(`isConnected in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); - - return this._connected; - } - - /** - * Return connected message if connected to the Mesh - * @return {string} - connected message. - */ - connectedMessage () { - log.log(`connectedMessage in mesh: isHost=<${this.isHost}> connected=<${this._connected}>`); - - let message; - if (this.isHost) { - message = formatMessage({ - id: 'mesh.registeredHost', - default: 'Registered Host Mesh [{ MESH_ID }]', - description: 'label for registered Host Mesh in connect modal for Mesh extension' - }, {MESH_ID: this._makeMeshIdLabel(this.meshId)}); - } else { - message = formatMessage({ - id: 'mesh.joinedMesh', - default: 'Joined Mesh [{ MESH_ID }]', - description: 'label for joined Mesh in connect modal for Mesh extension' - }, {MESH_ID: this._makeMeshIdLabel(this._hostMeshId)}); - } - return message; - } - - _makeMeshIdLabel (meshId) { - return meshId.slice(0, 6); - } - - _sendMessageToChromeMeshExtension (action) { - try { - log.log(`Send message to Chrome mesh extension: action=<${action}>`); - - chrome.runtime.sendMessage(CHROME_MESH_EXTENSION_ID, {action: action}, null, (response) => { - if (chrome.runtime.lastError === undefined) { - log.log(`Succeeded sending message to Chrome mesh extension: response=<${JSON.stringify(response)}>`); - } else { - log.error(`Failed to send message to Chrome extension: lastError=<${JSON.stringify(chrome.runtime.lastError)}>`); - } - }); - } - catch (error) { - log.warn(`Failed to send message to Chrome extension: ${error}`); - } - } - - _changeWebRTCIPHandlingPolicy () { - this._sendMessageToChromeMeshExtension('change'); - } - - _revertWebRTCIPHandlingPolicy () { - this._sendMessageToChromeMeshExtension('revert'); - } - /** * @returns {object} metadata for this extension and its blocks. */ @@ -368,394 +93,144 @@ class Scratch3MeshBlocks { menus: { variableNames: { acceptReporters: true, - items: '_getVariableNamesMenuItems' + items: 'getVariableNamesMenuItems' } } }; } getSensorValue (args) { - return this._getVariable(args.NAME); + return this.meshService.getVariable(args.NAME); } - _getVariable (name) { - if (!this.variableNames.includes(name)) { - return ''; - } - return this.variables[name].value; + getVariableNamesMenuItems () { + return [' '].concat(this.meshService.variableNames); } - _getGlobalVariables () { - const variables = {}; - const stage = this.runtime.getTargetForStage(); - for (const varId in stage.variables) { - const currVar = stage.variables[varId]; - if (currVar.type === Variable.SCALAR_TYPE) { - variables[currVar.name] = { - name: currVar.name, - value: currVar.value, - owner: this.meshId - }; - } - } - return variables; + /** + * Called by the runtime when user wants to scan for a peripheral. + */ + scan () { + this.meshService.scan(MESH_HOST_PERIPHERAL_ID); } - _onRtcOpen (connection, dataChannel) { - log.log(`data channel open`); - - this.rtcDataChannels.push(dataChannel); - - this._sendVariablesTo(this._getGlobalVariables(), dataChannel); - if (this.isHost) { - this._sendVariablesTo(this.variables, dataChannel); + /** + * Called by the runtime when user wants to connect to a certain peripheral. + * @param {string} meshId - the Mesh ID of the peripheral to connect to. + */ + connect (meshId) { + if (meshId === MESH_HOST_PERIPHERAL_ID) { + this.meshService = new MeshHost(this, this.meshId, this.meshService.webSocket); + this.meshService.connect(); + } else { + this.meshService = new MeshPeer(this, this.meshId, this.meshService.webSocket); + this.meshService.connect(meshId); } - - this._revertWebRTCIPHandlingPolicy(); - } - - _sendVariablesTo (variables, dataChannel) { - Object.keys(variables).forEach(name => { - const variable = variables[name]; - - const message = { - owner: variable.owner, - type: 'variable', - data: { - name: variable.name, - value: variable.value - } - }; - dataChannel.send(JSON.stringify(message)); - log.log(`send variable: name=<${variable.name}> value=<${variable.value}> owner=<${variable.owner}>`); - }); } - _onWebSocketMessage (message) { - log.log(`received WebSocket message: isHost=<${this.isHost}> message=<${JSON.stringify(message)}>`); - - const {action, result, data} = message; - - if (message.hasOwnProperty('result') && !result) { - log.error(`failed action: action=<${action}> error=<${data.error}>`); - - switch (action) { - case 'offer': - if (!this.isHost) { - this.disconnect(); - - this.runtime.emit(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR, { - extensionId: Scratch3MeshBlocks.EXTENSION_ID - }); - } - break; - } - return; - } - - let connection; - let now; - - switch (action) { - case 'register': - if (!this.isHost) { - log.error(`failed action: action=<${message.action}> reason=`); - return; - } - - this._connected = true; - this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); - break; - case 'list': - now = Math.floor(Date.now() / 1000); - data.hosts.forEach(host => { - const t = host.ttl - now; - let rssi; - if (t >= 4 * 60) { - rssi = 0; - } else if (t >= 3 * 60) { - rssi = -20; - } else if (t >= 2 * 60) { - rssi = -40; - } else if (t >= 1 * 60) { - rssi = -60; - } else { - rssi = -80; - } - this._availablePeripherals[host.meshId] = { - name: formatMessage({ - id: 'mesh.clientPeripheralName', - default: 'Join Mesh [{ MESH_ID }]', - description: 'label for "Join Mesh" in connect modal for Mesh extension' - }, {MESH_ID: this._makeMeshIdLabel(host.meshId)}), - peripheralId: host.meshId, - rssi: rssi - }; - }); - - this.runtime.emit( - this.runtime.constructor.PERIPHERAL_LIST_UPDATE, - this._availablePeripherals - ); - break; - case 'offer': - if (!this.isHost) { - return; - } - - if (data.hostMeshId !== this.meshId) { - log.error(`failed action: action=<${message.action}> reason= ` + - `actual=<${data.hostMeshId}> expected=<${this.meshId}>`); - } - - this._changeWebRTCIPHandlingPolicy(); - - connection = new RTCPeerConnection({iceServers: ICE_SERVERS}); - this.rtcConnections.push(connection); - - connection.onconnectionstatechange = () => { - this._onRtcConnectionStateChange(connection); - }; - connection.onicecandidate = e => { - if (e.candidate) { - log.log('ICE candidate', JSON.stringify(e.candidate, null, 2)); - } else { - log.log(`Host: answer to Client:\n${JSON.stringify(connection.localDescription)}`); - - this._websocket.send(JSON.stringify({ - action: 'answer', - data: { - meshId: this.meshId, - clientMeshId: data.meshId, - hostDescription: connection.localDescription - } - })); - } - }; - connection.ondatachannel = event => { - const dataChannel = event.channel; - - dataChannel.onopen = () => { - this._onRtcOpen(connection, dataChannel); - }; - dataChannel.onmessage = e => { - this._onRtcMessage(e.data); - }; - dataChannel.onclose = () => { - this._onRtcClose(connection, dataChannel); - }; - }; - - connection.setRemoteDescription(new RTCSessionDescription(data.clientDescription)); - connection.createAnswer().then( - desc => { - connection.setLocalDescription(desc); - }, - error => { - log.error(`Host: Error in createAnswer: ${error}`); - connection.close(); - this.rtcConnections = this.rtcConnections.filter(c => c !== connection); - } - ); - break; - case 'answer': - if (this.isHost) { - return; - } - - if (data.clientMeshId !== this.meshId) { - log.error(`failed action: action=<${message.action}> reason=` + - ` actual=<${data.clientMeshId}> expected=<${this.meshId}>`); - return; - } - - if (this.rtcConnections.length === 0) { - log.error(`failed action: action=<${message.action}> reason=`); - return; - } - - log.log('Client: set Host description'); - - connection = this.rtcConnections[0]; - connection.setRemoteDescription(new RTCSessionDescription(data.hostDescription)); - - this._websocket.close(); - - this._hostMeshId = data.meshId; - this._connected = true; - this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTED); - break; - } + /** + * Disconnect from the Mesh. + */ + disconnect () { + this.meshService.disconnect(); } - _onRtcConnectionStateChange (connection) { - log.log(`onRtcConnectionStateChange: ${connection.connectionState}`); - - switch (connection.connectionState) { - case 'disconnected': - case 'failed': - connection.close(); - break; - } + /** + * Return true if connected to the Mesh + * @return {boolean} - whether the Mesh is connected. + */ + isConnected () { + return this.meshService.isConnected(); } - _onRtcMessage (data) { + /** + * Return connected message if connected to the Mesh + * @return {string} - connected message. + */ + connectedMessage () { let message; - try { - message = JSON.parse(data); - } catch (error) { - log.error(`Invalid WebRTC message format: ${error}`); - return; - } - - log.log(`received WebRTC message: isHost=<${this.isHost}> owner=<${message.owner}> type=<${message.type}>` + - ` data=<${JSON.stringify(message.data)}>`); - - let broadcastName; - let variable; - switch (message.type) { - case 'broadcast': - broadcastName = message.data; - log.log(`received broadcast: ${broadcastName}`); - - if (this.isHost) { - log.log('send broadcast all clients'); - - this._sendMessage(message); - } - if (this.meshId === message.owner) { - log.log('ignore broadcast: reason='); - } else { - log.log('process broadcast'); - - const args = { - BROADCAST_OPTION: { - id: null, - name: broadcastName - } - }; - const util = BlockUtility.lastInstance(); - if (!util.sequencer) { - util.sequencer = this.runtime.sequencer; - } - this._opcodeFunctions.event_broadcast(args, util); - } - break; - case 'variable': - variable = message.data; - log.log(`received variable: name=<${variable.name}> value=<${variable.value}>`); - - if (this.isHost) { - log.log('send variable all clients'); - - this._sendMessage(message); - } - if (this.meshId === message.owner) { - log.log('ignore variable: reason='); - } else { - log.log(`update variable: name=<${variable.name}>` + - ` from=<${this._getVariable(variable.name)}> to=<${variable.value}>`); - this._setVariable(variable.name, variable.value, message.owner); - } - break; - default: - log.error(`invalid message type: ${message.type}`); - break; + if (this.meshService.isHost) { + message = formatMessage({ + id: 'mesh.registeredHost', + default: 'Registered Host Mesh [{ MESH_ID }]', + description: 'label for registered Host Mesh in connect modal for Mesh extension' + }, {MESH_ID: this.makeMeshIdLabel(this.meshService.meshId)}); + } else { + message = formatMessage({ + id: 'mesh.joinedMesh', + default: 'Joined Mesh [{ MESH_ID }]', + description: 'label for joined Mesh in connect modal for Mesh extension' + }, {MESH_ID: this.makeMeshIdLabel(this.meshService.hostMeshId)}); } + return message; } - _onRtcClose (connection, dataChannel) { - log.log(`data channel close`); - - this.rtcConnections = this.rtcConnections.filter(c => c !== connection); - this.rtcDataChannels = this.rtcDataChannels.filter(c => c !== dataChannel); - - if (!this.isHost && this._connected) { - this.disconnect(); - - this.runtime.emit(this.runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, { - extensionId: Scratch3MeshBlocks.EXTENSION_ID - }); - } - - log.log(`data channel closed`); + makeMeshIdLabel (meshId) { + return meshId.slice(0, 6); } - _setVariable (name, value, owner) { - if (!this.variableNames.includes(name)) { - this.variableNames.push(name); - } - this.variables[name] = { - name: name, - value: value, - owner: owner + setOpcodeFunctionHOC () { + this.opcodeFunctions = { + event_broadcast: this.runtime.getOpcodeFunction('event_broadcast'), + event_broadcastandwait: this.runtime.getOpcodeFunction('event_broadcastandwait'), + data_setvariableto: this.runtime.getOpcodeFunction('data_setvariableto'), + data_changevariableby: this.runtime.getOpcodeFunction('data_changevariableby') }; - } - - _getVariableNamesMenuItems () { - return [' '].concat(this.variableNames); - } - _sendMessage (message) { - try { - this.rtcDataChannels.forEach(channel => { - channel.send(JSON.stringify(message)); - }); - } catch (e) { - log.error(`Failed to send WebRTC message: error=<${e}> message=<${JSON.stringify(message)}>`); - } + this.runtime._primitives.event_broadcast = this.broadcast.bind(this); + this.runtime._primitives.event_broadcastandwait = this.broadcastAndWait.bind(this); + this.runtime._primitives.data_setvariableto = this.setVariableTo.bind(this); + this.runtime._primitives.data_changevariableby = this.changeVariableBy.bind(this); } - _broadcast (args, util) { + broadcast (args, util) { try { log.log('event_broadcast in mesh'); - this._sendBroadcast(args); - this._opcodeFunctions.event_broadcast(args, util); + + this.opcodeFunctions.event_broadcast(args, util); + this.meshService.sendRTCBroadcastMessage(args.BROADCAST_OPTION.name); } catch (error) { log.error(`Failed to execute event_broadcast: ${error}`); } } - _broadcastAndWait (args, util) { + broadcastAndWait (args, util) { try { log.log('event_broadcastandwait in mesh'); - this._sendBroadcast(args); - this._opcodeFunctions.event_broadcastandwait(args, util); + + const first = !util.stackFrame.startedThreads; + this.opcodeFunctions.event_broadcastandwait(args, util); + if (first) { + this.meshService.sendRTCBroadcastMessage(args.BROADCAST_OPTION.name); + } } catch (error) { log.error(`Failed to execute event_broadcastandwait: ${error}`); } } - _sendBroadcast (args) { - const broadcastName = args.BROADCAST_OPTION.name; - this._sendMessage({ - owner: this.meshId, - type: 'broadcast', - data: broadcastName - }); - } - - _setVariableTo (args, util) { + setVariableTo (args, util) { try { log.log('data_setvariableto in mesh'); - this._opcodeFunctions.data_setvariableto(args, util); - this._sendVariableByOpcodeFunction(args); + + this.opcodeFunctions.data_setvariableto(args, util); + this.sendVariableByOpcodeFunction(args); } catch (error) { log.error(`Failed to execute data_setvariableto: ${error}`); } } - _changeVariableBy (args, util) { + changeVariableBy (args, util) { try { log.log('data_changevariableby in mesh'); - this._opcodeFunctions.data_changevariableby(args, util); - this._sendVariableByOpcodeFunction(args); + + this.opcodeFunctions.data_changevariableby(args, util); + this.sendVariableByOpcodeFunction(args); } catch (error) { log.error(`Failed to execute data_changevariableby: ${error}`); } } - _sendVariableByOpcodeFunction (args) { + sendVariableByOpcodeFunction (args) { const stage = this.runtime.getTargetForStage(); let variable = stage.lookupVariableById(args.VARIABLE.id); if (!variable) { @@ -765,41 +240,12 @@ class Scratch3MeshBlocks { return; } - this._sendVariable(variable.name, variable.value); + this.meshService.sendRTCVariableMessage(variable.name, variable.value); } - _sendVariable (name, value) { - try { - this._sendMessage({ - owner: this.meshId, - type: 'variable', - data: { - name: name, - value: value - } - }); - } catch (error) { - log.error(`Failed to send variable: error=<${error}> name=<${name}> value=<${value}>`); - } - } - - _setOpcodeFunctionHOC () { - this._opcodeFunctions = { - event_broadcast: this.runtime.getOpcodeFunction('event_broadcast'), - event_broadcastandwait: this.runtime.getOpcodeFunction('event_broadcastandwait'), - data_setvariableto: this.runtime.getOpcodeFunction('data_setvariableto'), - data_changevariableby: this.runtime.getOpcodeFunction('data_changevariableby') - }; - - this.runtime._primitives.event_broadcast = this._broadcast.bind(this); - this.runtime._primitives.event_broadcastandwait = this._broadcastAndWait.bind(this); - this.runtime._primitives.data_setvariableto = this._setVariableTo.bind(this); - this.runtime._primitives.data_changevariableby = this._changeVariableBy.bind(this); - } - - _setVariableFunctionHOC () { + setVariableFunctionHOC () { const stage = this.runtime.getTargetForStage(); - this._variableFunctions = { + this.variableFunctions = { runtime: { createNewGlobalVariable: this.runtime.createNewGlobalVariable.bind(this.runtime) }, @@ -811,24 +257,25 @@ class Scratch3MeshBlocks { } }; - this.runtime.createNewGlobalVariable = this._createNewGlobalVariable.bind(this); + this.runtime.createNewGlobalVariable = this.createNewGlobalVariable.bind(this); - stage.lookupOrCreateVariable = this._lookupOrCreateVariable.bind(this); - stage.createVariable = this._createVariable.bind(this); - stage.setVariableValue = this._setVariableValue.bind(this); - stage.renameVariable = this._renameVariable.bind(this); + stage.lookupOrCreateVariable = this.lookupOrCreateVariable.bind(this); + stage.createVariable = this.createVariable.bind(this); + stage.setVariableValue = this.setVariableValue.bind(this); + stage.renameVariable = this.renameVariable.bind(this); } - _createNewGlobalVariable (variableName, optVarId, optVarType) { + createNewGlobalVariable (variableName, optVarId, optVarType) { log.log('runtime.createNewGlobalVariable in mesh'); - const variable = this._variableFunctions.runtime.createNewGlobalVariable(variableName, optVarId, optVarType); + + const variable = this.variableFunctions.runtime.createNewGlobalVariable(variableName, optVarId, optVarType); if (variable.type === Variable.SCALAR_TYPE) { - this._sendVariable(variable.name, variable.value); + this.meshService.sendRTCVariableMessage(variable.name, variable.value); } return variable; } - _lookupOrCreateVariable (id, name) { + lookupOrCreateVariable (id, name) { log.log('stage.lookupOrCreateVariable in mesh'); const stage = this.runtime.getTargetForStage(); @@ -841,48 +288,48 @@ class Scratch3MeshBlocks { // No variable with this name exists - create it locally. const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false); stage.variables[id] = newVariable; - this._sendVariable(newVariable.name, newVariable.value); + this.meshService.sendRTCVariableMessage(newVariable.name, newVariable.value); return newVariable; } - _createVariable (id, name, type, isCloud) { + createVariable (id, name, type, isCloud) { log.log('stage.createVariable in mesh'); const stage = this.runtime.getTargetForStage(); if (!stage.variables.hasOwnProperty(id)) { - this._variableFunctions.stage.createVariable(id, name, type, isCloud); + this.variableFunctions.stage.createVariable(id, name, type, isCloud); if (type === Variable.SCALAR_TYPE) { const variable = stage.variables[id]; - this._sendVariable(variable.name, variable.value); + this.meshService.sendRTCVariableMessage(variable.name, variable.value); } } } - _setVariableValue (id, newValue) { + setVariableValue (id, newValue) { log.log('stage.setVariableValue in mesh'); const stage = this.runtime.getTargetForStage(); if (stage.variables.hasOwnProperty(id)) { const variable = stage.variables[id]; if (variable.id === id) { - this._variableFunctions.stage.setVariableValue(id, newValue); + this.variableFunctions.stage.setVariableValue(id, newValue); if (variable.type === Variable.SCALAR_TYPE) { - this._sendVariable(variable.name, variable.value); + this.meshService.sendRTCVariableMessage(variable.name, variable.value); } } } } - _renameVariable (id, newName) { + renameVariable (id, newName) { log.log('stage.renameVariable in mesh'); const stage = this.runtime.getTargetForStage(); if (stage.variables.hasOwnProperty(id)) { const variable = stage.variables[id]; if (variable.id === id) { - this._variableFunctions.stage.renameVariable(id, newName); + this.variableFunctions.stage.renameVariable(id, newName); if (variable.type === Variable.SCALAR_TYPE) { - this._sendVariable(variable.name, variable.value); + this.meshService.sendRTCVariableMessage(variable.name, variable.value); } } } diff --git a/src/extensions/scratch3_mesh/mesh-host.js b/src/extensions/scratch3_mesh/mesh-host.js new file mode 100644 index 00000000000..99c8b57ee7d --- /dev/null +++ b/src/extensions/scratch3_mesh/mesh-host.js @@ -0,0 +1,151 @@ +const MeshService = require('./mesh-service'); +const MeshPeer = require('./mesh-peer'); + +class MeshHost extends MeshService { + constructor (blocks, meshId, webSocket) { + super(blocks, meshId, webSocket); + + this.isHost = true; + } + + get logPrefix () { + return 'Mesh Host'; + } + + connect () { + if (this.connectionState === 'connected') { + this.infoLog('Already connected'); + return; + } + if (this.connectionState === 'connecting') { + this.infoLog('Now connecting, please wait to connect.'); + return; + } + + this.setConnectionState('connecting'); + + this.sendWebSocketMessage('register', { + meshId: this.meshId + }); + + this.connectTimeoutId = + setTimeout(this.onConnectTimeout.bind(this), this.connectTimeoutSeconds * 1000); + } + + onConnectTimeout () { + this.connectTimeoutId = null; + if (!this.isConnected()) { + this.webSocket.close(); + } + } + + onWebSocketClose () { + this.setConnectionState('disconnected'); + } + + registerWebSocketAction (result, data) { + if (!result) { + this.setConnectionState('request_error'); + this.errorLog(`Failed to register: reason=<${data.error}>`); + return; + } + + this.setConnectionState('connected'); + this.infoLog('Connected as Mesh Host.'); + } + + offerWebSocketAction (result, data) { + const peerMeshId = data.meshId; + if (data.hostMeshId !== this.meshId) { + this.logError(`Invalid Mesh ID in offer from peer:` + + ` peer=<${peerMeshId}> received=<${data.hostMeshId}> own=<${this.meshId}>`); + + this.sendWebSocketMessage('answer', { + meshId: this.meshId, + clientMeshId: peerMeshId + }, false); + return; + } + + this.changeWebRTCIPHandlingPolicy(); + + const connection = this.openRTCPeerConnection(peerMeshId); + + connection.onconnectionstatechange = () => { + this.onRTCConnectionStateChange(connection, peerMeshId); + }; + connection.onicecandidate = event => { + this.onRTCICECandidate(connection, peerMeshId, event, description => { + this.debugLog(`Answer to Peer: peer=<${peerMeshId}> description=<\n` + + `${JSON.stringify(description, null, 2)}\n` + + `>`); + + this.sendWebSocketMessage('answer', { + meshId: this.meshId, + clientMeshId: peerMeshId, + hostDescription: description + }); + }); + }; + connection.ondatachannel = event => { + this.onRTCDataChannel(connection, peerMeshId, event); + }; + + connection.setRemoteDescription(new RTCSessionDescription(data.clientDescription)); + + connection.createAnswer().then( + desc => { + connection.setLocalDescription(desc); + }, + error => { + this.errorLog(`Failed createAnswer: peer=<${peerMeshId}> reason=<${error}>`); + this.closeRTCPeerConnection(peerMeshId); + } + ); + } + + onRTCDataChannel (connection, peerMeshId, event) { + this.debugLog(`WebRTC data channel by remote peer: peer=<${peerMeshId}>`); + + const dataChannel = event.channel; + + dataChannel.onopen = () => { + this.onRTCDataChannelOpen(connection, dataChannel, peerMeshId); + }; + dataChannel.onmessage = e => { + this.onRTCDataChannelMessage(connection, dataChannel, peerMeshId, e); + }; + dataChannel.onclose = () => { + this.onRTCDataChannelClose(connection, dataChannel, peerMeshId); + }; + } + + onRTCDataChannelOpen (connection, dataChannel, peerMeshId) { + MeshService.prototype.onRTCDataChannelOpen.call(this, connection, dataChannel, peerMeshId); + this.sendVariablesTo(this.variables, peerMeshId); + } + + answerWebSocketAction (result, data) { + if (!result) { + this.closeRTCPeerConnection(data.clientMeshId); + this.errorLog(`Failed to answer: reason=<${data.error}>`); + return; + } + + this.infoLog(`Answered to peer: peer=<${data.clientMeshId}>`); + } + + broadcastRTCAction (peerMeshId, message) { + this.sendRTCMessage(message); + + MeshPeer.prototype.broadcastRTCAction.call(this, peerMeshId, message); + } + + variableRTCAction (peerMeshId, message) { + this.sendRTCMessage(message); + + MeshPeer.prototype.variableRTCAction.call(this, peerMeshId, message); + } +} + +module.exports = MeshHost; diff --git a/src/extensions/scratch3_mesh/mesh-peer.js b/src/extensions/scratch3_mesh/mesh-peer.js new file mode 100644 index 00000000000..12a0eaec923 --- /dev/null +++ b/src/extensions/scratch3_mesh/mesh-peer.js @@ -0,0 +1,171 @@ +const MeshService = require('./mesh-service'); +const BlockUtility = require('../../engine/block-utility.js'); + +class MeshPeer extends MeshService { + get logPrefix () { + return 'Mesh Peer'; + } + + connect (hostMeshId) { + if (this.connectionState === 'connected') { + this.infoLog('Already connected'); + return; + } + if (this.connectionState === 'connecting') { + this.infoLog('Now connecting, please wait to connect.'); + return; + } + + if (!hostMeshId || hostMeshId.trim() === '') { + this.setConnectionState('request_error'); + + this.errorLog('Not select Host Mesh ID'); + return; + } + + this.hostMeshId = hostMeshId; + + this.setConnectionState('connecting'); + + this.changeWebRTCIPHandlingPolicy(); + + const connection = this.openRTCPeerConnection(hostMeshId); + + connection.onconnectionstatechange = () => { + this.onRTCConnectionStateChange(connection, hostMeshId); + }; + connection.onicecandidate = event => { + this.onRTCICECandidate(connection, hostMeshId, event, description => { + this.debugLog(`Offer to Host: host=<${hostMeshId}> description=<\n` + + `${JSON.stringify(description, null, 2)}\n` + + `>`); + + this.sendWebSocketMessage('offer', { + meshId: this.meshId, + hostMeshId: hostMeshId, + clientDescription: description + }); + }); + }; + + const dataChannel = connection.createDataChannel('dataChannel'); + dataChannel.onopen = () => { + this.onRTCDataChannelOpen(connection, dataChannel, hostMeshId); + }; + dataChannel.onmessage = e => { + this.onRTCDataChannelMessage(connection, dataChannel, hostMeshId, e); + }; + dataChannel.onclose = () => { + this.onRTCDataChannelClose(connection, dataChannel, hostMeshId); + }; + + connection.createOffer().then( + desc => { + connection.setLocalDescription(desc); + }, + error => { + this.setConnectionState('request_error'); + this.errorLog(`Failed createOffer: host=<${hostMeshId}> reason=<${error}>`); + } + ); + + this.connectTimeoutId = + setTimeout(this.onConnectTimeout.bind(this), this.connectTimeoutSeconds * 1000); + } + + onConnectTimeout () { + this.connectTimeoutId = null; + if (!this.isConnected()) { + Object.keys(this.rtcConnections).forEach(meshId => { + this.rtcConnections[meshId].close(); + }); + this.rtcConnections = {}; + this.rtcDataChannels = {}; + + this.webSocket.close(); + } + } + + onWebSocketClose () { + this.debugLog('WebSocket closed.'); + } + + offerWebSocketAction (result, data) { + if (!result) { + this.setConnectionState('request_error'); + + this.errorLog(`Failed to offer: reason=<${data.error}>`); + return; + } + + this.infoLog(`Offered to host: host=<${data.hostMeshId}>`); + } + + answerWebSocketAction (result, data) { + const hostMeshId = data.meshId; + + if (this.connectionState !== 'connecting') { + this.logError(`Received answer, but WebRTC not connecting: host=<${hostMeshId}>`); + return; + } + + if (data.clientMeshId !== this.meshId) { + this.setConnectionState('request_error'); + + this.logError(`Invalid Mesh ID in answer from host:` + + ` host=<${hostMeshId}> received=<${data.clientMeshId}> own=<${this.meshId}>`); + return; + } + + const connection = this.rtcConnections[hostMeshId]; + connection.setRemoteDescription(new RTCSessionDescription(data.hostDescription)); + } + + onRTCDataChannelOpen (connection, dataChannel, peerMeshId) { + MeshService.prototype.onRTCDataChannelOpen.call(this, connection, dataChannel, peerMeshId); + + this.setConnectionState('connected'); + } + + onRTCDataChannelClose (connection, dataChannel, peerMeshId) { + MeshService.prototype.onRTCDataChannelClose.call(this, connection, dataChannel, peerMeshId); + + this.setConnectionState('disconnected'); + } + + broadcastRTCAction (peerMeshId, message) { + const broadcast = message.data; + + if (this.meshId === message.owner) { + this.debugLog(`Ignore broadcast: reason= ${JSON.stringify(broadcast)}`); + return; + } + + this.debugLog(`Process broadcast: name=<${broadcast.name}>`); + + const args = { + BROADCAST_OPTION: { + id: null, + name: broadcast.name + } + }; + const util = BlockUtility.lastInstance(); + if (!util.sequencer) { + util.sequencer = this.runtime.sequencer; + } + this.blocks.opcodeFunctions.event_broadcast(args, util); + } + + variableRTCAction (peerMeshId, message) { + const variable = message.data; + + if (this.meshId === message.owner) { + this.debugLog(`Ignore variable: reason= ${JSON.stringify(variable)}`); + return; + } + + this.setVariable(variable.name, variable.value, message.owner); + } +} + +module.exports = MeshPeer; diff --git a/src/extensions/scratch3_mesh/mesh-service.js b/src/extensions/scratch3_mesh/mesh-service.js new file mode 100644 index 00000000000..70da3a829ac --- /dev/null +++ b/src/extensions/scratch3_mesh/mesh-service.js @@ -0,0 +1,512 @@ +/* global chrome */ + +const log = require('../../util/log'); +const formatMessage = require('format-message'); +const Variable = require('../../engine/variable'); + +const DEBUG = true; +const CHROME_MESH_EXTENSION_ID = 'ioaoebnfpgnbehdolokpdddomfnhpckn'; +const MESH_WSS_URL = 'wss://api.smalruby.jp/mesh-signaling'; + +class MeshService { + constructor (blocks, meshId, webSocket) { + this.blocks = blocks; + + this.runtime = this.blocks.runtime; + + this.meshId = meshId; + + this.setWebSocket(webSocket); + + this.connectionState = 'disconnected'; + + this.connectTimeoutId = null; + + this.connectTimeoutSeconds = 10; + + this.rtcConnections = {}; + + this.rtcDataChannels = {}; + + this.variables = {}; + + this.variableNames = []; + + this.availablePeripherals = {}; + } + + get logPrefix () { + return 'Mesh Service'; + } + + setWebSocket (webSocket) { + this.webSocket = webSocket; + + if (this.webSocket) { + this.webSocket.onopen = this.onWebSocketOpen.bind(this); + this.webSocket.onmessage = this.onWebSocketMessage.bind(this); + this.webSocket.onclose = this.onWebSocketClose.bind(this); + this.webSocket.onerror = this.onWebSocketError.bind(this); + } + } + + isWebSocketOpened () { + return this.webSocket && this.webSocket.readyState === 1; + } + + openWebSocket () { + if (!this.webSocket || this.webSocket.readyState === 2 || this.webSocket.readyState === 3) { + this.setWebSocket(new WebSocket(MESH_WSS_URL)); + } + } + + scan (hostMeshId) { + try { + this.debugLog(`Scan: hostMeshId=<${hostMeshId}>`); + + this.availablePeripherals = {}; + this.availablePeripherals[hostMeshId] = { + name: formatMessage({ + id: 'mesh.hostPeripheralName', + default: 'Host Mesh [{ MESH_ID }]', + description: 'label for "Host Mesh" in connect modal for Mesh extension' + }, {MESH_ID: this.blocks.makeMeshIdLabel(this.meshId)}), + peripheralId: hostMeshId, + rssi: 0 + }; + + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_LIST_UPDATE); + + if (this.isWebSocketOpened()) { + this.sendWebSocketMessage('list', { + meshId: this.meshId + }); + } else { + this.openWebSocket(); + } + } catch (error) { + this.errorLog(`Failed to scan: reason=<${error}>`); + } + } + + connect () { + } + + isConnected () { + return this.connectionState === 'connected'; + } + + disconnect () { + if (this.connectionState === 'disconnected') { + this.infoLog('Already disconnected.'); + return; + } + if (this.connectionState === 'disconnecting') { + this.infoLog('Now disconnecting, please wait to disconnect.'); + return; + } + + if (this.connectTimeoutId) { + clearTimeout(this.connectTimeoutId); + this.connectTimeoutId = null; + } + + this.setConnectionState('disconnecting'); + this.webSocket.close(); + + Object.keys(this.rtcConnections).forEach(meshId => { + this.rtcConnections[meshId].close(); + }); + this.rtcConnections = {}; + this.rtcDataChannels = {}; + } + + setConnectionState (connectionState) { + this.debugLog(`set connection state: from=<${this.connectionState}> to=<${connectionState}>`); + + const prevConnectionState = this.connectionState; + + this.connectionState = connectionState; + + switch (this.connectionState) { + case 'connected': + clearTimeout(this.connectTimeoutId); + this.connectTimeoutId = null; + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_CONNECTED); + break; + case 'request_error': + this.disconnect(); + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR); + break; + case 'disconnected': + if (prevConnectionState === 'disconnecting') { + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_DISCONNECTED); + } else if (prevConnectionState === 'connecting') { + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR); + } else { + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR); + } + break; + } + } + + onWebSocketOpen () { + try { + this.debugLog('WebSocket opened.'); + + this.sendWebSocketMessage('list', { + meshId: this.meshId + }); + } catch (error) { + this.errorLog(`Failed in WebSocket open event handler: reason=<${error}>`); + } + } + + onWebSocketMessage (event) { + try { + this.debugLog(`Received WebSocket message: message=<${event.data}>`); + + const message = JSON.parse(event.data); + const {action, result, data} = message; + + const actionMethodName = `${action}WebSocketAction`; + if (this[actionMethodName]) { + this.infoLog(`Process WebSocket message: ` + + `action=${action} result=<${result}> data=<${JSON.stringify(data)}>`); + this[actionMethodName](result, data); + } else { + this.errorLog(`Unknown WebSocket message action: ${action}`); + } + } catch (error) { + this.errorLog(`Failed to process WebSocket message: reason=<${error}>`); + } + } + + onWebSocketClose () { + this.debugLog('WebSocket closed.'); + } + + onWebSocketError (event) { + this.errorLog(`Occured WebSocket error: ${event}`); + } + + listWebSocketAction (result, data) { + if (!result) { + this.errorLog(`Failed to list: reason=<${data.error}>`); + return; + } + + const now = Math.floor(Date.now() / 1000); + data.hosts.forEach(host => { + const t = host.ttl - now; + let rssi; + if (t >= 4 * 60) { + rssi = 0; + } else if (t >= 3 * 60) { + rssi = -20; + } else if (t >= 2 * 60) { + rssi = -40; + } else if (t >= 1 * 60) { + rssi = -60; + } else { + rssi = -80; + } + this.availablePeripherals[host.meshId] = { + name: formatMessage({ + id: 'mesh.clientPeripheralName', + default: 'Join Mesh [{ MESH_ID }]', + description: 'label for "Join Mesh" in connect modal for Mesh extension' + }, {MESH_ID: this.blocks.makeMeshIdLabel(host.meshId)}), + peripheralId: host.meshId, + rssi: rssi + }; + }); + + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_LIST_UPDATE); + } + + sendWebSocketMessage (action, data, result) { + const message = { + action: action, + data: data + }; + if (typeof result !== 'undefined') { + message.result = result; + } + + this.debugLog(`Send WebSocket message: message=<${JSON.stringify(message)}>`); + + this.webSocket.send(JSON.stringify(message)); + } + + openRTCPeerConnection (meshId) { + if (this.rtcConnections[meshId]) { + this.infoLog(`Already open WebRTC connection: peer=<${meshId}>`); + + const channel = this.rtcDataChannels[meshId]; + if (channel) { + channel.onopen = null; + channel.onmessage = null; + channel.onclose = null; + delete this.rtcDataChannels[meshId]; + } + this.closeRTCPeerConnection(meshId); + } + + this.debugLog(`Open WebRTC connection: peer=<${meshId}>`); + + const connection = new RTCPeerConnection({ + iceServers: [ + { + urls: [ + 'stun:stun.l.google.com:19302', + 'stun:stun1.l.google.com:19302', + 'stun:stun2.l.google.com:19302', + 'stun:stun3.l.google.com:19302', + 'stun:stun4.l.google.com:19302' + ] + } + ] + }); + this.rtcConnections[meshId] = connection; + return connection; + } + + onRTCConnectionStateChange (connection, peerMeshId) { + this.debugLog(`Changed WebRTC connection state: ${connection.connectionState}`); + + switch (connection.connectionState) { + case 'disconnected': + this.errorLog(`Disconnected WebRTC connection by peer: peer=<${peerMeshId}>`); + this.closeRTCPeerConnection(peerMeshId); + break; + case 'failed': + this.errorLog(`Failed WebRTC connection: peer=<${peerMeshId}>`); + this.closeRTCPeerConnection(peerMeshId); + break; + } + } + + onRTCICECandidate (connection, peerMeshId, event, onDescriptionCreate) { + if (event.candidate) { + this.debugLog(`ICE candidate: peer=<${peerMeshId}> candidate=<\n` + + `${JSON.stringify(event.candidate, null, 2)}\n` + + `>`); + } else { + onDescriptionCreate(connection.localDescription); + } + } + + closeRTCPeerConnection (meshId) { + this.debugLog(`Close WebRTC connection: peer=<${meshId}>`); + + const connection = this.rtcConnections[meshId]; + if (connection) { + connection.close(); + delete this.rtcConnections[meshId]; + } + } + + getGlobalVariables () { + const variables = {}; + const stage = this.runtime.getTargetForStage(); + for (const varId in stage.variables) { + const currVar = stage.variables[varId]; + if (currVar.type === Variable.SCALAR_TYPE) { + variables[currVar.name] = { + name: currVar.name, + value: currVar.value, + owner: this.meshId + }; + } + } + return variables; + } + + onRTCDataChannelOpen (connection, dataChannel, peerMeshId) { + this.debugLog(`Open WebRTC data channel: peer=<${peerMeshId}>`); + + this.revertWebRTCIPHandlingPolicy(); + + if (this.rtcDataChannels[peerMeshId]) { + this.errorLog(`Already open WebRTC data channel: peer=<${peerMeshId}>`); + } + this.rtcDataChannels[peerMeshId] = dataChannel; + + this.sendVariablesTo(this.getGlobalVariables(), peerMeshId); + } + + onRTCDataChannelMessage (connection, dataChannel, peerMeshId, event) { + try { + this.debugLog(`Received WebRTC message: peer=<${peerMeshId}> data=<${event.data}>`); + + const message = JSON.parse(event.data); + + const {type, data} = message; + + const actionMethodName = `${type}RTCAction`; + if (this[actionMethodName]) { + this.infoLog(`Process WebRTC message: ` + + `type=${type} peer=<${peerMeshId}> data=<${JSON.stringify(data)}>`); + + this[actionMethodName](peerMeshId, message); + } else { + this.errorLog(`Unknown WebRTC message type: type=<${type}> peer=<${peerMeshId}>`); + } + } catch (error) { + this.errorLog(`Failed to process WebRTC message: ${error}`); + return; + } + } + + onRTCDataChannelClose (connection, dataChannel, peerMeshId) { + this.debugLog(`Close WebRTC data channel: peer=<${peerMeshId}>`); + + this.closeRTCPeerConnection(peerMeshId); + delete this.rtcDataChannels[peerMeshId]; + } + + sendMessageToChromeMeshExtension (action) { + try { + this.debugLog(`Send message to Chrome mesh extension: action=<${action}>`); + + chrome.runtime.sendMessage(CHROME_MESH_EXTENSION_ID, {action: action}, null, response => { + if (typeof chrome.runtime.lastError === 'undefined') { + this.debugLog(`Succeeded sending message to Chrome mesh extension: ` + + `response=<${JSON.stringify(response)}>`); + } else { + this.errorLog(`Failed to send message to Chrome extension: ` + + `lastError=<${JSON.stringify(chrome.runtime.lastError)}>`); + } + }); + } catch (error) { + this.debugLog(`Failed to send message to Chrome extension: ${error}`); + } + } + + changeWebRTCIPHandlingPolicy () { + this.debugLog('Change WebRTC IPHandlingPolicy to default.'); + + this.sendMessageToChromeMeshExtension('change'); + } + + revertWebRTCIPHandlingPolicy () { + this.debugLog('Revert WebRTC IPHandlingPolicy to before default.'); + + this.sendMessageToChromeMeshExtension('revert'); + } + + emitPeripheralEvent (event) { + this.debugLog(`Emit Peripheral event: event=<${event}>`); + + if (event === this.runtime.constructor.PERIPHERAL_LIST_UPDATE) { + this.runtime.emit(event, this.availablePeripherals); + } else { + this.runtime.emit(event, { + extensionId: this.blocks.constructor.EXTENSION_ID + }); + } + } + + setVariable (name, value, owner) { + if (this.variables[name]) { + this.infoLog(`Update variable: name=<${name}> value=<${value}> from=<${this.getVariable(name)}> ` + + `owner=<${owner}>`); + } else { + this.infoLog(`Create variable: name=<${name}> value=<${value}> ` + + `owner=<${owner}>`); + } + + if (!this.variableNames.includes(name)) { + this.variableNames.push(name); + } + + this.variables[name] = { + name: name, + value: value, + owner: owner + }; + } + + getVariable (name) { + const variable = this.variables[name]; + if (!variable) { + return ''; + } + return variable.value; + } + + sendRTCMessage (message) { + const peers = Object.keys(this.rtcDataChannels); + + this.debugLog(`Send WebRTC message to all peers: ` + + `message=<${JSON.stringify(message)}> peers=<${peers.join(', ')}>`); + + try { + peers.forEach(meshId => { + const channel = this.rtcDataChannels[meshId]; + channel.send(JSON.stringify(message)); + }); + } catch (error) { + this.errorLog(`Failed to send WebRTC message: error=<${error}> message=<${JSON.stringify(message)}>`); + } + } + + sendRTCBroadcastMessage (name) { + this.sendRTCMessage({ + owner: this.meshId, + type: 'broadcast', + data: { + name: name + } + }); + } + + sendRTCVariableMessage (name, value) { + this.sendRTCMessage({ + owner: this.meshId, + type: 'variable', + data: { + name: name, + value: value + } + }); + } + + sendVariablesTo (variables, peerMeshId) { + const channel = this.rtcDataChannels[peerMeshId]; + Object.keys(variables).forEach(name => { + const variable = variables[name]; + + const message = { + owner: variable.owner, + type: 'variable', + data: { + name: variable.name, + value: variable.value + } + }; + + this.debugLog(`Send WebRTC message: ` + + `message=<${JSON.stringify(message)}> peer=<${peerMeshId}>`); + + channel.send(JSON.stringify(message)); + }); + } + + debugLog (message) { + if (DEBUG) { + log.log(`(${this.logPrefix}) ${message}`); + } + } + + infoLog (message) { + log.info(`(${this.logPrefix}) ${message}`); + } + + errorLog (message) { + log.error(`(${this.logPrefix}) ${message}`); + } +} + +module.exports = MeshService; From 49206e4d7275e763fd4b4e37e45bdf8a25de0757 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 23 Apr 2020 23:08:53 +0900 Subject: [PATCH 39/42] changed wss url to api.smalruby.app. --- src/extensions/scratch3_mesh/mesh-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/scratch3_mesh/mesh-service.js b/src/extensions/scratch3_mesh/mesh-service.js index 70da3a829ac..e688ff27122 100644 --- a/src/extensions/scratch3_mesh/mesh-service.js +++ b/src/extensions/scratch3_mesh/mesh-service.js @@ -6,7 +6,7 @@ const Variable = require('../../engine/variable'); const DEBUG = true; const CHROME_MESH_EXTENSION_ID = 'ioaoebnfpgnbehdolokpdddomfnhpckn'; -const MESH_WSS_URL = 'wss://api.smalruby.jp/mesh-signaling'; +const MESH_WSS_URL = 'wss://api.smalruby.app/mesh-signaling'; class MeshService { constructor (blocks, meshId, webSocket) { From 5f70ec3aef032eec47f280f493edc63588e3a80d Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 9 May 2020 16:07:09 +0900 Subject: [PATCH 40/42] fixed. --- src/extensions/scratch3_mesh/index.js | 2 +- src/extensions/scratch3_mesh/mesh-host.js | 126 ++++++++----- src/extensions/scratch3_mesh/mesh-peer.js | 122 ++++++------ src/extensions/scratch3_mesh/mesh-service.js | 185 +++++++++---------- src/util/debug-logger.js | 15 ++ 5 files changed, 255 insertions(+), 195 deletions(-) create mode 100644 src/util/debug-logger.js diff --git a/src/extensions/scratch3_mesh/index.js b/src/extensions/scratch3_mesh/index.js index 4e7f64c5041..3deaa1a3427 100644 --- a/src/extensions/scratch3_mesh/index.js +++ b/src/extensions/scratch3_mesh/index.js @@ -132,7 +132,7 @@ class Scratch3MeshBlocks { * Disconnect from the Mesh. */ disconnect () { - this.meshService.disconnect(); + this.meshService.requestDisconnect(); } /** diff --git a/src/extensions/scratch3_mesh/mesh-host.js b/src/extensions/scratch3_mesh/mesh-host.js index 99c8b57ee7d..ed2a8778003 100644 --- a/src/extensions/scratch3_mesh/mesh-host.js +++ b/src/extensions/scratch3_mesh/mesh-host.js @@ -1,6 +1,12 @@ const MeshService = require('./mesh-service'); const MeshPeer = require('./mesh-peer'); +const log = require('../../util/log'); +const debugLogger = require('../../util/debug-logger'); +const debug = debugLogger(true); + +const HEATBEAT_MINUTES = 5; + class MeshHost extends MeshService { constructor (blocks, meshId, webSocket) { super(blocks, meshId, webSocket); @@ -14,11 +20,11 @@ class MeshHost extends MeshService { connect () { if (this.connectionState === 'connected') { - this.infoLog('Already connected'); + log.info('Already connected'); return; } if (this.connectionState === 'connecting') { - this.infoLog('Now connecting, please wait to connect.'); + log.info('Now connecting, please wait to connect.'); return; } @@ -39,22 +45,50 @@ class MeshHost extends MeshService { } } + restartHeatbeat () { + debug(() => { + const at = new Date(); + at.setSeconds(at.getSeconds() + HEATBEAT_MINUTES * 60); + return `Heatbeat: at=<${at.toLocaleString()}>`; + }); + + clearTimeout(this.restartHeatbeatTimeoutId); + this.restartHeatbeatTimeoutId = setTimeout(() => { + if (this.connectionState === 'connected') { + this.sendWebSocketMessage('heartbeat', {}); + + this.restartHeatbeat(); + } + }, HEATBEAT_MINUTES * 60 * 1000); + } + onWebSocketClose () { - this.setConnectionState('disconnected'); + debug(() => 'WebSocket closed.'); + + clearTimeout(this.restartHeatbeatTimeoutId); + this.restartHeatbeatTimeoutId = null; + + if (this.connectionState !== 'disconnected') { + this.disconnect(); + } } registerWebSocketAction (result, data) { if (!result) { this.setConnectionState('request_error'); - this.errorLog(`Failed to register: reason=<${data.error}>`); + log.error(`Failed to register: reason=<${data.error}>`); return; } this.setConnectionState('connected'); - this.infoLog('Connected as Mesh Host.'); + log.info('Connected as Mesh Host.'); + + this.restartHeatbeat(); } offerWebSocketAction (result, data) { + this.restartHeatbeat(); + const peerMeshId = data.meshId; if (data.hostMeshId !== this.meshId) { this.logError(`Invalid Mesh ID in offer from peer:` + @@ -67,45 +101,45 @@ class MeshHost extends MeshService { return; } - this.changeWebRTCIPHandlingPolicy(); - - const connection = this.openRTCPeerConnection(peerMeshId); - - connection.onconnectionstatechange = () => { - this.onRTCConnectionStateChange(connection, peerMeshId); - }; - connection.onicecandidate = event => { - this.onRTCICECandidate(connection, peerMeshId, event, description => { - this.debugLog(`Answer to Peer: peer=<${peerMeshId}> description=<\n` + - `${JSON.stringify(description, null, 2)}\n` + - `>`); - - this.sendWebSocketMessage('answer', { - meshId: this.meshId, - clientMeshId: peerMeshId, - hostDescription: description + this.changeWebRTCIPHandlingPolicy().then(() => { + const connection = this.openRTCPeerConnection(peerMeshId); + + connection.onconnectionstatechange = () => { + this.onRTCConnectionStateChange(connection, peerMeshId); + }; + connection.onicecandidate = event => { + this.onRTCICECandidate(connection, peerMeshId, event, description => { + debug(() => `Answer to Peer: peer=<${peerMeshId}> description=<\n` + + `${JSON.stringify(description, null, 2)}\n` + + `>`); + + this.sendWebSocketMessage('answer', { + meshId: this.meshId, + clientMeshId: peerMeshId, + hostDescription: description + }); }); - }); - }; - connection.ondatachannel = event => { - this.onRTCDataChannel(connection, peerMeshId, event); - }; - - connection.setRemoteDescription(new RTCSessionDescription(data.clientDescription)); - - connection.createAnswer().then( - desc => { - connection.setLocalDescription(desc); - }, - error => { - this.errorLog(`Failed createAnswer: peer=<${peerMeshId}> reason=<${error}>`); - this.closeRTCPeerConnection(peerMeshId); - } - ); + }; + connection.ondatachannel = event => { + this.onRTCDataChannel(connection, peerMeshId, event); + }; + + connection.setRemoteDescription(new RTCSessionDescription(data.clientDescription)); + + connection.createAnswer().then( + desc => { + connection.setLocalDescription(desc); + }, + error => { + log.error(`Failed createAnswer: peer=<${peerMeshId}> reason=<${error}>`); + this.closeRTCPeerConnection(peerMeshId); + } + ); + }); } onRTCDataChannel (connection, peerMeshId, event) { - this.debugLog(`WebRTC data channel by remote peer: peer=<${peerMeshId}>`); + debug(() => `WebRTC data channel by remote peer: peer=<${peerMeshId}>`); const dataChannel = event.channel; @@ -126,13 +160,21 @@ class MeshHost extends MeshService { } answerWebSocketAction (result, data) { + this.restartHeatbeat(); + if (!result) { this.closeRTCPeerConnection(data.clientMeshId); - this.errorLog(`Failed to answer: reason=<${data.error}>`); + log.error(`Failed to answer: reason=<${data.error}>`); return; } - this.infoLog(`Answered to peer: peer=<${data.clientMeshId}>`); + log.info(`Answered to peer: peer=<${data.clientMeshId}>`); + } + + heartbeatWebSocketAction (result, data) { + this.restartHeatbeat(); + + log.info(`Heartbeat: result=<${result ? 'OK' : 'NG'}>`); } broadcastRTCAction (peerMeshId, message) { diff --git a/src/extensions/scratch3_mesh/mesh-peer.js b/src/extensions/scratch3_mesh/mesh-peer.js index 12a0eaec923..6771bada00d 100644 --- a/src/extensions/scratch3_mesh/mesh-peer.js +++ b/src/extensions/scratch3_mesh/mesh-peer.js @@ -1,6 +1,10 @@ const MeshService = require('./mesh-service'); const BlockUtility = require('../../engine/block-utility.js'); +const log = require('../../util/log'); +const debugLogger = require('../../util/debug-logger'); +const debug = debugLogger(true); + class MeshPeer extends MeshService { get logPrefix () { return 'Mesh Peer'; @@ -8,18 +12,18 @@ class MeshPeer extends MeshService { connect (hostMeshId) { if (this.connectionState === 'connected') { - this.infoLog('Already connected'); + log.info('Already connected'); return; } if (this.connectionState === 'connecting') { - this.infoLog('Now connecting, please wait to connect.'); + log.info('Now connecting, please wait to connect.'); return; } if (!hostMeshId || hostMeshId.trim() === '') { this.setConnectionState('request_error'); - this.errorLog('Not select Host Mesh ID'); + log.error('Not select Host Mesh ID'); return; } @@ -27,50 +31,50 @@ class MeshPeer extends MeshService { this.setConnectionState('connecting'); - this.changeWebRTCIPHandlingPolicy(); - - const connection = this.openRTCPeerConnection(hostMeshId); - - connection.onconnectionstatechange = () => { - this.onRTCConnectionStateChange(connection, hostMeshId); - }; - connection.onicecandidate = event => { - this.onRTCICECandidate(connection, hostMeshId, event, description => { - this.debugLog(`Offer to Host: host=<${hostMeshId}> description=<\n` + - `${JSON.stringify(description, null, 2)}\n` + - `>`); - - this.sendWebSocketMessage('offer', { - meshId: this.meshId, - hostMeshId: hostMeshId, - clientDescription: description + this.changeWebRTCIPHandlingPolicy().then(() => { + const connection = this.openRTCPeerConnection(hostMeshId); + + connection.onconnectionstatechange = () => { + this.onRTCConnectionStateChange(connection, hostMeshId); + }; + connection.onicecandidate = event => { + this.onRTCICECandidate(connection, hostMeshId, event, description => { + debug(() => `Offer to Host: host=<${hostMeshId}> description=<\n` + + `${JSON.stringify(description, null, 2)}\n` + + `>`); + + this.sendWebSocketMessage('offer', { + meshId: this.meshId, + hostMeshId: hostMeshId, + clientDescription: description + }); }); - }); - }; - - const dataChannel = connection.createDataChannel('dataChannel'); - dataChannel.onopen = () => { - this.onRTCDataChannelOpen(connection, dataChannel, hostMeshId); - }; - dataChannel.onmessage = e => { - this.onRTCDataChannelMessage(connection, dataChannel, hostMeshId, e); - }; - dataChannel.onclose = () => { - this.onRTCDataChannelClose(connection, dataChannel, hostMeshId); - }; - - connection.createOffer().then( - desc => { - connection.setLocalDescription(desc); - }, - error => { - this.setConnectionState('request_error'); - this.errorLog(`Failed createOffer: host=<${hostMeshId}> reason=<${error}>`); - } - ); - - this.connectTimeoutId = - setTimeout(this.onConnectTimeout.bind(this), this.connectTimeoutSeconds * 1000); + }; + + const dataChannel = connection.createDataChannel('dataChannel'); + dataChannel.onopen = () => { + this.onRTCDataChannelOpen(connection, dataChannel, hostMeshId); + }; + dataChannel.onmessage = e => { + this.onRTCDataChannelMessage(connection, dataChannel, hostMeshId, e); + }; + dataChannel.onclose = () => { + this.onRTCDataChannelClose(connection, dataChannel, hostMeshId); + }; + + connection.createOffer().then( + desc => { + connection.setLocalDescription(desc); + }, + error => { + this.setConnectionState('request_error'); + log.error(`Failed createOffer: host=<${hostMeshId}> reason=<${error}>`); + } + ); + + this.connectTimeoutId = + setTimeout(this.onConnectTimeout.bind(this), this.connectTimeoutSeconds * 1000); + }); } onConnectTimeout () { @@ -87,38 +91,40 @@ class MeshPeer extends MeshService { } onWebSocketClose () { - this.debugLog('WebSocket closed.'); + debug(() => 'WebSocket closed.'); } offerWebSocketAction (result, data) { if (!result) { this.setConnectionState('request_error'); - this.errorLog(`Failed to offer: reason=<${data.error}>`); + log.error(`Failed to offer: reason=<${data.error}>`); return; } - this.infoLog(`Offered to host: host=<${data.hostMeshId}>`); + log.info(`Offered to host: host=<${data.hostMeshId}>`); } answerWebSocketAction (result, data) { const hostMeshId = data.meshId; if (this.connectionState !== 'connecting') { - this.logError(`Received answer, but WebRTC not connecting: host=<${hostMeshId}>`); + log.error(`Received answer, but WebRTC not connecting: host=<${hostMeshId}>`); return; } if (data.clientMeshId !== this.meshId) { - this.setConnectionState('request_error'); + this.disconnect(); - this.logError(`Invalid Mesh ID in answer from host:` + - ` host=<${hostMeshId}> received=<${data.clientMeshId}> own=<${this.meshId}>`); + log.error(`Invalid Mesh ID in answer from host:` + + ` host=<${hostMeshId}> received=<${data.clientMeshId}> own=<${this.meshId}>`); return; } const connection = this.rtcConnections[hostMeshId]; connection.setRemoteDescription(new RTCSessionDescription(data.hostDescription)); + + log.info(`Received answer and set host description: host=<${hostMeshId}>`); } onRTCDataChannelOpen (connection, dataChannel, peerMeshId) { @@ -130,18 +136,20 @@ class MeshPeer extends MeshService { onRTCDataChannelClose (connection, dataChannel, peerMeshId) { MeshService.prototype.onRTCDataChannelClose.call(this, connection, dataChannel, peerMeshId); - this.setConnectionState('disconnected'); + if (this.connectState !== 'disconnected') { + this.disconnect(); + } } broadcastRTCAction (peerMeshId, message) { const broadcast = message.data; if (this.meshId === message.owner) { - this.debugLog(`Ignore broadcast: reason= ${JSON.stringify(broadcast)}`); + debug(() => `Ignore broadcast: reason= ${JSON.stringify(broadcast)}`); return; } - this.debugLog(`Process broadcast: name=<${broadcast.name}>`); + debug(() => `Process broadcast: name=<${broadcast.name}>`); const args = { BROADCAST_OPTION: { @@ -160,7 +168,7 @@ class MeshPeer extends MeshService { const variable = message.data; if (this.meshId === message.owner) { - this.debugLog(`Ignore variable: reason= ${JSON.stringify(variable)}`); + debug(() => `Ignore variable: reason= ${JSON.stringify(variable)}`); return; } diff --git a/src/extensions/scratch3_mesh/mesh-service.js b/src/extensions/scratch3_mesh/mesh-service.js index e688ff27122..e2b42856c19 100644 --- a/src/extensions/scratch3_mesh/mesh-service.js +++ b/src/extensions/scratch3_mesh/mesh-service.js @@ -1,12 +1,15 @@ /* global chrome */ -const log = require('../../util/log'); const formatMessage = require('format-message'); const Variable = require('../../engine/variable'); -const DEBUG = true; +const log = require('../../util/log'); +const debugLogger = require('../../util/debug-logger'); +const debug = debugLogger(true); + const CHROME_MESH_EXTENSION_ID = 'ioaoebnfpgnbehdolokpdddomfnhpckn'; -const MESH_WSS_URL = 'wss://api.smalruby.app/mesh-signaling'; +//const MESH_WSS_URL = 'wss://api.smalruby.app/mesh-signaling'; +const MESH_WSS_URL = 'ws://localhost:8080'; class MeshService { constructor (blocks, meshId, webSocket) { @@ -62,7 +65,7 @@ class MeshService { scan (hostMeshId) { try { - this.debugLog(`Scan: hostMeshId=<${hostMeshId}>`); + debug(() => `Scan: hostMeshId=<${hostMeshId}>`); this.availablePeripherals = {}; this.availablePeripherals[hostMeshId] = { @@ -85,7 +88,7 @@ class MeshService { this.openWebSocket(); } } catch (error) { - this.errorLog(`Failed to scan: reason=<${error}>`); + log.error(`Failed to scan: reason=<${error}>`); } } @@ -96,13 +99,18 @@ class MeshService { return this.connectionState === 'connected'; } + requestDisconnect () { + debug(() => 'MeshService.requestDisconnect'); + + this.setConnectionState('disconnecting'); + this.disconnect(); + } + disconnect () { + debug(() => 'MeshService.disconnect'); + if (this.connectionState === 'disconnected') { - this.infoLog('Already disconnected.'); - return; - } - if (this.connectionState === 'disconnecting') { - this.infoLog('Now disconnecting, please wait to disconnect.'); + log.info('Already disconnected.'); return; } @@ -111,7 +119,6 @@ class MeshService { this.connectTimeoutId = null; } - this.setConnectionState('disconnecting'); this.webSocket.close(); Object.keys(this.rtcConnections).forEach(meshId => { @@ -119,10 +126,12 @@ class MeshService { }); this.rtcConnections = {}; this.rtcDataChannels = {}; + + this.setConnectionState('disconnected'); } setConnectionState (connectionState) { - this.debugLog(`set connection state: from=<${this.connectionState}> to=<${connectionState}>`); + debug(() => `set connection state: from=<${this.connectionState}> to=<${connectionState}>`); const prevConnectionState = this.connectionState; @@ -134,65 +143,60 @@ class MeshService { this.connectTimeoutId = null; this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_CONNECTED); break; - case 'request_error': - this.disconnect(); - this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR); - break; case 'disconnected': - if (prevConnectionState === 'disconnecting') { - this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_DISCONNECTED); - } else if (prevConnectionState === 'connecting') { + if (prevConnectionState === 'connecting') { this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_REQUEST_ERROR); - } else { + } else if (prevConnectionState !== 'disconnecting' && prevConnectionState !== 'disconnected') { this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR); } + this.emitPeripheralEvent(this.runtime.constructor.PERIPHERAL_DISCONNECTED); break; } } onWebSocketOpen () { try { - this.debugLog('WebSocket opened.'); + debug(() => 'WebSocket opened.'); this.sendWebSocketMessage('list', { meshId: this.meshId }); } catch (error) { - this.errorLog(`Failed in WebSocket open event handler: reason=<${error}>`); + log.error(`Failed in WebSocket open event handler: reason=<${error}>`); } } onWebSocketMessage (event) { try { - this.debugLog(`Received WebSocket message: message=<${event.data}>`); + debug(() => `Received WebSocket message: message=<${event.data}>`); const message = JSON.parse(event.data); const {action, result, data} = message; const actionMethodName = `${action}WebSocketAction`; if (this[actionMethodName]) { - this.infoLog(`Process WebSocket message: ` + - `action=${action} result=<${result}> data=<${JSON.stringify(data)}>`); + log.info(`Process WebSocket message: ` + + `action=${action} result=<${result}> data=<${JSON.stringify(data)}>`); this[actionMethodName](result, data); } else { - this.errorLog(`Unknown WebSocket message action: ${action}`); + log.error(`Unknown WebSocket message action: ${action}`); } } catch (error) { - this.errorLog(`Failed to process WebSocket message: reason=<${error}>`); + log.error(`Failed to process WebSocket message: reason=<${error}>`); } } onWebSocketClose () { - this.debugLog('WebSocket closed.'); + debug(() => 'WebSocket closed.'); } onWebSocketError (event) { - this.errorLog(`Occured WebSocket error: ${event}`); + log.error(`Occured WebSocket error: ${event}`); } listWebSocketAction (result, data) { if (!result) { - this.errorLog(`Failed to list: reason=<${data.error}>`); + log.error(`Failed to list: reason=<${data.error}>`); return; } @@ -234,14 +238,14 @@ class MeshService { message.result = result; } - this.debugLog(`Send WebSocket message: message=<${JSON.stringify(message)}>`); + debug(() => `Send WebSocket message: message=<${JSON.stringify(message)}>`); this.webSocket.send(JSON.stringify(message)); } openRTCPeerConnection (meshId) { if (this.rtcConnections[meshId]) { - this.infoLog(`Already open WebRTC connection: peer=<${meshId}>`); + log.info(`Already open WebRTC connection: peer=<${meshId}>`); const channel = this.rtcDataChannels[meshId]; if (channel) { @@ -253,7 +257,7 @@ class MeshService { this.closeRTCPeerConnection(meshId); } - this.debugLog(`Open WebRTC connection: peer=<${meshId}>`); + debug(() => `Open WebRTC connection: peer=<${meshId}>`); const connection = new RTCPeerConnection({ iceServers: [ @@ -273,15 +277,15 @@ class MeshService { } onRTCConnectionStateChange (connection, peerMeshId) { - this.debugLog(`Changed WebRTC connection state: ${connection.connectionState}`); + debug(() => `Changed WebRTC connection state: ${connection.connectionState}`); switch (connection.connectionState) { case 'disconnected': - this.errorLog(`Disconnected WebRTC connection by peer: peer=<${peerMeshId}>`); + log.error(`Disconnected WebRTC connection by peer: peer=<${peerMeshId}>`); this.closeRTCPeerConnection(peerMeshId); break; case 'failed': - this.errorLog(`Failed WebRTC connection: peer=<${peerMeshId}>`); + log.error(`Failed WebRTC connection: peer=<${peerMeshId}>`); this.closeRTCPeerConnection(peerMeshId); break; } @@ -289,16 +293,16 @@ class MeshService { onRTCICECandidate (connection, peerMeshId, event, onDescriptionCreate) { if (event.candidate) { - this.debugLog(`ICE candidate: peer=<${peerMeshId}> candidate=<\n` + - `${JSON.stringify(event.candidate, null, 2)}\n` + - `>`); + debug(() => `ICE candidate: peer=<${peerMeshId}> candidate=<\n` + + `${JSON.stringify(event.candidate, null, 2)}\n` + + `>`); } else { onDescriptionCreate(connection.localDescription); } } closeRTCPeerConnection (meshId) { - this.debugLog(`Close WebRTC connection: peer=<${meshId}>`); + debug(() => `Close WebRTC connection: peer=<${meshId}>`); const connection = this.rtcConnections[meshId]; if (connection) { @@ -324,12 +328,12 @@ class MeshService { } onRTCDataChannelOpen (connection, dataChannel, peerMeshId) { - this.debugLog(`Open WebRTC data channel: peer=<${peerMeshId}>`); + debug(() => `Open WebRTC data channel: peer=<${peerMeshId}>`); this.revertWebRTCIPHandlingPolicy(); if (this.rtcDataChannels[peerMeshId]) { - this.errorLog(`Already open WebRTC data channel: peer=<${peerMeshId}>`); + log.error(`Already open WebRTC data channel: peer=<${peerMeshId}>`); } this.rtcDataChannels[peerMeshId] = dataChannel; @@ -338,7 +342,7 @@ class MeshService { onRTCDataChannelMessage (connection, dataChannel, peerMeshId, event) { try { - this.debugLog(`Received WebRTC message: peer=<${peerMeshId}> data=<${event.data}>`); + debug(() => `Received WebRTC message: peer=<${peerMeshId}> data=<${event.data}>`); const message = JSON.parse(event.data); @@ -346,75 +350,80 @@ class MeshService { const actionMethodName = `${type}RTCAction`; if (this[actionMethodName]) { - this.infoLog(`Process WebRTC message: ` + - `type=${type} peer=<${peerMeshId}> data=<${JSON.stringify(data)}>`); + log.info(`Process WebRTC message: ` + + `type=${type} peer=<${peerMeshId}> data=<${JSON.stringify(data)}>`); this[actionMethodName](peerMeshId, message); } else { - this.errorLog(`Unknown WebRTC message type: type=<${type}> peer=<${peerMeshId}>`); + log.error(`Unknown WebRTC message type: type=<${type}> peer=<${peerMeshId}>`); } } catch (error) { - this.errorLog(`Failed to process WebRTC message: ${error}`); + log.error(`Failed to process WebRTC message: ${error}`); return; } } onRTCDataChannelClose (connection, dataChannel, peerMeshId) { - this.debugLog(`Close WebRTC data channel: peer=<${peerMeshId}>`); + debug(() => `Close WebRTC data channel: peer=<${peerMeshId}>`); + + this.revertWebRTCIPHandlingPolicy(); this.closeRTCPeerConnection(peerMeshId); delete this.rtcDataChannels[peerMeshId]; } sendMessageToChromeMeshExtension (action) { - try { - this.debugLog(`Send message to Chrome mesh extension: action=<${action}>`); - - chrome.runtime.sendMessage(CHROME_MESH_EXTENSION_ID, {action: action}, null, response => { - if (typeof chrome.runtime.lastError === 'undefined') { - this.debugLog(`Succeeded sending message to Chrome mesh extension: ` + - `response=<${JSON.stringify(response)}>`); - } else { - this.errorLog(`Failed to send message to Chrome extension: ` + + debug(() => `Send message to Chrome mesh extension: action=<${action}>`); + + return new Promise((resolve, reject) => { + try { + chrome.runtime.sendMessage(CHROME_MESH_EXTENSION_ID, {action: action}, null, response => { + if (typeof chrome.runtime.lastError === 'undefined') { + debug(() => `Succeeded sending message to Chrome mesh extension: ` + + `response=<${JSON.stringify(response)}>`); + } else { + log.error(`Failed to send message to Chrome extension: ` + `lastError=<${JSON.stringify(chrome.runtime.lastError)}>`); - } - }); - } catch (error) { - this.debugLog(`Failed to send message to Chrome extension: ${error}`); - } + } + resolve(); + }); + } catch (error) { + debug(() => `Failed to send message to Chrome extension: ${error}`); + resolve(); + } + }); } changeWebRTCIPHandlingPolicy () { - this.debugLog('Change WebRTC IPHandlingPolicy to default.'); + debug(() => 'Change WebRTC IPHandlingPolicy to default.'); - this.sendMessageToChromeMeshExtension('change'); + return this.sendMessageToChromeMeshExtension('change'); } revertWebRTCIPHandlingPolicy () { - this.debugLog('Revert WebRTC IPHandlingPolicy to before default.'); + debug(() => 'Revert WebRTC IPHandlingPolicy to before default.'); - this.sendMessageToChromeMeshExtension('revert'); + return this.sendMessageToChromeMeshExtension('revert'); } emitPeripheralEvent (event) { - this.debugLog(`Emit Peripheral event: event=<${event}>`); + debug(() => `Emit Peripheral event: event=<${event}>`); if (event === this.runtime.constructor.PERIPHERAL_LIST_UPDATE) { - this.runtime.emit(event, this.availablePeripherals); - } else { - this.runtime.emit(event, { - extensionId: this.blocks.constructor.EXTENSION_ID - }); + return new Promise(() => this.runtime.emit(event, this.availablePeripherals)); } + return new Promise(() => this.runtime.emit(event, { + extensionId: this.blocks.constructor.EXTENSION_ID + })); } setVariable (name, value, owner) { if (this.variables[name]) { - this.infoLog(`Update variable: name=<${name}> value=<${value}> from=<${this.getVariable(name)}> ` + - `owner=<${owner}>`); + log.info(`Update variable: name=<${name}> value=<${value}> from=<${this.getVariable(name)}> ` + + `owner=<${owner}>`); } else { - this.infoLog(`Create variable: name=<${name}> value=<${value}> ` + - `owner=<${owner}>`); + log.info(`Create variable: name=<${name}> value=<${value}> ` + + `owner=<${owner}>`); } if (!this.variableNames.includes(name)) { @@ -439,8 +448,8 @@ class MeshService { sendRTCMessage (message) { const peers = Object.keys(this.rtcDataChannels); - this.debugLog(`Send WebRTC message to all peers: ` + - `message=<${JSON.stringify(message)}> peers=<${peers.join(', ')}>`); + debug(() => `Send WebRTC message to all peers: ` + + `message=<${JSON.stringify(message)}> peers=<${peers.join(', ')}>`); try { peers.forEach(meshId => { @@ -448,7 +457,7 @@ class MeshService { channel.send(JSON.stringify(message)); }); } catch (error) { - this.errorLog(`Failed to send WebRTC message: error=<${error}> message=<${JSON.stringify(message)}>`); + log.error(`Failed to send WebRTC message: error=<${error}> message=<${JSON.stringify(message)}>`); } } @@ -487,26 +496,12 @@ class MeshService { } }; - this.debugLog(`Send WebRTC message: ` + - `message=<${JSON.stringify(message)}> peer=<${peerMeshId}>`); + debug(() => `Send WebRTC message: ` + + `message=<${JSON.stringify(message)}> peer=<${peerMeshId}>`); channel.send(JSON.stringify(message)); }); } - - debugLog (message) { - if (DEBUG) { - log.log(`(${this.logPrefix}) ${message}`); - } - } - - infoLog (message) { - log.info(`(${this.logPrefix}) ${message}`); - } - - errorLog (message) { - log.error(`(${this.logPrefix}) ${message}`); - } } module.exports = MeshService; diff --git a/src/util/debug-logger.js b/src/util/debug-logger.js new file mode 100644 index 00000000000..e094058dd17 --- /dev/null +++ b/src/util/debug-logger.js @@ -0,0 +1,15 @@ +const log = require('./log'); + +const debugLogger = debugFlag => { + const debug = func => { + if (debugFlag) { + const message = func(); + if (message) { + log.debug(message); + } + } + }; + return debug; +} + +module.exports = debugLogger; From 6a3e230b9b158c044eb49e8ae6ef66b9ad084b7d Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 9 May 2020 18:02:33 +0900 Subject: [PATCH 41/42] fixed. --- src/extensions/scratch3_mesh/mesh-host.js | 20 +++++++++++++------- src/extensions/scratch3_mesh/mesh-service.js | 3 +-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/extensions/scratch3_mesh/mesh-host.js b/src/extensions/scratch3_mesh/mesh-host.js index ed2a8778003..c5b143e5a94 100644 --- a/src/extensions/scratch3_mesh/mesh-host.js +++ b/src/extensions/scratch3_mesh/mesh-host.js @@ -5,7 +5,7 @@ const log = require('../../util/log'); const debugLogger = require('../../util/debug-logger'); const debug = debugLogger(true); -const HEATBEAT_MINUTES = 5; +const HEATBEAT_MINUTES = 6; class MeshHost extends MeshService { constructor (blocks, meshId, webSocket) { @@ -55,7 +55,9 @@ class MeshHost extends MeshService { clearTimeout(this.restartHeatbeatTimeoutId); this.restartHeatbeatTimeoutId = setTimeout(() => { if (this.connectionState === 'connected') { - this.sendWebSocketMessage('heartbeat', {}); + this.sendWebSocketMessage('heartbeat', { + meshId: this.meshId + }); this.restartHeatbeat(); } @@ -87,12 +89,10 @@ class MeshHost extends MeshService { } offerWebSocketAction (result, data) { - this.restartHeatbeat(); - const peerMeshId = data.meshId; if (data.hostMeshId !== this.meshId) { - this.logError(`Invalid Mesh ID in offer from peer:` + - ` peer=<${peerMeshId}> received=<${data.hostMeshId}> own=<${this.meshId}>`); + log.error(`Invalid Mesh ID in offer from peer:` + + ` peer=<${peerMeshId}> received=<${data.hostMeshId}> own=<${this.meshId}>`); this.sendWebSocketMessage('answer', { meshId: this.meshId, @@ -174,7 +174,13 @@ class MeshHost extends MeshService { heartbeatWebSocketAction (result, data) { this.restartHeatbeat(); - log.info(`Heartbeat: result=<${result ? 'OK' : 'NG'}>`); + debug(() => `Heartbeat: result=<${result ? 'OK' : 'NG'}>`); + + if (!result) { + log.error('Failed Heartbeat: reason='); + + this.webSocket.close(); + } } broadcastRTCAction (peerMeshId, message) { diff --git a/src/extensions/scratch3_mesh/mesh-service.js b/src/extensions/scratch3_mesh/mesh-service.js index e2b42856c19..f998716558b 100644 --- a/src/extensions/scratch3_mesh/mesh-service.js +++ b/src/extensions/scratch3_mesh/mesh-service.js @@ -8,8 +8,7 @@ const debugLogger = require('../../util/debug-logger'); const debug = debugLogger(true); const CHROME_MESH_EXTENSION_ID = 'ioaoebnfpgnbehdolokpdddomfnhpckn'; -//const MESH_WSS_URL = 'wss://api.smalruby.app/mesh-signaling'; -const MESH_WSS_URL = 'ws://localhost:8080'; +const MESH_WSS_URL = 'wss://api.smalruby.app/mesh-signaling'; class MeshService { constructor (blocks, meshId, webSocket) { From b68eea82a0a26c12dc62f7f84452bf80db41b042 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 9 May 2020 18:08:40 +0900 Subject: [PATCH 42/42] fixed heartbeat period. --- src/extensions/scratch3_mesh/mesh-host.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/scratch3_mesh/mesh-host.js b/src/extensions/scratch3_mesh/mesh-host.js index c5b143e5a94..9977920c872 100644 --- a/src/extensions/scratch3_mesh/mesh-host.js +++ b/src/extensions/scratch3_mesh/mesh-host.js @@ -5,7 +5,7 @@ const log = require('../../util/log'); const debugLogger = require('../../util/debug-logger'); const debug = debugLogger(true); -const HEATBEAT_MINUTES = 6; +const HEATBEAT_MINUTES = 4; class MeshHost extends MeshService { constructor (blocks, meshId, webSocket) {