diff --git a/examples/color-your-leds/README.md b/examples/color-your-leds/README.md new file mode 100644 index 0000000..24e5bfc --- /dev/null +++ b/examples/color-your-leds/README.md @@ -0,0 +1 @@ +# Color your LEDs \ No newline at end of file diff --git a/examples/color-your-leds/app.yaml b/examples/color-your-leds/app.yaml new file mode 100644 index 0000000..cbfc08d --- /dev/null +++ b/examples/color-your-leds/app.yaml @@ -0,0 +1,6 @@ +name: Color your LEDs +icon: 🎨 +description: Control the color of your LEDs from a web interface. + +bricks: + - arduino:web_ui diff --git a/examples/color-your-leds/assets/app.js b/examples/color-your-leds/assets/app.js new file mode 100644 index 0000000..9bf33f4 --- /dev/null +++ b/examples/color-your-leds/assets/app.js @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +// +// SPDX-License-Identifier: MPL-2.0 + +const led1ColorPicker = document.getElementById('led1-color'); +const led2ColorPicker = document.getElementById('led2-color'); +const led3ColorPicker = document.getElementById('led3-color'); +const led4ColorPicker = document.getElementById('led4-color'); +const led1HexDisplay = document.getElementById('led1-hex'); +const led2HexDisplay = document.getElementById('led2-hex'); +const led3HexDisplay = document.getElementById('led3-hex'); +const led4HexDisplay = document.getElementById('led4-hex'); +let errorContainer; + +/* + * Socket initialization. We need it to communicate with the server + */ +const socket = io(`http://${window.location.host}`); // Initialize socket.io connection + +// Start the application +document.addEventListener('DOMContentLoaded', () => { + errorContainer = document.getElementById('error-container'); + initSocketIO(); + + // Add event listeners + led1ColorPicker.addEventListener('input', function(e) { + led1HexDisplay.textContent = e.target.value; + const rgb = hexToRgb(e.target.value); + socket.emit('set_color', { led: 1, color: rgb }); + console.log(`LED 1 - R: ${rgb.r}, G: ${rgb.g}, B: ${rgb.b}`); + }); + + led2ColorPicker.addEventListener('input', function(e) { + led2HexDisplay.textContent = e.target.value; + const rgb = hexToRgb(e.target.value); + socket.emit('set_color', { led: 2, color: rgb }); + console.log(`LED 2 - R: ${rgb.r}, G: ${rgb.g}, B: ${rgb.b}`); + }); + + led3ColorPicker.addEventListener('input', function(e) { + led3HexDisplay.textContent = e.target.value; + const rgb = hexToRgb(e.target.value); + socket.emit('set_color', { led: 3, color: rgb }); + console.log(`LED 3 - R: ${rgb.r}, G: ${rgb.g}, B: ${rgb.b}`); + }); + + led4ColorPicker.addEventListener('input', function(e) { + led4HexDisplay.textContent = e.target.value; + const rgb = hexToRgb(e.target.value); + socket.emit('set_color', { led: 4, color: rgb }); + console.log(`LED 4 - R: ${rgb.r}, G: ${rgb.g}, B: ${rgb.b}`); + }); +}); + +// Function to convert hex to RGB +function hexToRgb(hex) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return { r, g, b }; +} + +function initSocketIO() { + socket.on('led_status_update', (message) => { + updateLedStatus(message); + }); + + socket.on('disconnect', () => { + if (errorContainer) { + errorContainer.textContent = 'Connection to the board lost. Please check the connection.'; + errorContainer.style.display = 'block'; + } + }); +} \ No newline at end of file diff --git a/examples/color-your-leds/assets/fonts/Open Sans/OFL.txt b/examples/color-your-leds/assets/fonts/Open Sans/OFL.txt new file mode 100644 index 0000000..d2a4922 --- /dev/null +++ b/examples/color-your-leds/assets/fonts/Open Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/examples/color-your-leds/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf b/examples/color-your-leds/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..548c15f Binary files /dev/null and b/examples/color-your-leds/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf differ diff --git a/examples/color-your-leds/assets/fonts/Roboto/OFL.txt b/examples/color-your-leds/assets/fonts/Roboto/OFL.txt new file mode 100644 index 0000000..5d6f71c --- /dev/null +++ b/examples/color-your-leds/assets/fonts/Roboto/OFL.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/color-your-leds/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf b/examples/color-your-leds/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf new file mode 100644 index 0000000..3a2d704 Binary files /dev/null and b/examples/color-your-leds/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf differ diff --git a/examples/color-your-leds/assets/fonts/fonts.css b/examples/color-your-leds/assets/fonts/fonts.css new file mode 100644 index 0000000..86cf716 --- /dev/null +++ b/examples/color-your-leds/assets/fonts/fonts.css @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-display: swap; + src: url('Roboto/RobotoMono-VariableFont_wght.ttf') format('truetype'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + src: url('Open Sans/OpenSans-VariableFont_wdth,wght.ttf') format('truetype'); +} \ No newline at end of file diff --git a/examples/color-your-leds/assets/img/logo.svg b/examples/color-your-leds/assets/img/logo.svg new file mode 100644 index 0000000..d23ae68 --- /dev/null +++ b/examples/color-your-leds/assets/img/logo.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/color-your-leds/assets/img/logo.svg.license b/examples/color-your-leds/assets/img/logo.svg.license new file mode 100644 index 0000000..c274485 --- /dev/null +++ b/examples/color-your-leds/assets/img/logo.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + +SPDX-License-Identifier: MPL-2.0 diff --git a/examples/color-your-leds/assets/index.html b/examples/color-your-leds/assets/index.html new file mode 100644 index 0000000..d29a5eb --- /dev/null +++ b/examples/color-your-leds/assets/index.html @@ -0,0 +1,53 @@ + + + + + + + + Color your LEDs + + + + +
+
+

Color your LEDs

+ +
+
+
+
+

+
+
+ + + #000000 +
+
+ + + #000000 +
+
+ + + #000000 +
+
+ + + #000000 +
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/examples/color-your-leds/assets/libs/socket.io.min.js b/examples/color-your-leds/assets/libs/socket.io.min.js new file mode 100644 index 0000000..530b185 --- /dev/null +++ b/examples/color-your-leds/assets/libs/socket.io.min.js @@ -0,0 +1,6 @@ +/*! + * Socket.IO v4.8.1 + * (c) 2014-2024 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r1))return this.writeBuffer;for(var t,n=1,i=0;i=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i=0&&t.num1?e-1:0),s=1;s1?i-1:0),e=1;ei.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t})); \ No newline at end of file diff --git a/examples/color-your-leds/assets/style.css b/examples/color-your-leds/assets/style.css new file mode 100644 index 0000000..04c744f --- /dev/null +++ b/examples/color-your-leds/assets/style.css @@ -0,0 +1,138 @@ +/* Basic shell */ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #DAE3E3; + color: #333; + display: flex; + flex-direction: column; + align-items: center; +} + +.main-content { + display: flex; + gap: 32px; + align-items: flex-start; + justify-content: center; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding: 12px 0; +} + +.arduino-text { + color: #008184; + font-family: "Roboto Mono", monospace; + font-size: 20px; + font-weight: 700; + margin: 0; + font-style: normal; + line-height: 170%; + letter-spacing: 2.4px; +} + +.arduino-logo { + height: 32px; + width: auto; +} + +.container { + height: 75%; + border-radius: 16px; + background: #FFF; + padding: 24px; +} + +h1, h2 { + color: #2c3e50; + text-align: center; +} + +#boardContainer { + display: flex; + justify-content: center; + align-items: center; + background-color: #ffffff; + margin-bottom: 20px; + width: 100%; + max-width: 100%; + margin-left: auto; + margin-right: auto; +} + +/* Basic shell … (unchanged) */ + +.stage { display:grid; place-items:center; padding:16px; } + +.board { + position: relative; + width: min(92vw, 920px); + margin: 0 auto; +} +.board img { + position: relative; + display:block; width:100%; height:auto; + user-select:none; -webkit-user-drag:none; + z-index: 1; +} + +/* Wrapper equals the CONTROL slot (47×12), scales with image */ +.toggle-wrap{ + position:absolute; /* we set top/left inline */ + width:calc(47px * var(--img-scale,1)); + height:calc(12px * var(--img-scale,1)); + display:grid; place-items:center; + cursor:pointer; -webkit-tap-highlight-color:transparent; + transform: translate(-50%,-50%); /* center on the coords */ + z-index: 2; +} + +/* Native-hidden checkbox */ +.switch-input{ + position:absolute; inline-size:1px; block-size:1px; + margin:-1px; padding:0; border:0; clip:rect(0 0 0 0); overflow:hidden; +} + +/* iOS/Android-style visual */ +.switch-ios{ + position:relative; + width:calc(22px * var(--img-scale,1)); + height:calc(12px * var(--img-scale,1)); + background:#c9d4d4; border-radius:999px; + box-shadow:inset 0 0 0 calc(1px * var(--img-scale,1)) #0000000f; + transition:background .18s ease, box-shadow .18s ease; +} +.switch-ios::after{ + content:""; + position:absolute; + width:calc(10px * var(--img-scale,1)); height:calc(10px * var(--img-scale,1)); + top:calc(1px * var(--img-scale,1)); left:calc(1px * var(--img-scale,1)); + background:#fff; border-radius:50%; box-shadow:0 1px 2px #0003; + transition:transform .18s ease; +} +.switch-input:checked + .switch-ios{ background:#008184; } +.switch-input:checked + .switch-ios::after{ transform:translateX(calc(10px * var(--img-scale,1))); } +.toggle-wrap:has(.switch-input:focus-visible) .switch-ios{ + outline:calc(2px * var(--img-scale,1)) solid #34C75955; + outline-offset:calc(2px * var(--img-scale,1)); +} +.switch-ios:hover{ box-shadow:inset 0 0 0 calc(1px * var(--img-scale,1)) #0000001a; } + +/* + * Responsive design + */ +@media (max-width: 768px) { + .arduino-text { + font-size: 14px; + } + + .arduino-logo { + height: 20px; + width: auto; + } +} \ No newline at end of file diff --git a/examples/color-your-leds/python/main.py b/examples/color-your-leds/python/main.py new file mode 100644 index 0000000..02d208e --- /dev/null +++ b/examples/color-your-leds/python/main.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +# +# SPDX-License-Identifier: MPL-2.0 +from arduino.app_utils import * +from arduino.app_bricks.web_ui import WebUI + +ui = WebUI() + +def on_set_color(id, message: dict): + ledid = message.get("led") + rgb_color = message.get("color") + # Led 1 and 2 are controlled by directly (MPU), while Led 3 and 4 are controlled via Bridge (MCU) + try: + if ledid not in (1, 2, 3, 4): + raise ValueError(f"Unknown led '{ledid}'") + + if not rgb_color or not all(k in rgb_color for k in ("r", "g", "b")): + raise ValueError("Color must be an object with 'r', 'g', 'b' keys") + + match ledid: + case 1: + Leds.set_led1_color(rgb_color["r"] != 0, rgb_color["g"] != 0, rgb_color["b"] != 0) + case 2: + Leds.set_led2_color(rgb_color["r"] != 0, rgb_color["g"] != 0, rgb_color["b"] != 0) + case 3: + Bridge.call("set_led3_color", rgb_color["r"], rgb_color["g"], rgb_color["b"]) + case 4: + Bridge.call("set_led4_color", rgb_color["r"] != 0, rgb_color["g"] != 0, rgb_color["b"] != 0) + + except Exception as e: + ui.send_message("error", f"LED color set error: {e}") + +#Initialize LEDs to off state (only 1 and 2 here, 3 and 4 will be set in MCU setup) +on_set_color(1, {"led": 1, "color": {"r": 0, "g": 0, "b": 0}}) +on_set_color(2, {"led": 2, "color": {"r": 0, "g": 0, "b": 0}}) + +ui.on_message("set_color", on_set_color) + +App.run() diff --git a/examples/color-your-leds/sketch/sketch.ino b/examples/color-your-leds/sketch/sketch.ino new file mode 100644 index 0000000..8c2e32b --- /dev/null +++ b/examples/color-your-leds/sketch/sketch.ino @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +// +// SPDX-License-Identifier: MPL-2.0 + +#include + +// Led 3 can be controlled via PWM pins +void set_led3_color(int r, int g, int b) { + analogWrite(LED3_R, r); + analogWrite(LED3_G, g); + analogWrite(LED3_B, b); +} + +// Led 4 is a simple ON/OFF LED for each color channel, HIGH = OFF, LOW = ON +void set_led4_color(bool r, bool g, bool b) { + digitalWrite(LED_BUILTIN + 3, r ? LOW : HIGH); + digitalWrite(LED_BUILTIN + 4, g ? LOW : HIGH); + digitalWrite(LED_BUILTIN + 5, b ? LOW : HIGH); +} + +void setup() +{ + pinMode(LED_BUILTIN + 3, OUTPUT); + pinMode(LED_BUILTIN + 4, OUTPUT); + pinMode(LED_BUILTIN + 5, OUTPUT); + + set_led3_color(0, 0, 0); + set_led4_color(false, false, false); + + Bridge.begin(); + + Bridge.provide("set_led3_color", set_led3_color); + Bridge.provide("set_led4_color", set_led4_color); +} + +void loop() {} diff --git a/examples/color-your-leds/sketch/sketch.yaml b/examples/color-your-leds/sketch/sketch.yaml new file mode 100644 index 0000000..d9fe917 --- /dev/null +++ b/examples/color-your-leds/sketch/sketch.yaml @@ -0,0 +1,11 @@ +profiles: + default: + fqbn: arduino:zephyr:unoq + platforms: + - platform: arduino:zephyr + libraries: + - MsgPack (0.4.2) + - DebugLog (0.8.4) + - ArxContainer (0.7.0) + - ArxTypeTraits (0.3.1) +default_profile: default diff --git a/examples/mascot-jump-game/README.md b/examples/mascot-jump-game/README.md new file mode 100644 index 0000000..1599378 --- /dev/null +++ b/examples/mascot-jump-game/README.md @@ -0,0 +1,431 @@ +# Mascot Jump Game + +An endless runner game inspired by the classic browser dinosaur game, where you control an LED character jumping over electronic components. Features progressively increasing difficulty, score tracking, one-button gameplay, and synchronized LED matrix animations on the UNO Q. + +![Mascot Jump Game Example](assets/docs_assets/thumbnail.png) + +## Description + +The App uses the `web_ui` Brick to create a browser-based game with real-time communication between the UNO Q and a web interface. The backend manages game physics, collision detection, and scoring at 60 FPS, while the frontend renders the LED character using PNG images for different animations. + +![Mascot Jump Game - LED Character](assets/docs_assets/led_character_animation.png) + +Key features include: + +- LED character with six animation states (4 running patterns, jump, game over) +- Electronic component obstacles: resistors, transistors, and microchips +- Synchronized LED matrix display mirroring game state +- Progressive difficulty scaling with score +- Keyboard and mouse control +- Session high score tracking + +## Bricks Used + +The mascot jump game example uses the following Bricks: + +- `web_ui`: Brick to create a web interface with real-time communication between the browser and Arduino board with game state updates, input handling, and rendering synchronization. + +## Hardware and Software Requirements + +### Hardware + +- Arduino UNO Q (x1) +- USB-C® cable (for power and programming) (x1) + +### Software + +- Arduino App Lab + +**Note:** You can also run this example using your Arduino UNO Q as a Single Board Computer (SBC) using a [USB-C hub](https://store.arduino.cc/products/usb-c-to-hdmi-multiport-adapter-with-ethernet-and-usb-hub) with a mouse, keyboard and display attached. + +## How to Use the Example + +1. **Run the App** + +![Arduino App Lab - Run App](assets/docs_assets/launch-app.png) + +2. **Access the Web Interface** + +The App should open automatically in the web browser. You can also open it manually via `.local:7000`. The `WebUI` brick establishes a WebSocket connection for real-time communication between browser and UNO Q. + +3. **Wait for Game Initialization** + +The game loads and displays the LED character in idle state. The `GameState` class initializes with default parameters, while the Arduino sketch begins polling game state through `Bridge.call("get_led_state").result(gameState)`. + +4. **Start Playing** + +Press **SPACE** or **UP ARROW** to jump over obstacles. The keypress triggers a `player_action` WebSocket message to the backend, which validates and applies the jump physics. Use **R** to restart after game over. + +![Gameplay Example](assets/docs_assets/game_play_state.gif) + +5. **Avoid Obstacles** + +Jump over three types of electronic components: *resistors* (small), *transistors* (medium), and *microchips* (large). The backend's `spawn_obstacle()` creates new obstacles at random intervals, while the game loop moves them across the screen. Your score increases continuously based on survival time. + +6. **Game Over** + +When you hit an obstacle, `check_collisions()` detects the hit and triggers game over. Your final score and session high score are displayed. The LED character shows a fallen animation. Press **SPACE** to call `game.reset()` and restart. + +![Game Over Screen](assets/docs_assets/game_over_state.gif) + +7. **LED Matrix Synchronization** + +The LED matrix on your UNO Q mirrors the game state. The Arduino sketch calls `Bridge.call("get_led_state").result(gameState)` every 50 ms to get the current state (*running*, *jumping*, *game_over*, or *idle*), then displays the matching LED frame from `game_frames.h`. For more information about the LED matrix, see the [LED Matrix setion from the UNO Q user manual](https://docs.arduino.cc/tutorials/uno-q/user-manual/#led-matrix). + +![LED Matrix Frames](assets/docs_assets/led_matrix_frames.png) + +8. **Progressive Difficulty** + +The game speed increases as your score grows using `BASE_SPEED + (score / 1500.0)`. The `game_loop()` runs at 60 FPS, updating physics, moving obstacles, checking collisions, and broadcasting state to all connected clients. + +## How it Works + +Once the App is running, it performs the following operations: + +- **Managing game state and physics calculations on the backend.** + +The backend maintains the complete game state and physics engine: + +```python +from arduino.app_utils import * +from arduino.app_bricks.web_ui import WebUI +import time +import random +import threading +import json +... +class GameState: + def __init__(self): + self.reset() + self.high_score = 0 + + def reset(self): + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True + self.obstacles = [] + self.score = 0 + self.game_over = False + self.speed = BASE_SPEED + self.last_spawn_time = time.time() + self.next_spawn_delay = random.uniform(SPAWN_MIN_MS/1000, SPAWN_MAX_MS/1000) + + def update_physics(self, dt): + if not self.on_ground: + self.velocity_y += GRAVITY * dt * 60 # Scale for 60 FPS base + self.mascot_y += self.velocity_y * dt * 60 + + # Ground collision + if self.mascot_y >= GROUND_Y - MASCOT_HEIGHT: + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True +... +game = GameState() +``` + +The physics engine calculates gravity effects, jump trajectories, and collision boundaries at a fixed timestep for consistent gameplay. + +- **Providing LED matrix state through Bridge communication.** + +The LED Matrix on the UNO Q displays the game state in real-time with a simplified mascot design: + +```python +def get_led_state(): + global game_started + + if game.game_over: + return "game_over" + elif not game_started and game.score == 0: + return "idle" + elif not game.on_ground: + return "jumping" + else: + return "running" + +... +# Provide function to Arduino sketch +Bridge.provide("get_led_state", get_led_state) +``` + +The LED matrix shows different animations: + +- **Running State:** 4-frame animation cycling through leg positions +- **Jumping State:** Mascot in mid-air with arms spread +- **Idle State:** Standing mascot waiting to start +- **Game Over State:** Fallen mascot rotated 45 degrees with extended arms + +The Arduino sketch processes these states using the `Arduino_LED_Matrix` library: + +```cpp +#include +#include +#include "game_frames.h" + +Arduino_LED_Matrix matrix; + +int animationFrame = 0; +unsigned long lastFrameTime = 0; +const unsigned long ANIMATION_DELAY = 200; + +void setup() { + matrix.begin(); + matrix.setGrayscaleBits(3); // 3-bit grayscale (0-7 brightness levels) + Bridge.begin(); +} + +void loop() { + String gameState; + bool ok = Bridge.call("get_led_state").result(gameState); + + if (ok) { + if (gameState == "running") { + // Animate between four running frames + unsigned long currentTime = millis(); + if (currentTime - lastFrameTime > ANIMATION_DELAY) { + animationFrame = (animationFrame + 1) % 4; + lastFrameTime = currentTime; + } + + switch(animationFrame) { + case 0: matrix.draw(running_frame1); break; + case 1: matrix.draw(running_frame2); break; + case 2: matrix.draw(running_frame3); break; + case 3: matrix.draw(running_frame4); break; + } + } else if (gameState == "jumping") { + matrix.draw(jumping); + animationFrame = 0; + } else if (gameState == "game_over") { + matrix.draw(game_over); + animationFrame = 0; + } else if (gameState == "idle") { + matrix.draw(idle); + animationFrame = 0; + } else { + matrix.draw(idle); + } + } else { + matrix.draw(idle); + } + + delay(50); // Update at ~20 FPS +} +``` + +- **Processing user input through WebSocket events.** + +Input handling uses event-based communication: + +```python +def on_player_action(client_id, data): + global game_started + action = data.get('action') + + if action == 'jump': + game_started = True + if game.jump(): + ui.send_message('jump_confirmed', {'success': True}) + elif action == 'restart': + game.reset() + game_started = True # Game restarts + ui.send_message('game_reset', {'state': game.to_dict()}) + +ui = WebUI() +... +ui.on_message('player_action', on_player_action) +``` + +The backend validates inputs to prevent invalid actions, such as jumping while airborne or during the game-over state. + +- **Running the main game loop with fixed timestep updates.** + +The game loop runs at 60 FPS intervals: + +```python +def game_loop(): + global game_running, game_started + last_update = time.time() + + while game_running: + current_time = time.time() + dt = current_time - last_update + + if not game.game_over: + game.update_physics(dt) + game.update_obstacles(dt) + game.check_collisions() + game.score += int(60 * dt) + game.speed = BASE_SPEED + (game.score / 1500.0) + + ui.send_message('game_update', game.to_dict()) + + last_update = current_time + sleep_time = max(0, (1/FPS) - (time.time() - current_time)) + time.sleep(sleep_time) +``` + +- **Handling obstacle generation and collision detection.** + +The system manages three types of electronic component obstacles: + +```python +OBSTACLE_TYPES = [ + {'name': 'resistor', 'height': 28},    # Small + {'name': 'transistor', 'height': 38},  # Medium + {'name': 'microchip', 'height': 48}    # Large +] + +def spawn_obstacle(self): + obstacle_type = random.choice(OBSTACLE_TYPES) + height = obstacle_type['height'] + + obstacle = { + 'x': GAME_WIDTH + 30, + 'y': GROUND_Y - height, + 'width': OBSTACLE_WIDTH, + 'height': height, + 'type': obstacle_type['name'] + } + self.obstacles.append(obstacle) +``` + +- **Synchronizing game state with frontend rendering.** + +The frontend maintains rendering with PNG images for the LED character: + +```javascript +function loadLEDImages() { +   const imagesToLoad = [ + { key: 'move1', src: 'img/ledcharacter_move1.png' }, + { key: 'move2', src: 'img/ledcharacter_move2.png' }, + { key: 'move3', src: 'img/ledcharacter_move3.png' }, + { key: 'move4', src: 'img/ledcharacter_move4.png' }, + { key: 'jump', src: 'img/ledcharacter_jump.png' }, + { key: 'gameover', src: 'img/ledcharacter_gameover.png' } + ]; + ... +} + +// Cycle through movement patterns on each jump +socket.on('jump_confirmed', (data) => { +   if (data.success) { +       currentMovePattern = (currentMovePattern % 4) + 1; + } +}); + +function drawMascot() { + if (!gameConfig || !gameState || !imagesLoaded) return; + + const x = gameConfig.mascot_x; + const y = Math.round(gameState.mascot_y); + + let imageToUse = null; + + // Select appropriate image based on game state + if (gameState.game_over) { + imageToUse = ledImages.gameover; + } else if (!gameState.on_ground) { + imageToUse = ledImages.jump; + } else { + // Use current movement pattern + switch(currentMovePattern) { + case 1: imageToUse = ledImages.move1; break; + case 2: imageToUse = ledImages.move2; break; + case 3: imageToUse = ledImages.move3; break; + case 4: imageToUse = ledImages.move4; break; + default: imageToUse = ledImages.move1; + } + } + + ... +} +``` + +The high-level data flow looks like this: + +1. **User Input**: Player presses SPACE/UP or clicks to jump +2. **WebSocket**: Input is sent to backend +3. **Backend Processing**: Validates action and updates game state +4. **Game Loop (60 FPS)**: +- Physics update (such as gravity, velocity, and position) +- Collision detection +- State broadcast to clients +5. **Parallel Rendering**: +- Frontend: Canvas draws mascot and obstacles +- LED matrix update: UNO Q displays synchronized LED animations based on game state +6. **Visual Feedback**: Updated display on browser and LED matrix + +## Understanding the Code + +Here is a brief explanation of the App components: + +### 🔧 Backend (`main.py`) + +The Python® component manages all game logic and state. + +- **Game state management**: Tracks the LED character's position, velocity, obstacle locations, score, and game status +- **Physics engine**: Simulates gravity and jump mechanics with frame-independent movement at 60 FPS +- **Obstacle system**: Randomly spawns three types of electronic components (resistors, transistors, microchips) at intervals between 900-1500 ms, moves them across the screen, and removes them when off-screen +- **Collision detection**: Checks if the LED character intersects with any obstacles each frame and triggers game over on collision +- **Bridge communication**: Provides game state to the Arduino LED matrix through the `get_led_state` function +- **Game loop**: Updates physics, obstacles, and score 60 times per second, then broadcasts the game state to the web interface + +### 🔧 Frontend (`app.js` + `index.html`) + +The web interface renders the game using HTML5 Canvas and PNG images. + +- **Canvas rendering**: Displays the LED character using 6 PNG sprites, cycles through 4 running patterns with each jump, and renders electronic component obstacles at 60 FPS +- **Input handling**: Captures keyboard controls (**SPACE/UP** to jump, **R** to restart) and sends actions to the backend via WebSocket +- **Obstacle rendering**: Draws resistors with color bands (red, yellow, green), transistors with *TO-92* package and three pins, and microchips labeled IC555 +- **WebSocket communication**: Connects to the backend on page load, sends player actions, and receives real-time game state updates +- **Score display**: Shows current score and session high score with zero-padded formatting, updating in real-time + +### 🔧 Arduino Component (`sketch.ino` + `game_frames.h`) + +The Arduino sketch displays synchronized LED matrix animations. + +- **Bridge integration**: Retrieves the current game state from the Python® backend via Bridge communication +- **Animation system**: Plays different LED patterns based on game state (running, jumping, game over, or idle) +- **LED patterns**: Each frame is an 8x13 matrix (104 values) stored in `game_frames.h`: + +```cpp +// Example: Running frame 1 +uint8_t running_frame1[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, // Row 0: Head + 0,0,0,7,7,7,7,0,0,0,0,0,0, // Row 1: Body + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, // Row 5: Body/legs + 0,0,7,0,0,0,0,7,0,0,0,0,0, // Row 6: Legs animated + 7,7,7,7,7,7,7,7,7,7,7,7,7 // Row 7: Ground line +}; +``` + +### 👾 Customizing LED Matrix Frames + +The LED matrix frames can be easily customized in `game_frames.h`. Each frame is 8 rows × 13 columns (104 values): + +- **Brightness values**: 0 (off), 1-3 (dim), 4-5 (medium), 6-7 (bright) +- **Row 7**: Always the ground line (all 7s) +- **Animation**: Only row 6 changes between running frames (leg positions) + +To create custom frames: + +1. Design your pattern on an 8×13 grid +2. Use values 0-7 for different brightness levels +3. Replace the array values in `game_frames.h` +4. Upload the sketch to see your custom mascot + +### 🕹️ Game Configuration + +Key constants that define the gameplay, found in `main.py` and can be modified: + +- **Physics**: Gravity (0.65), jump velocity (-12.5), ground position (240px) +- **Canvas**: 800x300px with LED character size of 44x48px +- **Obstacles**: Resistor (28px), Transistor (38px), Microchip (48px), width (18px) +- **Timing**: Base speed (6.0), spawn intervals (900-1500 ms), target 60 FPS +- **Difficulty**: Speed increases with score (score/1500 rate) + +You can adjust these values at the top of `main.py` to customize gameplay difficulty, physics, and visual layout. LED matrix frames can be customized in `game_frames.h` by modifying the 8x13 arrays. \ No newline at end of file diff --git a/examples/mascot-jump-game/app.yaml b/examples/mascot-jump-game/app.yaml new file mode 100644 index 0000000..da022dd --- /dev/null +++ b/examples/mascot-jump-game/app.yaml @@ -0,0 +1,6 @@ +name: Mascot Jump Game +icon: 🏃 +description: An endless runner game where you jump over electronic components with the LED character + +bricks: + - arduino:web_ui diff --git a/examples/mascot-jump-game/assets/app.js b/examples/mascot-jump-game/assets/app.js new file mode 100644 index 0000000..d55668a --- /dev/null +++ b/examples/mascot-jump-game/assets/app.js @@ -0,0 +1,507 @@ +// SPDX-FileCopyrightText: Copyright (C) 2025 +// SPDX-License-Identifier: MPL-2.0 + +// Game configuration received from backend +let gameConfig = null; +let gameState = null; +let socket = null; + +// Canvas setup +let canvas = null; +let ctx = null; + +// Animation state +let currentMovePattern = 1; // Track movement pattern (1-4) +let blinkState = true; +let lastBlinkTime = Date.now(); +let animationId = null; + +// LED Character Images +let ledImages = { + move1: null, + move2: null, + move3: null, + move4: null, + jump: null, + gameover: null +}; + +// Track which images are loaded +let imagesLoaded = false; + +// Colors +const BG_COLOR = '#f5f5f5'; +const FG_COLOR = '#282828'; +const ACCENT_COLOR = '#3c3c3c'; + +document.addEventListener('DOMContentLoaded', () => { + loadLEDImages(); + initCanvas(); + initSocketIO(); + initInputHandlers(); + startGameLoop(); +}); + +function loadLEDImages() { + const imagesToLoad = [ + { key: 'move1', src: 'img/ledcharacter_move1.png' }, + { key: 'move2', src: 'img/ledcharacter_move2.png' }, + { key: 'move3', src: 'img/ledcharacter_move3.png' }, + { key: 'move4', src: 'img/ledcharacter_move4.png' }, + { key: 'jump', src: 'img/ledcharacter_jump.png' }, + { key: 'gameover', src: 'img/ledcharacter_gameover.png' } + ]; + + let loadedCount = 0; + + imagesToLoad.forEach(({ key, src }) => { + const img = new Image(); + img.onload = () => { + ledImages[key] = img; + loadedCount++; + if (loadedCount === imagesToLoad.length) { + imagesLoaded = true; + console.log('All LED character images loaded from img/ folder'); + } + }; + img.onerror = () => { + console.error(`Failed to load image: ${src}`); + // Try loading from root directory as fallback + const filename = src.split('/').pop(); + console.log(`Trying fallback path: ${filename}`); + img.src = filename; + }; + img.src = src; + }); +} + +function initCanvas() { + canvas = document.getElementById('gameCanvas'); + ctx = canvas.getContext('2d'); + + // Set canvas properties for pixels + ctx.imageSmoothingEnabled = false; + + // Handle window resize + window.addEventListener('resize', handleResize); + handleResize(); +} + +function handleResize() { + // Scale canvas to fit window while maintaining aspect ratio + const maxWidth = window.innerWidth - 40; + const maxHeight = window.innerHeight - 150; + const scale = Math.min(maxWidth / 800, maxHeight / 300, 1); + + if (scale < 1) { + canvas.style.width = `${800 * scale}px`; + canvas.style.height = `${300 * scale}px`; + } +} + +function initSocketIO() { + socket = io(`http://${window.location.host}`); + + socket.on('connect', () => { + console.log('Connected to game server'); + updateConnectionStatus(true); + socket.emit('client_connected', {}); + }); + + socket.on('disconnect', () => { + console.log('Disconnected from game server'); + updateConnectionStatus(false); + }); + + socket.on('game_init', (data) => { + console.log('Received game initialization:', data); + gameConfig = data.config; + gameState = data.state; + updateScoreDisplay(); + }); + + socket.on('game_update', (data) => { + gameState = data; + updateScoreDisplay(); + }); + + socket.on('game_reset', (data) => { + console.log('Game reset'); + gameState = data.state; + updateScoreDisplay(); + // Reset animation states + currentMovePattern = 1; + blinkState = true; + }); + + socket.on('jump_confirmed', (data) => { + if (data.success) { + console.log('⬆Jump confirmed'); + // Cycle to next movement pattern (1->2->3->4->1) + currentMovePattern = (currentMovePattern % 4) + 1; + } + }); + + socket.on('error', (error) => { + console.error('Socket error:', error); + showError('Connection error: ' + error); + }); +} + +function initInputHandlers() { + // Keyboard controls + document.addEventListener('keydown', handleKeyPress); + + // Touch/click controls for mobile + canvas.addEventListener('click', handleCanvasClick); + canvas.addEventListener('touchstart', handleCanvasTouch); + + // Prevent default touch behaviors + canvas.addEventListener('touchmove', (e) => e.preventDefault()); + canvas.addEventListener('touchend', (e) => e.preventDefault()); +} + +function handleKeyPress(e) { + switch(e.code) { + case 'Space': + case 'ArrowUp': + e.preventDefault(); + performAction(); + break; + case 'KeyR': + e.preventDefault(); + restartGame(); + break; + } +} + +function handleCanvasClick(e) { + e.preventDefault(); + performAction(); +} + +function handleCanvasTouch(e) { + e.preventDefault(); + performAction(); +} + +function performAction() { + if (!gameState) return; + + if (gameState.game_over) { + restartGame(); + } else { + jump(); + } +} + +function jump() { + if (socket && socket.connected) { + socket.emit('player_action', { action: 'jump' }); + } +} + +function restartGame() { + if (socket && socket.connected) { + socket.emit('player_action', { action: 'restart' }); + } +} + +function updateConnectionStatus(connected) { + const statusElement = document.getElementById('connectionStatus'); + if (statusElement) { + statusElement.className = `connection-status ${connected ? 'connected' : 'disconnected'}`; + statusElement.textContent = connected ? 'Connected' : 'Disconnected'; + } +} + +function updateScoreDisplay() { + if (!gameState) return; + + const scoreElement = document.getElementById('score'); + const highScoreElement = document.getElementById('highScore'); + + if (scoreElement) { + scoreElement.textContent = String(Math.floor(gameState.score)).padStart(5, '0'); + } + + if (highScoreElement) { + highScoreElement.textContent = String(Math.floor(gameState.high_score)).padStart(5, '0'); + } +} + +function showError(message) { + console.error(message); + + const errorContainer = document.getElementById('errorContainer'); + if (errorContainer) { + errorContainer.textContent = message; + errorContainer.style.display = 'block'; + setTimeout(() => { + errorContainer.style.display = 'none'; + }, 5000); + } +} + +// Drawing functions +function clearCanvas() { + ctx.fillStyle = BG_COLOR; + ctx.fillRect(0, 0, canvas.width, canvas.height); +} + +function drawGround() { + if (!gameConfig) return; + + // Ground line + ctx.strokeStyle = ACCENT_COLOR; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(0, gameConfig.ground_y + 1); + ctx.lineTo(canvas.width, gameConfig.ground_y + 1); + ctx.stroke(); + + // Ground texture dots + ctx.fillStyle = ACCENT_COLOR; + for (let x = 0; x < canvas.width; x += 14) { + ctx.fillRect(x, gameConfig.ground_y + 3, 1, 1); + } +} + +function drawMascot() { + if (!gameConfig || !gameState || !imagesLoaded) return; + + const x = gameConfig.mascot_x; + const y = Math.round(gameState.mascot_y); + + let imageToUse = null; + + // Select appropriate image based on game state + if (gameState.game_over) { + imageToUse = ledImages.gameover; + } else if (!gameState.on_ground) { + imageToUse = ledImages.jump; + } else { + // Use current movement pattern + switch(currentMovePattern) { + case 1: + imageToUse = ledImages.move1; + break; + case 2: + imageToUse = ledImages.move2; + break; + case 3: + imageToUse = ledImages.move3; + break; + case 4: + imageToUse = ledImages.move4; + break; + default: + imageToUse = ledImages.move1; + } + } + + // Draw the LED character image if available + if (imageToUse) { + // Draw image at original size or scale if needed + // Assuming the PNGs are sized appropriately for the mascot + ctx.drawImage(imageToUse, x, y, gameConfig.mascot_width, gameConfig.mascot_height); + } else { + // Fallback: draw a simple rectangle if image not loaded + ctx.fillStyle = FG_COLOR; + ctx.fillRect(x, y, gameConfig.mascot_width, gameConfig.mascot_height); + + // Simple face + ctx.fillStyle = BG_COLOR; + ctx.fillRect(x + 10, y + 10, 5, 5); + ctx.fillRect(x + 29, y + 10, 5, 5); + } +} + +function drawObstacles() { + if (!gameState || !gameState.obstacles) return; + + for (const obstacle of gameState.obstacles) { + const x = Math.round(obstacle.x); + const y = Math.round(obstacle.y); + const h = obstacle.height; + + // Determine obstacle type based on height + if (h <= 32) { + // Small: Resistor + drawResistor(x, y - 10); + } else if (h <= 42) { + // Medium: Transistor + drawTransistor(x, y - 8); + } else { + // Large: Microchip + drawMicrochip(x, y); + } + } +} + +function drawResistor(x, y) { + ctx.fillStyle = '#8B4513'; // Brown color for resistor body + ctx.fillRect(x, y + 8, 20, 14); + + // Resistor bands + ctx.fillStyle = '#FF0000'; // Red band + ctx.fillRect(x + 3, y + 8, 3, 14); + ctx.fillStyle = '#FFFF00'; // Yellow band + ctx.fillRect(x + 9, y + 8, 3, 14); + ctx.fillStyle = '#00FF00'; // Green band + ctx.fillRect(x + 15, y + 8, 3, 14); + + // Wires + ctx.fillStyle = '#606060'; + ctx.fillRect(x - 3, y + 13, 5, 3); + ctx.fillRect(x + 18, y + 13, 5, 3); + + // Add vertical wires + ctx.fillRect(x - 1, y + 3, 2, 10); + ctx.fillRect(x + 19, y + 3, 2, 10); +} + +function drawTransistor(x, y) { + // Pixel art transistor (medium obstacle) + ctx.fillStyle = FG_COLOR; + + // Main body (TO-92 package style) + ctx.fillRect(x + 2, y + 2, 16, 24); + + // Rounded top + ctx.fillRect(x + 4, y, 12, 3); + ctx.fillRect(x + 6, y - 1, 8, 1); + + // Three legs + ctx.fillStyle = '#606060'; + ctx.fillRect(x + 4, y + 26, 3, 12); + ctx.fillRect(x + 9, y + 26, 3, 12); + ctx.fillRect(x + 14, y + 26, 3, 12); + + // Label + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(x + 5, y + 8, 10, 10); + ctx.fillStyle = FG_COLOR; + ctx.font = '12px monospace'; + ctx.fillText('T', x + 8, y + 16); +} + +function drawMicrochip(x, y) { + // Pixel art microchip/IC (large obstacle) + ctx.fillStyle = FG_COLOR; + + // Main IC body + ctx.fillRect(x + 2, y + 10, 14, 20); + + // Notch at top + ctx.fillStyle = BG_COLOR; + ctx.fillRect(x + 7, y + 10, 4, 3); + + // IC pins + ctx.fillStyle = '#606060'; + for (let i = 0; i < 4; i++) { + // Left side pins + ctx.fillRect(x - 2, y + 14 + i*4, 4, 2); + // Right side pins + ctx.fillRect(x + 14, y + 14 + i*4, 4, 2); + } + + // Label on IC + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(x + 4, y + 16, 10, 8); + ctx.fillStyle = FG_COLOR; + ctx.font = '6px monospace'; + ctx.fillText('IC', x + 6, y + 21); + ctx.fillText('555', x + 5, y + 23); +} + +function drawGameOver() { + if (!gameState || !gameState.game_over) return; + + // Semi-transparent overlay + ctx.fillStyle = 'rgba(245, 245, 245, 0.8)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Game Over text + ctx.fillStyle = FG_COLOR; + ctx.font = 'bold 32px Consolas, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2 - 30); + + // Score display + ctx.font = '20px Consolas, monospace'; + ctx.fillText(`Score: ${Math.floor(gameState.score)}`, canvas.width/2, canvas.height/2); + + // Blinking restart prompt + const currentTime = Date.now(); + if (currentTime - lastBlinkTime > 500) { + blinkState = !blinkState; + lastBlinkTime = currentTime; + } + + if (blinkState) { + ctx.font = '16px Consolas, monospace'; + ctx.fillStyle = ACCENT_COLOR; + ctx.fillText('Press SPACE to restart', canvas.width/2, canvas.height/2 + 35); + } +} + +function drawDebugInfo() { + // Optional: Display debug information + if (!gameState || !gameConfig) return; + + ctx.fillStyle = ACCENT_COLOR; + ctx.font = '10px monospace'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + const debugInfo = [ + `FPS: ${(1000 / 16).toFixed(0)}`, + `Speed: ${gameState.speed.toFixed(1)}`, + `Obstacles: ${gameState.obstacles.length}`, + `Y: ${gameState.mascot_y.toFixed(0)}`, + `Vel: ${gameState.velocity_y.toFixed(1)}`, + `Pattern: ${currentMovePattern}`, + `Images: ${imagesLoaded ? 'Loaded' : 'Loading...'}` + ]; + + debugInfo.forEach((info, i) => { + ctx.fillText(info, 10, 10 + i * 12); + }); +} + +// Main game rendering loop +function render() { + clearCanvas(); + drawGround(); + drawObstacles(); + drawMascot(); + drawGameOver(); + + // Uncomment for debug info + //drawDebugInfo(); +} + +function startGameLoop() { + function loop() { + render(); + animationId = requestAnimationFrame(loop); + } + loop(); +} + +function stopGameLoop() { + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } +} + +// Clean up on page unload +window.addEventListener('beforeunload', () => { + stopGameLoop(); + if (socket) { + socket.disconnect(); + } +}); \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/docs_assets/game_over_state.gif b/examples/mascot-jump-game/assets/docs_assets/game_over_state.gif new file mode 100644 index 0000000..c36b1d5 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/game_over_state.gif differ diff --git a/examples/mascot-jump-game/assets/docs_assets/game_play_state.gif b/examples/mascot-jump-game/assets/docs_assets/game_play_state.gif new file mode 100644 index 0000000..e1e8ac9 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/game_play_state.gif differ diff --git a/examples/mascot-jump-game/assets/docs_assets/launch-app.png b/examples/mascot-jump-game/assets/docs_assets/launch-app.png new file mode 100644 index 0000000..5e18786 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/launch-app.png differ diff --git a/examples/mascot-jump-game/assets/docs_assets/led_character_animation.png b/examples/mascot-jump-game/assets/docs_assets/led_character_animation.png new file mode 100644 index 0000000..5e48f19 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/led_character_animation.png differ diff --git a/examples/mascot-jump-game/assets/docs_assets/led_matrix_frames.png b/examples/mascot-jump-game/assets/docs_assets/led_matrix_frames.png new file mode 100644 index 0000000..5470ca2 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/led_matrix_frames.png differ diff --git a/examples/mascot-jump-game/assets/docs_assets/thumbnail.png b/examples/mascot-jump-game/assets/docs_assets/thumbnail.png new file mode 100644 index 0000000..285edbf Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/thumbnail.png differ diff --git a/examples/mascot-jump-game/assets/fonts/Open Sans/OFL.txt b/examples/mascot-jump-game/assets/fonts/Open Sans/OFL.txt new file mode 100644 index 0000000..a5ec031 --- /dev/null +++ b/examples/mascot-jump-game/assets/fonts/Open Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf b/examples/mascot-jump-game/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..548c15f Binary files /dev/null and b/examples/mascot-jump-game/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf differ diff --git a/examples/mascot-jump-game/assets/fonts/Roboto/OFL.txt b/examples/mascot-jump-game/assets/fonts/Roboto/OFL.txt new file mode 100644 index 0000000..38d9750 --- /dev/null +++ b/examples/mascot-jump-game/assets/fonts/Roboto/OFL.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/mascot-jump-game/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf b/examples/mascot-jump-game/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf new file mode 100644 index 0000000..3a2d704 Binary files /dev/null and b/examples/mascot-jump-game/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf differ diff --git a/examples/mascot-jump-game/assets/fonts/fonts.css b/examples/mascot-jump-game/assets/fonts/fonts.css new file mode 100644 index 0000000..86cf716 --- /dev/null +++ b/examples/mascot-jump-game/assets/fonts/fonts.css @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-display: swap; + src: url('Roboto/RobotoMono-VariableFont_wght.ttf') format('truetype'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + src: url('Open Sans/OpenSans-VariableFont_wdth,wght.ttf') format('truetype'); +} \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg b/examples/mascot-jump-game/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg new file mode 100644 index 0000000..c942003 --- /dev/null +++ b/examples/mascot-jump-game/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/img/favicon.png b/examples/mascot-jump-game/assets/img/favicon.png new file mode 100644 index 0000000..019a8cf Binary files /dev/null and b/examples/mascot-jump-game/assets/img/favicon.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_gameover.png b/examples/mascot-jump-game/assets/img/ledcharacter_gameover.png new file mode 100644 index 0000000..1ee456f Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_gameover.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_jump.png b/examples/mascot-jump-game/assets/img/ledcharacter_jump.png new file mode 100644 index 0000000..499df74 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_jump.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move1.png b/examples/mascot-jump-game/assets/img/ledcharacter_move1.png new file mode 100644 index 0000000..c140792 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move1.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move2.png b/examples/mascot-jump-game/assets/img/ledcharacter_move2.png new file mode 100644 index 0000000..ca4ce96 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move2.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move3.png b/examples/mascot-jump-game/assets/img/ledcharacter_move3.png new file mode 100644 index 0000000..1187ff4 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move3.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move4.png b/examples/mascot-jump-game/assets/img/ledcharacter_move4.png new file mode 100644 index 0000000..c3dde6e Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move4.png differ diff --git a/examples/mascot-jump-game/assets/img/logo.svg b/examples/mascot-jump-game/assets/img/logo.svg new file mode 100644 index 0000000..d23ae68 --- /dev/null +++ b/examples/mascot-jump-game/assets/img/logo.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/img/logo.svg.license b/examples/mascot-jump-game/assets/img/logo.svg.license new file mode 100644 index 0000000..c274485 --- /dev/null +++ b/examples/mascot-jump-game/assets/img/logo.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + +SPDX-License-Identifier: MPL-2.0 diff --git a/examples/mascot-jump-game/assets/index.html b/examples/mascot-jump-game/assets/index.html new file mode 100644 index 0000000..8e0220e --- /dev/null +++ b/examples/mascot-jump-game/assets/index.html @@ -0,0 +1,54 @@ + + + + + + + + Mascot Jumpe Game + + + + +
Connecting...
+ +
+
+

Mascot Jump Game

+ +
+ +
+ + +
+
+ Score: + 00000 + HI: + 00000 +
+ +
+ + SPACE or Jump + + + + R Restart + +
+
+ + +
+
+ + + + + diff --git a/examples/mascot-jump-game/assets/libs/socket.io.min.js b/examples/mascot-jump-game/assets/libs/socket.io.min.js new file mode 100644 index 0000000..530b185 --- /dev/null +++ b/examples/mascot-jump-game/assets/libs/socket.io.min.js @@ -0,0 +1,6 @@ +/*! + * Socket.IO v4.8.1 + * (c) 2014-2024 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r1))return this.writeBuffer;for(var t,n=1,i=0;i=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i=0&&t.num1?e-1:0),s=1;s1?i-1:0),e=1;ei.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t})); \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/style.css b/examples/mascot-jump-game/assets/style.css new file mode 100644 index 0000000..f2714b9 --- /dev/null +++ b/examples/mascot-jump-game/assets/style.css @@ -0,0 +1,352 @@ +/* +SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + +SPDX-License-Identifier: MPL-2.0 +*/ + +@import url('fonts/fonts.css'); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Consolas', 'Courier New', monospace; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + user-select: none; + -webkit-user-select: none; +} + +.container { + width: 100%; + max-width: 900px; + padding: 20px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 0 10px; +} + +.arduino-text { + color: #008184; + font-family: "Roboto Mono", monospace; + font-size: 20px; + font-weight: 700; + margin: 0; + font-style: normal; + line-height: 170%; + letter-spacing: 2.4px; +} + +.arduino-logo { + height: 24px; + width: auto; +} + +/* Game container */ +.game-container { + background: white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); + position: relative; +} + +/* Canvas */ +#gameCanvas { + display: block; + margin: 0 auto; + border: 2px solid #282828; + background: #f5f5f5; + border-radius: 8px; + cursor: pointer; + image-rendering: pixelated; + image-rendering: crisp-edges; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-crisp-edges; + transition: transform 0.2s ease; +} + +#gameCanvas:hover { + transform: scale(1.005); +} + +#gameCanvas:active { + transform: scale(0.995); +} + +/* Game info section */ +.game-info { + margin-top: 20px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10px; +} + +/* Score display */ +.score-display { + display: flex; + align-items: baseline; + gap: 15px; + font-size: 24px; + color: #282828; +} + +.score-label, +.high-score-label { + font-weight: 500; + opacity: 0.7; + font-size: 20px; +} + +.score-value, +.high-score-value { + font-weight: 700; + font-variant-numeric: tabular-nums; + letter-spacing: 2px; +} + +.high-score-label { + margin-left: 10px; +} + +.high-score-value { + color: #008184; +} + +/* Controls info */ +.controls-info { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + color: #666; +} + +.control-item { + display: flex; + align-items: center; + gap: 5px; +} + +.key-icon { + display: inline-block; + padding: 3px 8px; + background: #f0f0f0; + border: 1px solid #d0d0d0; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + color: #282828; + box-shadow: 0 2px 0 #d0d0d0; + position: relative; + top: -1px; +} + +.control-divider { + color: #ccc; + font-size: 16px; +} + +/* Connection status */ +.connection-status { + position: fixed; + top: 20px; + right: 20px; + padding: 8px 16px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + transition: all 0.3s ease; + z-index: 1000; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.connection-status.connected { + background: #4caf50; + color: white; + animation: pulse 2s ease-out; +} + +.connection-status.disconnected { + background: #f44336; + color: white; + animation: blink 1s infinite; +} + +/* Error message */ +.error-message { + margin-top: 15px; + padding: 10px 15px; + background: #fff3cd; + border: 1px solid #ffc107; + border-radius: 6px; + color: #856404; + font-size: 14px; + text-align: center; + animation: slideIn 0.3s ease; +} + +/* Animations */ +@keyframes pulse { + 0% { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } + 50% { + box-shadow: 0 2px 20px rgba(76, 175, 80, 0.4); + } + 100% { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } +} + +@keyframes blink { + 0%, 50%, 100% { + opacity: 1; + } + 25%, 75% { + opacity: 0.5; + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .header { + margin-bottom: 15px; + } + + .arduino-text { + font-size: 24px; + } + + .game-container { + padding: 15px; + } + + #gameCanvas { + max-width: 100%; + height: auto; + } + + .game-info { + flex-direction: column; + gap: 15px; + align-items: center; + text-align: center; + } + + .score-display { + font-size: 20px; + } + + .controls-info { + flex-wrap: wrap; + justify-content: center; + } + + .connection-status { + top: 10px; + right: 10px; + padding: 6px 12px; + font-size: 10px; + } +} + +@media (max-width: 480px) { + .arduino-text { + font-size: 20px; + } + + .arduino-logo { + height: 24px; + } + + .score-display { + font-size: 18px; + gap: 10px; + } + + .score-label, + .high-score-label { + font-size: 16px; + } + + .controls-info { + font-size: 12px; + } + + .key-icon { + padding: 2px 6px; + font-size: 10px; + } +} + +@media (hover: none) and (pointer: coarse) { + #gameCanvas { + cursor: default; + } + + #gameCanvas:hover { + transform: none; + } + + #gameCanvas:active { + transform: scale(0.98); + } + + .key-icon { + display: none; + } + + .control-item:first-child::before { + content: "Tap to "; + } + + .control-divider, + .control-item:last-child { + display: none; + } +} + +@media print { + body { + background: white; + } + + .connection-status { + display: none; + } + + .game-container { + box-shadow: none; + border: 1px solid #ddd; + } +} diff --git a/examples/mascot-jump-game/python/main.py b/examples/mascot-jump-game/python/main.py new file mode 100644 index 0000000..ea94914 --- /dev/null +++ b/examples/mascot-jump-game/python/main.py @@ -0,0 +1,244 @@ +# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +# +# SPDX-License-Identifier: MPL-2.0 + +from arduino.app_utils import * +from arduino.app_bricks.web_ui import WebUI +import time +import random +import threading +import json + +# Game Constants +GAME_WIDTH = 800 +GAME_HEIGHT = 300 +GROUND_Y = 240 +FPS = 60 + +MASCOT_WIDTH = 44 +MASCOT_HEIGHT = 48 +MASCOT_X = 80 + +OBSTACLE_WIDTH = 18 +MIN_OBSTACLE_HEIGHT = 28 # Resistor height +MID_OBSTACLE_HEIGHT = 38 # Transistor height +MAX_OBSTACLE_HEIGHT = 48 # Microchip height + +# Obstacle types with their specific heights +OBSTACLE_TYPES = [ + {'name': 'resistor', 'height': 28}, # Small + {'name': 'transistor', 'height': 38}, # Medium + {'name': 'microchip', 'height': 48} # Large +] + +JUMP_VELOCITY = -12.5 +GRAVITY = 0.65 +BASE_SPEED = 6.0 + +SPAWN_MIN_MS = 900 +SPAWN_MAX_MS = 1500 + +class GameState: + """Manages the complete game state""" + def __init__(self): + self.reset() + self.high_score = 0 + + def reset(self): + """Reset game to initial state""" + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True + self.obstacles = [] + self.score = 0 + self.game_over = False + self.speed = BASE_SPEED + self.last_spawn_time = time.time() + self.next_spawn_delay = random.uniform(SPAWN_MIN_MS/1000, SPAWN_MAX_MS/1000) + + def update_physics(self, dt): + """Update mascot physics""" + if not self.on_ground: + self.velocity_y += GRAVITY * dt * 60 # Scale for 60 FPS base + self.mascot_y += self.velocity_y * dt * 60 + + # Ground collision + if self.mascot_y >= GROUND_Y - MASCOT_HEIGHT: + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True + + def update_obstacles(self, dt): + """Update obstacle positions and spawn new ones""" + current_time = time.time() + + # Move existing obstacles + for obstacle in self.obstacles: + obstacle['x'] -= self.speed * dt * 60 + + # Remove offscreen obstacles + self.obstacles = [obs for obs in self.obstacles if obs['x'] > -OBSTACLE_WIDTH - 10] + + # Spawn new obstacles + if current_time - self.last_spawn_time >= self.next_spawn_delay: + self.spawn_obstacle() + self.last_spawn_time = current_time + self.next_spawn_delay = random.uniform(SPAWN_MIN_MS/1000, SPAWN_MAX_MS/1000) + + def spawn_obstacle(self): + """Create a new obstacle""" + # Randomly select an obstacle type + obstacle_type = random.choice(OBSTACLE_TYPES) + height = obstacle_type['height'] + + obstacle = { + 'x': GAME_WIDTH + 30, + 'y': GROUND_Y - height, + 'width': OBSTACLE_WIDTH, + 'height': height, + 'type': obstacle_type['name'] + } + self.obstacles.append(obstacle) + + def check_collisions(self): + """Check for mascot-obstacle collisions""" + mascot_rect = { + 'x': MASCOT_X, + 'y': self.mascot_y, + 'width': MASCOT_WIDTH, + 'height': MASCOT_HEIGHT + } + + for obstacle in self.obstacles: + if self.rectangles_intersect(mascot_rect, obstacle): + self.game_over = True + self.high_score = max(self.high_score, self.score) + return True + return False + + def rectangles_intersect(self, rect1, rect2): + """Check if two rectangles intersect""" + return not (rect1['x'] + rect1['width'] < rect2['x'] or + rect2['x'] + rect2['width'] < rect1['x'] or + rect1['y'] + rect1['height'] < rect2['y'] or + rect2['y'] + rect2['height'] < rect1['y']) + + def jump(self): + """Make the mascot jump if on ground""" + if self.on_ground and not self.game_over: + self.velocity_y = JUMP_VELOCITY + self.on_ground = False + return True + return False + + def to_dict(self): + """Serialize game state for transmission""" + return { + 'mascot_y': self.mascot_y, + 'velocity_y': self.velocity_y, + 'on_ground': self.on_ground, + 'obstacles': self.obstacles, + 'score': self.score, + 'high_score': self.high_score, + 'game_over': self.game_over, + 'speed': self.speed + } + +# Initialize game and UI +game = GameState() +ui = WebUI() + +# Game loop control +game_running = True +game_thread = None +game_started = False # Track if game has started + +def get_led_state(): + """Return current LED state for the LED matrix display""" + global game_started + + if game.game_over: + return "game_over" + elif not game_started and game.score == 0: + return "idle" + elif not game.on_ground: + return "jumping" + else: + return "running" + +def game_loop(): + """Main game loop running at ~60 FPS""" + global game_running, game_started + last_update = time.time() + + while game_running: + current_time = time.time() + dt = current_time - last_update + + if not game.game_over: + # Update game logic + game.update_physics(dt) + game.update_obstacles(dt) + game.check_collisions() + + # Update score (approximately 1 point per frame at 60 FPS) + game.score += int(60 * dt) + + # Increase difficulty + game.speed = BASE_SPEED + (game.score / 1500.0) + + # Send game state to all connected clients + ui.send_message('game_update', game.to_dict()) + + last_update = current_time + + # Target 60 FPS + sleep_time = max(0, (1/FPS) - (time.time() - current_time)) + time.sleep(sleep_time) + +def on_player_action(client_id, data): + """Handle player input actions""" + global game_started + action = data.get('action') + + if action == 'jump': + game_started = True # Game starts on first jump + if game.jump(): + ui.send_message('jump_confirmed', {'success': True}) + elif action == 'restart': + game.reset() + game_started = True # Game restarts + ui.send_message('game_reset', {'state': game.to_dict()}) + +def on_client_connected(client_id, data): + """Send initial game state when client connects""" + ui.send_message('game_init', { + 'state': game.to_dict(), + 'config': { + 'width': GAME_WIDTH, + 'height': GAME_HEIGHT, + 'ground_y': GROUND_Y, + 'mascot_x': MASCOT_X, + 'mascot_width': MASCOT_WIDTH, + 'mascot_height': MASCOT_HEIGHT + } + }) + +# Register WebSocket event handlers +ui.on_message('player_action', on_player_action) +ui.on_message('client_connected', on_client_connected) + +# Provide the LED state function to the Arduino sketch +Bridge.provide("get_led_state", get_led_state) + +# Start game loop in separate thread +game_thread = threading.Thread(target=game_loop, daemon=True) +game_thread.start() + +# Run the app +try: + App.run() +except KeyboardInterrupt: + game_running = False + if game_thread: + game_thread.join() \ No newline at end of file diff --git a/examples/mascot-jump-game/python/requirements.txt b/examples/mascot-jump-game/python/requirements.txt new file mode 100644 index 0000000..5873083 --- /dev/null +++ b/examples/mascot-jump-game/python/requirements.txt @@ -0,0 +1 @@ +pygame==2.6.1 diff --git a/examples/mascot-jump-game/sketch/game_frames.h b/examples/mascot-jump-game/sketch/game_frames.h new file mode 100644 index 0000000..e5f0027 --- /dev/null +++ b/examples/mascot-jump-game/sketch/game_frames.h @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +// LED Matrix frames for Mascot Jump Game +// 8x13 LED matrix patterns (104 values) +// Simplified mascot design with animated legs +// Values: 0 (off), 5 (medium), 7 (brightest) for 3-bit grayscale + +// Running animation frame 1 +uint8_t running_frame1[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,7,0,0,0,0,7,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 // Ground line +}; + +// Running animation frame 2 +uint8_t running_frame2[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Running animation frame 3 +uint8_t running_frame3[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,7,0,0,0,7,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Running animation frame 4 +uint8_t running_frame4[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Jumping frame - mascot in air with arms spread +uint8_t jumping[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,5, + 0,0,0,7,3,3,7,0,5,0,0,0,5, + 0,0,5,7,7,7,7,5,0,0,0,0,5, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,5, + 0,5,7,0,0,5,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Game over frame with X pattern +uint8_t game_over[104] = { + 0,0,7,7,7,0,0,0,0,0,4,0,4, + 0,7,7,3,7,7,0,7,0,0,0,4,0, + 0,7,3,7,7,7,7,0,0,0,4,0,4, + 0,7,7,7,7,7,7,0,0,0,0,0,0, + 0,0,7,7,7,7,7,7,7,0,0,0,0, + 0,7,0,0,7,0,0,0,0,0,0,0,0, + 0,0,0,0,7,0,0,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Idle/waiting frame same as running frame with neutral legs +uint8_t idle[104] = { + 0,0,0,0,7,7,0,0,0,0,4,0,0, + 0,0,0,7,7,7,7,0,0,0,4,4,4, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; \ No newline at end of file diff --git a/examples/mascot-jump-game/sketch/sketch.ino b/examples/mascot-jump-game/sketch/sketch.ino new file mode 100644 index 0000000..7f42f8e --- /dev/null +++ b/examples/mascot-jump-game/sketch/sketch.ino @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +// +// SPDX-License-Identifier: MPL-2.0 + +#include +#include +#include "game_frames.h" + +Arduino_LED_Matrix matrix; + +// Animation state tracking +int animationFrame = 0; +unsigned long lastFrameTime = 0; +const unsigned long ANIMATION_DELAY = 200; // milliseconds between frames + +void setup() { + matrix.begin(); + matrix.setGrayscaleBits(3); // Use 3-bit grayscale (0-7 levels) + Bridge.begin(); +} + +void loop() { + String gameState; + bool ok = Bridge.call("get_led_state").result(gameState); + + if (ok) { + if (gameState == "running") { + // Animate between four running frames for leg movement + unsigned long currentTime = millis(); + if (currentTime - lastFrameTime > ANIMATION_DELAY) { + animationFrame = (animationFrame + 1) % 4; + lastFrameTime = currentTime; + } + + switch(animationFrame) { + case 0: + matrix.draw(running_frame1); + break; + case 1: + matrix.draw(running_frame2); + break; + case 2: + matrix.draw(running_frame3); + break; + case 3: + matrix.draw(running_frame4); + break; + } + + } else if (gameState == "jumping") { + // Show jumping frame when mascot is in the air + matrix.draw(jumping); + animationFrame = 0; // Reset animation frame + + } else if (gameState == "game_over") { + // Show game over pattern + matrix.draw(game_over); + animationFrame = 0; + + } else if (gameState == "idle") { + // Show idle frame when game has not started + matrix.draw(idle); + animationFrame = 0; + + } else { + // Default to idle if state is unknown + matrix.draw(idle); + } + } else { + // If communication fails, show idle + matrix.draw(idle); + } + + delay(50); // Update LED matrix at around 20 FPS +} \ No newline at end of file diff --git a/examples/mascot-jump-game/sketch/sketch.yaml b/examples/mascot-jump-game/sketch/sketch.yaml new file mode 100644 index 0000000..cb644bb --- /dev/null +++ b/examples/mascot-jump-game/sketch/sketch.yaml @@ -0,0 +1,11 @@ +profiles: + default: + fqbn: arduino:zephyr:unoq + platforms: + - platform: arduino:zephyr + libraries: + - MsgPack (0.4.2) + - DebugLog (0.8.4) + - ArxContainer (0.7.0) + - ArxTypeTraits (0.3.1) +default_profile: default \ No newline at end of file diff --git a/examples/object-hunting/app.yaml b/examples/object-hunting/app.yaml new file mode 100644 index 0000000..49566e3 --- /dev/null +++ b/examples/object-hunting/app.yaml @@ -0,0 +1,7 @@ +name: Object Hunting +icon: 🔍 +description: Detect a list of object to win the game + +bricks: + - arduino:video_object_detection + - arduino:web_ui diff --git a/examples/object-hunting/assets/app.js b/examples/object-hunting/assets/app.js new file mode 100644 index 0000000..e01e793 --- /dev/null +++ b/examples/object-hunting/assets/app.js @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +// +// SPDX-License-Identifier: MPL-2.0 + +const socket = io(`http://${window.location.host}`); +const errorContainer = document.getElementById('error-container'); + +// Game state +const targetObjects = ['book', 'bottle', 'chair', 'cup', 'cell phone']; +let foundObjects = []; +let gameStarted = false; + +// UI Elements +const gameIntro = document.getElementById('game-intro'); +const gameContent = document.getElementById('game-content'); +const startGameBtn = document.getElementById('start-game-btn'); +const objectsToFindList = document.getElementById('objects-to-find-list'); +const videoFeedContainer = document.getElementById('videoFeedContainer'); +const winScreen = document.getElementById('win-screen'); +const playAgainBtn = document.getElementById('play-again-btn'); + +document.addEventListener('DOMContentLoaded', () => { + initSocketIO(); + initializeConfidenceSlider(); + renderObjectsToFind(); + + startGameBtn.addEventListener('click', startGame); + playAgainBtn.addEventListener('click', resetGame); +}); + +function initSocketIO() { + socket.on('connect', () => { + if (errorContainer) { + errorContainer.style.display = 'none'; + errorContainer.textContent = ''; + } + }); + + socket.on('disconnect', () => { + if (errorContainer) { + errorContainer.textContent = 'Connection to the board lost. Please check the connection.'; + errorContainer.style.display = 'block'; + } + }); + + socket.on('detection', async (message) => { + if (gameStarted) { + handleDetection(message); + } + }); +} + +function startGame() { + gameStarted = true; + gameIntro.classList.add('hidden'); + gameContent.classList.remove('hidden'); + updateFoundCounter(); +} + +function resetGame() { + gameStarted = false; + foundObjects = []; + winScreen.classList.add('hidden'); + videoFeedContainer.classList.remove('hidden'); + gameIntro.classList.remove('hidden'); + gameContent.classList.add('hidden'); + renderObjectsToFind(); + updateFoundCounter(); +} + +function updateFoundCounter() { + const chip = document.getElementById('found-counter-chip'); + if (chip) { + chip.textContent = `${foundObjects.length}/5 found`; + } +} + +function renderObjectsToFind() { + objectsToFindList.innerHTML = ''; + targetObjects.forEach(obj => { + const item = document.createElement('div'); + item.id = `obj-${obj}`; + item.className = 'object-item'; + + const icon = document.createElement('img'); + icon.src = `./img/${obj}.svg`; + icon.alt = `${obj} icon`; + item.appendChild(icon); + + const text = document.createElement('span'); + text.textContent = obj; + item.appendChild(text); + + objectsToFindList.appendChild(item); + }); +} + +function handleDetection(detection) { + const detectedObject = detection.content.toLowerCase(); + if (targetObjects.includes(detectedObject) && !foundObjects.includes(detectedObject)) { + foundObjects.push(detectedObject); + const foundItem = document.getElementById(`obj-${detectedObject}`); + foundItem.classList.add('found'); + + const foundIcon = document.createElement('img'); + foundIcon.src = './img/found-icon.svg'; + foundIcon.alt = 'Found'; + foundIcon.className = 'found-icon'; + foundItem.appendChild(foundIcon); + + updateFoundCounter(); + checkWinCondition(); + } +} + +function checkWinCondition() { + if (foundObjects.length === targetObjects.length) { + gameStarted = false; + videoFeedContainer.classList.add('hidden'); + winScreen.classList.remove('hidden'); + } +} + +function initializeConfidenceSlider() { + const confidenceSlider = document.getElementById('confidenceSlider'); + const confidenceInput = document.getElementById('confidenceInput'); + const confidenceResetButton = document.getElementById('confidenceResetButton'); + + if (!confidenceSlider) return; + + confidenceSlider.addEventListener('input', updateConfidenceDisplay); + confidenceInput.addEventListener('input', handleConfidenceInputChange); + confidenceInput.addEventListener('blur', validateConfidenceInput); + updateConfidenceDisplay(); + + confidenceResetButton.addEventListener('click', (e) => { + if (e.target.classList.contains('reset-icon') || e.target.closest('.reset-icon')) { + resetConfidence(); + } + }); +} + +function handleConfidenceInputChange() { + const confidenceInput = document.getElementById('confidenceInput'); + const confidenceSlider = document.getElementById('confidenceSlider'); + + let value = parseFloat(confidenceInput.value); + + if (isNaN(value)) value = 0.5; + if (value < 0) value = 0; + if (value > 1) value = 1; + + confidenceSlider.value = value; + updateConfidenceDisplay(); +} + +function validateConfidenceInput() { + const confidenceInput = document.getElementById('confidenceInput'); + let value = parseFloat(confidenceInput.value); + + if (isNaN(value)) value = 0.5; + if (value < 0) value = 0; + if (value > 1) value = 1; + + confidenceInput.value = value.toFixed(2); + + handleConfidenceInputChange(); +} + +function updateConfidenceDisplay() { + const confidenceSlider = document.getElementById('confidenceSlider'); + const confidenceInput = document.getElementById('confidenceInput'); + const confidenceValueDisplay = document.getElementById('confidenceValueDisplay'); + const sliderProgress = document.getElementById('sliderProgress'); + + if (!confidenceSlider) return; + + const value = parseFloat(confidenceSlider.value); + socket.emit('override_th', value); // Send confidence to backend + const percentage = (value - confidenceSlider.min) / (confidenceSlider.max - confidenceSlider.min) * 100; + + const displayValue = value.toFixed(2); + confidenceValueDisplay.textContent = displayValue; + + if (document.activeElement !== confidenceInput) { + confidenceInput.value = displayValue; + } + + sliderProgress.style.width = percentage + '%'; + confidenceValueDisplay.style.left = percentage + '%'; +} + +function resetConfidence() { + + const confidenceSlider = document.getElementById('confidenceSlider'); + + const confidenceInput = document.getElementById('confidenceInput'); + + + + if (!confidenceSlider) return; + + + + confidenceSlider.value = '0.5'; + + confidenceInput.value = '0.50'; + + updateConfidenceDisplay(); + +} + + diff --git a/examples/object-hunting/assets/fonts/Open Sans/OFL.txt b/examples/object-hunting/assets/fonts/Open Sans/OFL.txt new file mode 100644 index 0000000..4fc6170 --- /dev/null +++ b/examples/object-hunting/assets/fonts/Open Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/object-hunting/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf b/examples/object-hunting/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..548c15f Binary files /dev/null and b/examples/object-hunting/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf differ diff --git a/examples/object-hunting/assets/fonts/Roboto/OFL.txt b/examples/object-hunting/assets/fonts/Roboto/OFL.txt new file mode 100644 index 0000000..68f7a96 --- /dev/null +++ b/examples/object-hunting/assets/fonts/Roboto/OFL.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/examples/object-hunting/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf b/examples/object-hunting/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf new file mode 100644 index 0000000..3a2d704 Binary files /dev/null and b/examples/object-hunting/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf differ diff --git a/examples/object-hunting/assets/fonts/fonts.css b/examples/object-hunting/assets/fonts/fonts.css new file mode 100644 index 0000000..86cf716 --- /dev/null +++ b/examples/object-hunting/assets/fonts/fonts.css @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-display: swap; + src: url('Roboto/RobotoMono-VariableFont_wght.ttf') format('truetype'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + src: url('Open Sans/OpenSans-VariableFont_wdth,wght.ttf') format('truetype'); +} \ No newline at end of file diff --git a/examples/object-hunting/assets/img/book.svg b/examples/object-hunting/assets/img/book.svg new file mode 100644 index 0000000..616d32e --- /dev/null +++ b/examples/object-hunting/assets/img/book.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/object-hunting/assets/img/bottle.svg b/examples/object-hunting/assets/img/bottle.svg new file mode 100644 index 0000000..6fa848e --- /dev/null +++ b/examples/object-hunting/assets/img/bottle.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/object-hunting/assets/img/cell phone.svg b/examples/object-hunting/assets/img/cell phone.svg new file mode 100644 index 0000000..ae2fc96 --- /dev/null +++ b/examples/object-hunting/assets/img/cell phone.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/object-hunting/assets/img/chair.svg b/examples/object-hunting/assets/img/chair.svg new file mode 100644 index 0000000..695ebd5 --- /dev/null +++ b/examples/object-hunting/assets/img/chair.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/examples/object-hunting/assets/img/congratulations.svg b/examples/object-hunting/assets/img/congratulations.svg new file mode 100644 index 0000000..656c04d --- /dev/null +++ b/examples/object-hunting/assets/img/congratulations.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/object-hunting/assets/img/cup.svg b/examples/object-hunting/assets/img/cup.svg new file mode 100644 index 0000000..6905ab4 --- /dev/null +++ b/examples/object-hunting/assets/img/cup.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/object-hunting/assets/img/found-icon.svg b/examples/object-hunting/assets/img/found-icon.svg new file mode 100644 index 0000000..51488dc --- /dev/null +++ b/examples/object-hunting/assets/img/found-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/object-hunting/assets/img/icon-reset.svg b/examples/object-hunting/assets/img/icon-reset.svg new file mode 100644 index 0000000..2ca50b3 --- /dev/null +++ b/examples/object-hunting/assets/img/icon-reset.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/examples/object-hunting/assets/img/info.svg b/examples/object-hunting/assets/img/info.svg new file mode 100644 index 0000000..809cb3e --- /dev/null +++ b/examples/object-hunting/assets/img/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/object-hunting/assets/img/logo.svg b/examples/object-hunting/assets/img/logo.svg new file mode 100644 index 0000000..d23ae68 --- /dev/null +++ b/examples/object-hunting/assets/img/logo.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/object-hunting/assets/img/logo.svg.license b/examples/object-hunting/assets/img/logo.svg.license new file mode 100644 index 0000000..c274485 --- /dev/null +++ b/examples/object-hunting/assets/img/logo.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + +SPDX-License-Identifier: MPL-2.0 diff --git a/examples/object-hunting/assets/img/no-face.svg b/examples/object-hunting/assets/img/no-face.svg new file mode 100644 index 0000000..aa21825 --- /dev/null +++ b/examples/object-hunting/assets/img/no-face.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/object-hunting/assets/index.html b/examples/object-hunting/assets/index.html new file mode 100644 index 0000000..3182c0b --- /dev/null +++ b/examples/object-hunting/assets/index.html @@ -0,0 +1,139 @@ + + + + + + + + Object Hunting + + + +
+
+

Object Hunting

+ +
+ +
+
+
+
+ Searching webcam + +
+ +
+ +
+ +
+
+

Find all these objects to win!

+
    +
  • book iconbook
  • +
  • bottle iconbottle
  • +
  • chair iconchair
  • +
  • cup iconcup
  • +
  • cell phone iconcell phone
  • +
+
+
Use your camera to detect the target objects.
+
Adjust confidence level to help detection.
+
+ +
+ + + + +
+
+ +
+ + + + + + + + + + diff --git a/examples/object-hunting/assets/libs/socket.io.min.js b/examples/object-hunting/assets/libs/socket.io.min.js new file mode 100644 index 0000000..530b185 --- /dev/null +++ b/examples/object-hunting/assets/libs/socket.io.min.js @@ -0,0 +1,6 @@ +/*! + * Socket.IO v4.8.1 + * (c) 2014-2024 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r1))return this.writeBuffer;for(var t,n=1,i=0;i=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i=0&&t.num1?e-1:0),s=1;s1?i-1:0),e=1;ei.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t})); \ No newline at end of file diff --git a/examples/object-hunting/assets/style.css b/examples/object-hunting/assets/style.css new file mode 100644 index 0000000..d1cf57f --- /dev/null +++ b/examples/object-hunting/assets/style.css @@ -0,0 +1,895 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@import url("fonts/fonts.css"); + +/* + * This CSS is used to center the various elements on the screen + */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; margin: 0; + padding: 20px; + background-color: #DAE3E3; + color: #333; + display: flex; + flex-direction: column; + align-items: center; +} + +.main-content { + display: flex; + gap: 24px; + align-items: stretch; + justify-content: center; + width: 100%; + max-width: 1280px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32px; +} + +.container { + width: 100%; + max-width: 550px; + min-width: 500px; + height: auto; + min-height: auto; + border-radius: 16px; + background: #FFF; + padding: 24px; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: space-between; + background-color: #ECF1F1; +} + +.right-column { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; + max-width: 550px; +} + +#game-intro { + justify-content: flex-start; + gap: 24px; + align-items: center; +} + +#game-content { + display: flex; + flex-direction: column; + gap: 24px; + flex-grow: 1; +} + +.objects-list-intro { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; +} + +.object-item-intro { + display: grid; + grid-template-columns: 48px 1fr; + align-items: center; + gap: 16px; + font-size: 16px; + text-transform: capitalize; + color: #2C353A; +} + +.object-item-intro img { + width: 48px; + height: 48px; +} + +.error-message { + margin-top: 20px; + padding: 10px; + border-radius: 5px; + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.container-right { + background-color: #ECF1F1; + padding: 16px; + border-radius: 16px; +} + +h2 { + color: #008184; + font-family: "Roboto Mono"; + font-size: 28px; + font-style: normal; + font-weight: 700; + line-height: 170%; + letter-spacing: 0.28px; + text-align: center; + margin: 0; +} + +/* + * Components styling + */ + +#videoFeedContainer { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; + width: 100%; + margin-left: auto; + margin-right: auto; +} + +#videoCanvas { + display: block; + width: 100%; + height: auto; + background-color: #333; +} + +.camera-status { + display: flex; + align-items: center; + flex-direction: column; + gap: 16px; +} + +.search { + display: flex; + align-items: center; + gap: 8px; + color: #2C353A; + font-family: "Roboto Mono", monospace; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 170%; + letter-spacing: 1.92px; +} + +.search-small { + color: #2C353A; + text-align: center; + font-family: "Open Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; +} + +#scanInfo { + text-align: center; + margin-bottom: 24px; + display: flex; + flex-direction: column; + gap: 8px; + align-self: stretch; + border-radius: 8px; + background: #FFF; +} + +#scanMessage { + text-align: center; + font-family: "Open Sans"; + font-size: 10px; + color: #2C353A; + line-height: 160%; + letter-spacing: 0.5px; +} + +#recentScansList { + list-style-type: none; + padding: 0; + flex: 1; +} + +#recentScansList li { + border-top: 1px solid #C9D2D2; + border-bottom: 1px solid #C9D2D2; + padding: 16px 0; + font-size: 12px; + color: #2C353A; +} + +.error-message { + color: red; + font-weight: bold; + text-align: center; + margin-top: 10px; +} + +#rescan-button-container { + display: flex; + justify-content: flex-end; +} + +#rescanButton { + background-color: #008184; + color: #fff; + border: none; + border-radius: 8px; + padding: 8px 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + outline: none; + box-shadow: none; + font-family: "Open Sans"; + font-size: 14px; + font-style: normal; + line-height: 160%; +} + +.arduino-text { + color: #008184; + font-family: "Roboto Mono", monospace; + font-size: 20px; + font-weight: 600; + margin: 0; + font-style: normal; + line-height: 170%; + letter-spacing: 0.28px; +} + +.icon-wrapper { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + border-radius: 50%; + transition: background-color 0.2s ease; + position: relative; +} + +.icon-wrapper:hover { + background-color: rgba(0, 129, 132, 0.1); + cursor: pointer; +} + +.icon-wrapper::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-family: "Open Sans", sans-serif; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s, visibility 0.3s; + z-index: 1000; + margin-bottom: 5px; +} + +.icon-wrapper::before { + content: ''; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: #333; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s, visibility 0.3s; + z-index: 1000; + margin-bottom: 1px; +} + +.icon-wrapper:hover::after, +.icon-wrapper:hover::before { + opacity: 1; + visibility: visible; +} + +.icon-wrapper.tooltip-success::after { + background-color: #16A588; + color: #FFF; + opacity: 1; + visibility: visible; +} + +.icon-wrapper.tooltip-success::before { + border-top-color: #16A588; + opacity: 1; + visibility: visible; +} + +.icon { + width: 12px; + height: 12px; + display: block; +} + +.delete-scan-logo:hover { + cursor: pointer; + background-color: rgba(0, 129, 132, 0.1); + padding: 4px; + border-radius: 50%; +} + +.arduino-logo { + height: 32px; + width: auto; +} + +.delete-scan-logo { + width: 18px; + height: 18px; + border-radius: 50%; + transition: background-color 0.2s ease; + padding: 4px; + +} + +.recent-scans-title-container { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + color: #2C353A; + justify-content: space-between; +} + +.recent-scans-title { + color: #2C353A; + font-family: "Roboto Mono", monospace; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 170%; + letter-spacing: 1.2px; + margin: 0; +} + +.no-recent-scans { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #5D6A6B; + gap: 8px; + margin: auto; +} + +.scan-container { + display: flex; + justify-content: space-between; + align-items: center; +} + +#scan-message { + font-family: "Open Sans"; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.1px; + color: #2C353A; + text-align: center; + +} + +.scan-header { + color: #2C353A; + font-family: "Open Sans"; + font-size: 10px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.5px; + align-self: stretch; +} + +.scan-content, +.scan-content-time { + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 160%; + color: #2C353A; + font-family: "Open Sans"; + letter-spacing: 0.5px; +} + +.scan-content-time { + font-weight: 400; +} + +.cell-border { + border-top: 1px solid #ccc; +} + +.scan-cell-container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 16px 0; +} + +.scan-error { + color: #b00020; + background: #ffeaea; + padding: 8px 12px; + border-radius: 4px; + margin-top: 8px; + font-weight: bold; +} + +/* + * Responsive design + */ + +@media (max-width: 1024px) { + .main-content { + flex-direction: column; + align-items: center; + gap: 24px; + } + + .container, .right-column { + width: 100%; + max-width: 800px; + } +} + +@media (max-width: 480px) { + .container { + min-width: 300px; + background-color: red; + } +} + +.scan-error { + text-align: center; +} + +.scan-header { + font-family: "Open Sans"; + font-size: 10px; + font-style: normal; + font-weight: 400; + color: #2C353A; +} + +.scan-content { + position: relative; + display: flex; + gap: 8px; + align-items: center; +} + +/* + * Responsive design + */ + + @media (max-width: 768px) { + body { + padding: 12px 16px; + } + + .container { + max-width: none; + padding: 20px; + min-height: auto; + } + + .arduino-text { + font-size: 24px; + } + + .main-content { + gap: 20px; + flex-direction: column; + } +} + +@media (max-width: 480px) { + body { + padding: 8px 12px; + } + + .container { + padding: 16px; + } + + .arduino-logo { + height: 20px; + width: auto; + } + + #rescanButton { + font-size: 12px; + } +} + + +/* Added from anomaly-detection */ + +.control-group { + position: relative; +} + +.slider-box { + display: flex; + align-items: center; + gap: 10px; +} + +.control-confidence { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.control-confidence-description { + color: #2C353A; + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; +} + +.control-group label { + color: #2C353A; + font-family: "Roboto Mono", monospace; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 170%; + letter-spacing: 1.2px; +} + +#confidenceSlider { + width: 100%; + height: 6px; + border-radius: 3px; + background: #DAE3E3; + outline: none; + -webkit-appearance: none; + appearance: none; + position: relative; + margin: 20px 0 10px 0; +} + +#confidenceSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: #008184; + cursor: pointer; + position: relative; + bottom: 3px; + z-index: 2; +} + +#confidenceSlider::-webkit-slider-runnable-track { + width: 100%; + height: 6px; + border-radius: 3px; + background: #DAE3E3; +} + +#confidenceSlider::-moz-range-track { + width: 100%; + height: 6px; + border-radius: 3px; + background: #DAE3E3; + border: none; +} + +.slider-container { + position: relative; + width: 100%; +} + +.slider-progress { + position: absolute; + top: 20px; + left: 0; + height: 6px; + background: #008184; + border-radius: 3px; + pointer-events: none; + z-index: 1; + transition: width 0.1s ease; +} + +.confidence-value-display { + position: absolute; + top: -3px; + transform: translateX(-50%); + color: #008184; + padding: 2px 6px; + pointer-events: none; + z-index: 3; + white-space: nowrap; + transition: left 0.1s ease; + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.12px; +} + +.confidence-limits { + color: #2C353A; + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.12px; + margin-top: 10px; +} + +.btn-tertiary { + border-radius: 6px; + border: 1px solid #C9D2D2; + background: white; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 4px 8px; + cursor: pointer; + transition: all 0.3s ease; + font-size: 12px; + min-width: 50px; + height: 36px; +} + +.confidence-input { + border: none; + background: transparent; + font-size: 12px; + font-weight: inherit; + color: inherit; + text-align: center; + width: 32px; + padding: 0; + margin: 0; + outline: none; + cursor: text; +} + +.confidence-input:focus { + background: rgba(0, 129, 132, 0.1); + border-radius: 2px; +} + +.reset-icon { + width: 18px; + height: 18px; + opacity: 0.7; + transition: opacity 0.3s ease; + cursor: pointer; +} + +.reset-icon svg path { + fill: black; +} + +.btn-tertiary:hover .reset-icon { + opacity: 1; +} + +.feedback-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; +} + +.feedback-text { + color: #5D6A6B; + text-align: center; +} +#recentDetections { + list-style-type: none; + padding: 0; + flex: 1; +} + +#recentDetections li { + border-top: 1px solid #C9D2D2; + border-bottom: 1px solid #C9D2D2; + font-size: 12px; + color: #2C353A; + padding: 16px 0; + word-break: break-all; + display: flex; + justify-content: space-between; + align-items: center; +} + +.detection-info { + display: flex; + align-items: center; + gap: 8px; +} + +.detection-time-info { + display: flex; + align-items: center; + gap: 8px; +} + + +#recentClassifications { + list-style-type: none; + padding: 0; + flex: 1; +} + + +/* Custom styles for layout */ + +.main-content > .container { + flex-grow: 1; +} + +.right-column { + flex-shrink: 0; +} + +@media (max-width: 1024px) { + #videoFeedContainer iframe, + #win-screen { + width: 100%; + height: auto; + aspect-ratio: 1024 / 768; + } +} + +.feedback-detection { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 16px; +} + +.feedback-detection .percentage { + font-size: 24px; + font-weight: bold; + color: #008184; +} + +.feedback-detection img { + max-width: 64px; + max-height: 64px; +} + +.feedback-detection p { + font-size: 18px; + font-weight: 500; + color: #2C353A; + margin: 0; +} + + + +#video-container { + padding: 24px 0px; +} + +.hidden { + display: none !important; +} + +#objects-to-find-list .object-item { + padding: 10px; + display: grid; + grid-template-columns: 48px 1fr; + align-items: center; + gap: 16px; + font-size: 16px; + text-transform: capitalize; + color: #2C353A; +} + +#objects-to-find-list .object-item img { + width: 48px; + height: 48px; +} + +#objects-to-find-list .object-item.found { + background-color: #DCF2ED; + border: 1px solid #16A588; + border-radius: 8px; + padding: 12px 24px 12px 16px; + grid-template-columns: 48px 1fr auto; + margin-bottom: 8px; +} + +.found-icon { + width: 24px !important; + height: 24px !important; +} + +#objects-to-find-list { + margin-top: 16px; +} + +.btn-primary { + background-color: #008184; + color: white; + border: none; + padding: 10px 20px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 0 auto; + cursor: pointer; + border-radius: 8px; + width: fit-content; +} + +.chip { + background-color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + color: #333; + border: 1px solid #ccc; +} + +#win-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + text-align: center; + height: 768px; + box-sizing: border-box; + padding: 0 32px; +} + +.congratulations-icon { + width: 40px; + height: 40px; +} + +.sub-text { + margin: 16px 0; + text-align: center; + font-family: "Open Sans"; + font-size: 16px; + color: #2C353A; +} \ No newline at end of file diff --git a/examples/object-hunting/python/main.py b/examples/object-hunting/python/main.py new file mode 100644 index 0000000..e6624ea --- /dev/null +++ b/examples/object-hunting/python/main.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +# +# SPDX-License-Identifier: MPL-2.0 + +from arduino.app_utils import App +from arduino.app_bricks.web_ui import WebUI +from arduino.app_bricks.video_objectdetection import VideoObjectDetection +from datetime import datetime, UTC + +ui = WebUI() +detection_stream = VideoObjectDetection() + +ui.on_message("override_th", lambda sid, threshold: detection_stream.override_threshold(threshold)) + +def send_detections_to_ui(detections: dict): + for key, value in detections.items(): + entry = { + "content": key, + "timestamp": datetime.now(UTC).isoformat() + } + ui.send_message("detection", message=entry) + +detection_stream.on_detect_all(send_detections_to_ui) + +App.run() diff --git a/examples/theremin/README.md b/examples/theremin/README.md new file mode 100644 index 0000000..0306c50 --- /dev/null +++ b/examples/theremin/README.md @@ -0,0 +1,146 @@ +# Theremin Simulator + +The **Theremin Simulator** example lets you create and control a virtual theremin instrument using an interactive web interface, producing synthesized audio output through a connected **USB** audio device with minimal latency. + +> **Note**: This example must be run in **[Network Mode](learn/network-mode)** or **[SBC Mode](learn/single-board-computer)**, since it requires a **USB-C® hub** and a **USB speaker**. + +![Theremin Simulator](assets/docs_assets/theremin-simulator.png) + +This example generates real-time audio by creating sine waves at varying frequencies and amplitudes based on user input from the web interface. The workflow involves receiving mouse/touch coordinates from the frontend, calculating the corresponding frequency and amplitude, generating audio blocks using a sine wave generator, and playing them through a **USB** audio device with minimal latency. + + +## Bricks Used + +- `web_ui`: Brick that provides the web interface and a WebSocket channel for real-time control of the theremin. +- `wave_generator`: Brick that generates continuous audio waveforms and streams them to the USB speaker with smooth frequency and amplitude transitions. + + +## Hardware and Software Requirements + +### Hardware + +- [Arduino UNO Q](https://store.arduino.cc/products/uno-q) (x1) +- **USB-C® hub with external power (x1)** +- A power supply (5 V, 3 A) for the USB hub adapter with external power (x1) +- A **USB audio device** (choose one): + - **USB speaker** (cabled) (x1) ✅ *supported* + - **USB wireless speaker receiver/dongle** (2.4 GHz) (x1) ✅ *supported* + - **USB‑C → 3.5 mm audio connector** + headphones/speakers (x1) ⚠️ *not tested* (may work) +- A **power supply** (5 V, 3 A) for the USB hub (e.g. a phone charger) + +> **Not supported:** **HDMI audio and Bluetooth® Speakers** output is not supported by this App. + +### Software + +- Arduino App Lab + +**Note:** A **USB-C® hub is mandatory** for this example. The UNO Q's single port must be used for the hub, which provides the necessary connections for both the power supply and the USB audio device. Consequently, this example must be run in **[Network Mode](learn/network-mode)** or **[SBC Mode](learn/single-board-computer)**. + + +## How to Use the Example + +1. Connect your **USB audio device** (e.g., USB speaker, wireless USB receiver, or USB‑C→3.5 mm dongle) to a powered **USB-C® hub** attached to the UNO Q. + ![hardware-setup](assets/docs_assets/hardware-setup.png) + +2. Launch the App by clicking the **Play** button in the top-right corner. Wait until the App has launched. + ![Launching an App](assets/docs_assets/launch-app-theremin.png) +3. Open the App in your browser at `:7000` *(typically 192.168.x.x, e.g., http://192.168.1.11:7000)*. +4. Click and drag your mouse (or use touch) on the interactive area to play: + - **Horizontal movement (X-axis)** controls the **pitch** (frequency). + - **Vertical movement (Y-axis)** controls the **volume** (amplitude). + + +## How it Works + +The application creates a real-time audio synthesizer controlled by a web interface. User interactions on the webpage are sent to the Python backend via a WebSocket. The backend uses the `wave_generator` brick to continuously generate and stream audio to the connected **USB** audio device with smooth transitions. + +- **User Interaction**: The frontend captures mouse or touch coordinates within a designated "play area". +- **Real-time Communication**: These coordinates are sent to the Python backend in real-time using the `web_ui` Brick's WebSocket channel. +- **Audio Synthesis**: The backend maps the X-coordinate to **frequency** and the Y-coordinate to **amplitude**, then updates the `wave_generator` brick's state. The brick handles smooth transitions using configurable envelope parameters (attack, release, glide). +- **Audio Output**: The `wave_generator` brick runs continuously in a background thread, generating audio blocks and streaming them to the **USB** audio device with minimal latency. + +High-level data flow: +``` +Web Browser Interaction → WebSocket → Python Backend → WaveGenerator Brick → USB Audio Device Output +``` + + +## Understanding the Code + +### 🔧 Backend (`main.py`) + +The Python code manages the web server, handles real-time user input, and controls the audio generation brick. + +- `ui = WebUI()` – Initializes the web server that serves the HTML interface and handles WebSocket communication. +- `wave_gen = WaveGenerator(...)` – Creates the wave generator brick with configured envelope parameters (attack=0.01s, release=0.03s, glide=0.02s). The brick automatically manages the USB speaker connection and audio streaming in a background thread. +- `ui.on_message('theremin:move', on_move)` – Registers a handler that fires whenever the frontend sends new coordinates. This function updates the wave generator's frequency and amplitude using `wave_gen.set_frequency()` and `wave_gen.set_amplitude()`. +- The `wave_generator` brick handles all audio generation and streaming automatically, including smooth transitions between frequency and amplitude changes, continuous audio output with ~**30 ms** blocks, and non-blocking playback without cracks or pops. + +### 💻 Frontend (`main.js`) + +The web interface provides the interactive play area and controls for the user. + +- **Socket.IO connection** to the backend to send and receive data in real time. +- **Event listeners** capture `mousedown`, `mousemove`, `mouseup` (and touch equivalents) to track user interaction in the play area. +- `socket.emit('theremin:move', { x, y })` – Sends normalized (0.0–1.0) X and Y coordinates to the backend; emissions are **throttled to ~80 Hz (≈12 ms)** to avoid overload. +- `socket.on('theremin:state', ...)` – Receives state updates from the backend (like the calculated frequency and amplitude) and updates the values displayed on the webpage. +- `socket.emit('theremin:set_volume', { volume })` – Sends a **0-100** hardware volume value to control the USB speaker's output level. +- `socket.emit('theremin:power', { on })` – Toggles synth power (**On/Off**). After turning **On**, move/tap in the play area to resume sound. + + + +## Troubleshooting + +### "No USB speaker found" error + +If the application fails to start and you see the following error in the logs, it means the required audio hardware is missing or not detected. +``` +arduino.app_peripherals.speaker.SpeakerException: No USB speaker found. +``` +**Fix:** +1. Make sure a **powered USB-C® hub** is connected to the UNO Q and its **5 V / 3 A** power supply is plugged in. +2. Verify the **USB audio device** (USB speaker, wireless USB receiver, or USB-C→3.5 mm dongle) is **connected to the hub** and, if it has a switch, **turned on**. +3. Restart the application. + +### No Sound Output + +- **Power Button:** Make sure the button in the web UI shows **On**. +- **Volume Slider:** Increase the volume slider in the web UI. +- **Pointer Position:** Move your mouse/finger toward the top of the play area (the bottom corresponds to zero volume). +- **Speaker/Headphone Volume:** Check the physical volume control and mute status on your speaker or headphones. +- **Output Path:** Remember that **HDMI audio** and **Bluetooth® speakers** are not supported; use a **USB** audio device. + +### Choppy or Crackling Audio + +- **CPU Load:** Close any other applications running on the Arduino UNO Q that may be consuming significant resources. +- **Power Supply:** Ensure you are using a stable, adequate power supply (5 V, 3 A) for the USB-C® hub, as insufficient power can affect USB peripheral performance. + + + +## Technical Details + +- **Sample rate:** 16,000 Hz +- **Audio format:** 32-bit float, little-endian +- **Block duration:** ~30 ms (≈480 samples per block) +- **Frequency range:** ~20 Hz to ~8,000 Hz +- **Update rate:** Frontend throttled to ~80 Hz (≈12 ms minimum between updates) + + + +## Compatibility Notes + +- **Works with:** + - **USB speakers** (cabled) + - **USB wireless speaker receivers** (2.4 GHz dongles) +- **Untested (may work):** + - **USB‑C → 3.5 mm audio dongles** feeding analog speakers/headphones +- **Not supported:** + - **HDMI audio** output + - **Bluetooth® speakers** + + +## License + +This example is licensed under the Mozilla Public License 2.0 (MPL-2.0). + +Copyright (C) ARDUINO SRL (http://www.arduino.cc) \ No newline at end of file diff --git a/examples/theremin/app.yaml b/examples/theremin/app.yaml new file mode 100644 index 0000000..78ddd6b --- /dev/null +++ b/examples/theremin/app.yaml @@ -0,0 +1,6 @@ +name: Theremin simulator +icon: 🎼 +description: A simple theremin simulator that generates audio based on user input. +bricks: + - arduino:web_ui + - arduino:wave_generator diff --git a/examples/theremin/assets/docs_assets/hardware-setup.png b/examples/theremin/assets/docs_assets/hardware-setup.png new file mode 100644 index 0000000..46a1932 Binary files /dev/null and b/examples/theremin/assets/docs_assets/hardware-setup.png differ diff --git a/examples/theremin/assets/docs_assets/launch-app-theremin.png b/examples/theremin/assets/docs_assets/launch-app-theremin.png new file mode 100644 index 0000000..50928ef Binary files /dev/null and b/examples/theremin/assets/docs_assets/launch-app-theremin.png differ diff --git a/examples/theremin/assets/docs_assets/theremin-simulator.png b/examples/theremin/assets/docs_assets/theremin-simulator.png new file mode 100644 index 0000000..fce4662 Binary files /dev/null and b/examples/theremin/assets/docs_assets/theremin-simulator.png differ diff --git a/examples/theremin/assets/fonts/Open Sans/OFL.txt b/examples/theremin/assets/fonts/Open Sans/OFL.txt new file mode 100644 index 0000000..d2a4922 --- /dev/null +++ b/examples/theremin/assets/fonts/Open Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/examples/theremin/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf b/examples/theremin/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..548c15f Binary files /dev/null and b/examples/theremin/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf differ diff --git a/examples/theremin/assets/fonts/Roboto/OFL.txt b/examples/theremin/assets/fonts/Roboto/OFL.txt new file mode 100644 index 0000000..5d6f71c --- /dev/null +++ b/examples/theremin/assets/fonts/Roboto/OFL.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/theremin/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf b/examples/theremin/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf new file mode 100644 index 0000000..3a2d704 Binary files /dev/null and b/examples/theremin/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf differ diff --git a/examples/theremin/assets/fonts/fonts.css b/examples/theremin/assets/fonts/fonts.css new file mode 100644 index 0000000..86cf716 --- /dev/null +++ b/examples/theremin/assets/fonts/fonts.css @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-display: swap; + src: url('Roboto/RobotoMono-VariableFont_wght.ttf') format('truetype'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + src: url('Open Sans/OpenSans-VariableFont_wdth,wght.ttf') format('truetype'); +} \ No newline at end of file diff --git a/examples/theremin/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg b/examples/theremin/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg new file mode 100644 index 0000000..c942003 --- /dev/null +++ b/examples/theremin/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/theremin/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg.license b/examples/theremin/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg.license new file mode 100644 index 0000000..c274485 --- /dev/null +++ b/examples/theremin/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + +SPDX-License-Identifier: MPL-2.0 diff --git a/examples/theremin/assets/img/favicon.png b/examples/theremin/assets/img/favicon.png new file mode 100644 index 0000000..019a8cf Binary files /dev/null and b/examples/theremin/assets/img/favicon.png differ diff --git a/examples/theremin/assets/img/grid.svg b/examples/theremin/assets/img/grid.svg new file mode 100644 index 0000000..4c189b7 --- /dev/null +++ b/examples/theremin/assets/img/grid.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/examples/theremin/assets/img/info.svg b/examples/theremin/assets/img/info.svg new file mode 100644 index 0000000..809cb3e --- /dev/null +++ b/examples/theremin/assets/img/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/theremin/assets/img/nodata.svg b/examples/theremin/assets/img/nodata.svg new file mode 100644 index 0000000..1b6f895 --- /dev/null +++ b/examples/theremin/assets/img/nodata.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/theremin/assets/img/play-area.svg b/examples/theremin/assets/img/play-area.svg new file mode 100644 index 0000000..5b52b0a --- /dev/null +++ b/examples/theremin/assets/img/play-area.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/examples/theremin/assets/img/power-off.svg b/examples/theremin/assets/img/power-off.svg new file mode 100644 index 0000000..b94ba3a --- /dev/null +++ b/examples/theremin/assets/img/power-off.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/theremin/assets/img/power-on.svg b/examples/theremin/assets/img/power-on.svg new file mode 100644 index 0000000..d362c30 --- /dev/null +++ b/examples/theremin/assets/img/power-on.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/theremin/assets/img/switch-off.svg b/examples/theremin/assets/img/switch-off.svg new file mode 100644 index 0000000..3a2894d --- /dev/null +++ b/examples/theremin/assets/img/switch-off.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/examples/theremin/assets/img/switch-on.svg b/examples/theremin/assets/img/switch-on.svg new file mode 100644 index 0000000..5e16d1d --- /dev/null +++ b/examples/theremin/assets/img/switch-on.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/examples/theremin/assets/img/theremin-on.svg b/examples/theremin/assets/img/theremin-on.svg new file mode 100644 index 0000000..a17ad1e --- /dev/null +++ b/examples/theremin/assets/img/theremin-on.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/theremin/assets/img/theremin.svg b/examples/theremin/assets/img/theremin.svg new file mode 100644 index 0000000..4937b9d --- /dev/null +++ b/examples/theremin/assets/img/theremin.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/theremin/assets/img/volume.svg b/examples/theremin/assets/img/volume.svg new file mode 100644 index 0000000..5c560dc --- /dev/null +++ b/examples/theremin/assets/img/volume.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/examples/theremin/assets/index.html b/examples/theremin/assets/index.html new file mode 100644 index 0000000..dca4cf9 --- /dev/null +++ b/examples/theremin/assets/index.html @@ -0,0 +1,79 @@ + + + + + + + + Theremin simulator + + + +
+
+

Theremin simulator

+ +
+
+
+
+
+ Theremin +
+
Freq: - Hz
+
Amp: -
+ +
+ Power + Access +
POWER
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
VOL
+ Grid Toggle +
GRID
+
+
+
PITCH
+
+
VOLUME
+ +
+
+ +
+ + + + + \ No newline at end of file diff --git a/examples/theremin/assets/libs/socket.io.min.js b/examples/theremin/assets/libs/socket.io.min.js new file mode 100644 index 0000000..530b185 --- /dev/null +++ b/examples/theremin/assets/libs/socket.io.min.js @@ -0,0 +1,6 @@ +/*! + * Socket.IO v4.8.1 + * (c) 2014-2024 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r1))return this.writeBuffer;for(var t,n=1,i=0;i=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i=0&&t.num1?e-1:0),s=1;s1?i-1:0),e=1;ei.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t})); \ No newline at end of file diff --git a/examples/theremin/assets/main.js b/examples/theremin/assets/main.js new file mode 100644 index 0000000..a14d227 --- /dev/null +++ b/examples/theremin/assets/main.js @@ -0,0 +1,296 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +(function(){ + const TEST_MODE = false; + const socket = io({ transports: ['websocket'] }); + const playArea = document.getElementById('play-area'); + const powerBtn = document.getElementById('power-btn'); + const accessBtn = document.getElementById('access-btn'); + const gridToggleBtn = document.getElementById('grid-toggle-btn'); + const freqDisplay = document.querySelector('#freq-display span'); + const ampDisplay = document.querySelector('#amp-display span'); + const visualizer = document.getElementById('visualizer'); + const visualizerCtx = visualizer.getContext('2d'); + const trailCanvas = document.getElementById('trail-canvas'); + const trailCtx = trailCanvas.getContext('2d'); + + const thereminSvg = document.getElementById('theremin-svg'); + + let currentVolume = 80; // Default volume (0-100) + let powerOn = false; + let accessOn = false; + let isGridOn = false; + let isDown = false; + let lastPos = { x: 0, y: 1 }; + let lastEmit = 0; + const EMIT_MIN_MS = 12; // throttle emits to ~80Hz + + // --- Canvas setup --- + function resizeCanvas() { + const rect = playArea.getBoundingClientRect(); + trailCanvas.width = rect.width; + trailCanvas.height = rect.height; + } + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + + // --- Control Buttons --- + let testModeInterval = null; + + function updateStateDisplay(freq, amp) { + if (freq !== undefined) { + freqDisplay.textContent = Math.round(freq); + } + if (amp !== undefined) { + ampDisplay.textContent = amp.toFixed(2); + } + if (freq !== undefined && amp !== undefined) { + drawVisualizer(freq, amp); + } + } + + + + accessBtn.addEventListener('click', () => { + powerOn = !powerOn; + accessBtn.src = powerOn ? 'img/switch-on.svg' : 'img/switch-off.svg'; + powerBtn.src = powerOn ? 'img/power-on.svg' : 'img/power-off.svg'; + thereminSvg.src = powerOn ? 'img/theremin-on.svg' : 'img/theremin.svg'; + socket.emit('theremin:power', { on: powerOn }); + + if (powerOn) { + if (TEST_MODE) { + if (testModeInterval) clearInterval(testModeInterval); + testModeInterval = setInterval(() => { + const randomFreq = Math.random() * 500 + 20; // Freq between 20 and 1020 + const randomAmp = Math.random(); + updateStateDisplay(randomFreq, randomAmp); + }, 100); + } + } else { + if (testModeInterval) { + clearInterval(testModeInterval); + testModeInterval = null; + } + updateStateDisplay(0, 0); // Reset to silent + } + }); + + gridToggleBtn.addEventListener('click', () => { + isGridOn = !isGridOn; + gridToggleBtn.src = isGridOn ? 'img/switch-on.svg' : 'img/switch-off.svg'; + playArea.classList.toggle('grid-on', isGridOn); + }); + + const volumeBtn = document.getElementById('volume-btn'); + + if (volumeBtn) { + volumeBtn.addEventListener('click', (event) => { + const plusBtn = event.target.closest('#volume-plus-btn'); + const minusBtn = event.target.closest('#volume-minus-btn'); + + let newVolume = currentVolume; + + if (plusBtn) { + newVolume = Math.min(100, currentVolume + 10); + } else if (minusBtn) { + newVolume = Math.max(0, currentVolume - 10); + } + + if (newVolume !== currentVolume) { + currentVolume = newVolume; + updateVolumeIndicator(currentVolume); + socket.emit('theremin:set_volume', { volume: newVolume }); + } + }); + } + + + + // --- Mouse Trail --- + const trailParticles = []; + + function addTrailParticle(x, y) { + trailParticles.push({ + x: x, + y: y, + size: 8, + opacity: 1, + }); + } + + function animateTrail() { + trailCtx.clearRect(0, 0, trailCanvas.width, trailCanvas.height); + + trailCtx.strokeStyle = 'yellow'; + trailCtx.lineCap = 'round'; + trailCtx.lineJoin = 'round'; + + for (let i = 1; i < trailParticles.length; i++) { + const p1 = trailParticles[i - 1]; + const p2 = trailParticles[i]; + + trailCtx.beginPath(); + trailCtx.moveTo(p1.x, p1.y); + trailCtx.lineTo(p2.x, p2.y); + + trailCtx.lineWidth = p2.size; + trailCtx.globalAlpha = p2.opacity; + + trailCtx.stroke(); + } + trailCtx.globalAlpha = 1.0; // Reset globalAlpha + + // Particle update logic + for (let i = 0; i < trailParticles.length; i++) { + const p = trailParticles[i]; + p.opacity -= 0.05; + p.size -= 0.2; + if (p.opacity <= 0 || p.size <= 0) { + trailParticles.splice(i, 1); + i--; + } + } + requestAnimationFrame(animateTrail); + } + animateTrail(); + + + // --- Theremin Play Area Logic --- + function sendPos(e){ + if(!powerOn || accessOn) return; + const now = Date.now(); + if(now - lastEmit < EMIT_MIN_MS) return; + lastEmit = now; + const rect = playArea.getBoundingClientRect(); + const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); + if(x === lastPos.x && y === lastPos.y) return; + lastPos.x = x; lastPos.y = y; + socket.emit('theremin:move', { x, y, ts: Date.now() }); + } + + function sendStop(){ + socket.emit('theremin:move', { x: (lastPos.x || 0), y: 1, ts: Date.now() }); + } + + playArea.addEventListener('mousemove', (e) => { + if (powerOn) { + const rect = playArea.getBoundingClientRect(); + addTrailParticle(e.clientX - rect.left, e.clientY - rect.top); + showDot(e.clientX, e.clientY); + sendPos(e); + } + }); + + playArea.addEventListener('mouseenter', (e) => { + if (powerOn) { + showDot(e.clientX, e.clientY); + } + }); + + playArea.addEventListener('mouseleave', (e) => { + if (powerOn) { + removeDot(); + sendStop(); + } + }); + + playArea.addEventListener('touchstart', (e) => { + e.preventDefault(); + isDown = true; + const touch = e.touches[0]; + sendPos(touch); + showDot(touch.clientX, touch.clientY); + if (powerOn) { + const rect = playArea.getBoundingClientRect(); + addTrailParticle(touch.clientX - rect.left, touch.clientY - rect.top); + } + }); + + playArea.addEventListener('touchmove', (e) => { + if(isDown) { + e.preventDefault(); + const touch = e.touches[0]; + sendPos(touch); + showDot(touch.clientX, touch.clientY); + if (powerOn) { + const rect = playArea.getBoundingClientRect(); + addTrailParticle(touch.clientX - rect.left, touch.clientY - rect.top); + } + } + }); + + playArea.addEventListener('touchend', (e) => { + e.preventDefault(); + if(isDown){ + isDown = false; + sendStop(); + removeDot(); + } + }); + + // --- Visual Indicators --- + function showDot(x, y){ + let dot = document.getElementById('lock-dot'); + if(!dot){ dot = document.createElement('div'); dot.id = 'lock-dot'; playArea.appendChild(dot); } + const r = playArea.getBoundingClientRect(); + dot.style.left = (x - r.left) + 'px'; + dot.style.top = (y - r.top) + 'px'; + } + + function removeDot(){ const d = document.getElementById('lock-dot'); if(d) d.remove(); } + + playArea.addEventListener('dragstart', (e) => { e.preventDefault(); }); + + function drawVisualizer(freq, amp) { + const width = visualizer.width; + const height = visualizer.height; + const mid = height / 2; + + visualizerCtx.clearRect(0, 0, width, height); + visualizerCtx.strokeStyle = '#25C2C7'; + visualizerCtx.lineWidth = 2; + visualizerCtx.beginPath(); + visualizerCtx.moveTo(0, mid); + + if (amp > 0) { + const freqScale = freq / 500; + for (let i = 0; i < width; i++) { + const y = mid + (amp * (height / 2) * Math.sin(i * freqScale * 2 * Math.PI / width)); + visualizerCtx.lineTo(i, y); + } + } else { + visualizerCtx.lineTo(width, mid); + } + visualizerCtx.stroke(); + } + + function updateVolumeIndicator(volume) { + const indicator = document.getElementById('volume-indicator'); + if (indicator) { + const angle = ((volume / 100.0) - 0.5) * 180; // -90 to +90 degrees + indicator.style.transform = `rotate(${angle}deg)`; + } + } + + // --- Socket Event Handlers --- + socket.on('theremin:state', (s) => { + if (accessOn) return; + updateStateDisplay(s.freq, s.amp); + }); + + socket.on('theremin:volume', (v) => { + if(v.volume !== undefined) { + currentVolume = v.volume; + updateVolumeIndicator(v.volume); + } + }); + + +})(); \ No newline at end of file diff --git a/examples/theremin/assets/style.css b/examples/theremin/assets/style.css new file mode 100644 index 0000000..330a768 --- /dev/null +++ b/examples/theremin/assets/style.css @@ -0,0 +1,374 @@ +/* +SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + +SPDX-License-Identifier: MPL-2.0 +*/ + +@import url('fonts/fonts.css'); + +/* typography utilities used across examples */ +body { + font-family: 'Open Sans', sans-serif; + display: flex; + justify-content: center; + margin: 0; + background-color: #ECF1F1; + color: #2C353A; + padding: 16px 40px 24px 40px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + position: sticky; + top: 0; + background: #ECF1F1; + z-index: 1000; + padding-top: 16px; +} + +.arduino-text { + color: #008184; + font-family: "Roboto Mono", monospace; + font-size: 20px; + font-weight: 600; + margin: 0; + font-style: normal; + line-height: 170%; + letter-spacing: 2.4px; +} + +.arduino-logo { + height: 24px; + width: auto; +} + +#app { + max-width: 1400px; + text-align: center; + box-sizing: border-box; +} + +/* theme variables */ +:root { + --accent: #008184 +} + +h1 { + margin: 0 0 10px; + color: var(--accent) +} + +#main-content { + display: flex; + align-items: center; +} + +#play-area { + width: 100%; + min-width: 1280px; + height: 700px; + background-image: url(img/play-area.svg); + background-size: cover; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: #6c757d; + position: relative; + user-select: none; + touch-action: none; + gap: 16px; + padding: 16px; + box-sizing: border-box; +} +#trail-canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; /* Ensure trail is visible */ +} + +#grid-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: none; + pointer-events: none; + background-image: repeating-linear-gradient(90deg, #C9D2D2 0 1px, transparent 1px 50px); +} + +#play-area.grid-on #grid-overlay { + display: block; +} + +#right-controls { + display: flex; + flex-direction: column; + gap: 16px; + width: 100px; + height: 700px; +} + +#volume-control-area, +#grid-toggle-area { + height: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +#volume-control-area svg { + width: 100%; + height: auto; +} + +#volume-knob { + cursor: pointer; +} + +#theremin-container { + position: relative; + width: 80%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; +} + +#theremin-svg { + width: 95%; + height: 100%; + pointer-events: none; /* Ensure mouse events pass through to playArea */ +} + +#theremin-display { + position: absolute; + left: 25.2%; + top: 78%; + width: 54.4%; + height: 18.45%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + color: white; + font-family: "Roboto Mono", monospace; +} + +#freq-display, #amp-display { + position: absolute; + top: 8px; + font-size: 12px; + font-family: "Open Sans"; +} + +#freq-display { + left: 160px; +} + +#amp-display { + right: 270px; +} + +#visualizer { + position: absolute; + bottom: 10px; + left: 149px; + width: 275px; + height: 100px; +} + +.theremin-controls { + position: absolute; + z-index: 15; +} + +.theremin-controls-led { + left: 350px; + bottom: 75px; +} + +.theremin-controls-power { + left: 400px; + bottom: 75px; +} + +.theremin-controls-volume { + left: 765px; + bottom: 60px; +} + +.theremin-controls-grid { + left: 855px; + bottom: 75px; +} + + +.control-btn { + width: 40px; + height: 40px; + cursor: pointer; + padding: 5px; + transition: all 0.2s ease; +} + +.control-btn-volume { + width: 60px; + height: 60px; + cursor: pointer; + border-radius: 50%; + padding: 5px; + transition: all 0.2s ease; +} + +.axis { + position: absolute; + background-color: white; + z-index: 10; +} + +.x-axis { + bottom: 16px; + left: 16px; + right: 16px; + height: 2px; +} + +.x-axis::after { + content: ''; + position: absolute; + top: -4px; + right: -10px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 10px solid white; +} + +.y-axis { + top: 16px; + left: 16px; + bottom: 16px; + width: 2px; +} + +.y-axis::before { + content: ''; + position: absolute; + left: -4px; + top: -10px; /* Keep the arrow pointing outwards */ + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 10px solid white; +} + +.axis-label { + position: absolute; + color: #2C353A; + text-align: center; + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 600; + z-index: 10; +} + +.x-label { + bottom: 24px; + right: 24px; + color: white; +} + +.y-label { + top: 16px; + left: 32px; +} + +#status-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; +} + +#status { + font-size: 13px; + color: #5d6a6b +} + +/* lock dot shown when area is locked */ +#lock-dot { + position: absolute; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--accent); + pointer-events: none; + transform: translate(-50%, -50%); + box-shadow: 0 0 8px rgba(0, 129, 132, 0.6) +} + +/* + * Responsive design + */ +@media (max-width: 768px) { + body { + padding: 12px 20px; + } + + .arduino-text { + font-size: 14px; + } + + .arduino-logo { + height: 16px; + width: auto; + } +} + +.control-label { + position: absolute; + color: white; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 160%; /* 19.2px */ + letter-spacing: 0.12px; + z-index: 15; + pointer-events: none; +} + +#power-label { + left: 401px; + bottom: 55px; + width: 40px; + text-align: center; +} + +#volume-label { + left: 785px; + bottom: 55px; + width: 40px; + text-align: center; +} + +#volume-indicator { + transform-origin: 34px 25px; + transition: transform 0.2s ease-in-out; + pointer-events: none; +} + +#grid-label { + left: 860px; + bottom: 55px; + width: 40px; + text-align: center; +} \ No newline at end of file diff --git a/examples/theremin/python/main.py b/examples/theremin/python/main.py new file mode 100644 index 0000000..5c2acab --- /dev/null +++ b/examples/theremin/python/main.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +# +# SPDX-License-Identifier: MPL-2.0 + +from arduino.app_bricks.web_ui import WebUI +from arduino.app_bricks.wave_generator import WaveGenerator +from arduino.app_utils import App, Logger +import logging + +logger = Logger("theremin", logging.DEBUG) + +# configuration +SAMPLE_RATE = 16000 + +# Wave generator brick - handles audio generation and streaming automatically +wave_gen = WaveGenerator( + sample_rate=SAMPLE_RATE, + wave_type="sine", + block_duration=0.03, + attack=0.01, + release=0.03, + glide=0.02, +) + +# Set initial state +wave_gen.set_frequency(440.0) +wave_gen.set_amplitude(0.0) + + +# --- Web UI and event handlers ----------------------------------------------------- +# The WaveGenerator brick handles audio generation and streaming automatically in +# a background thread. We only need to update frequency and amplitude via its API. +ui = WebUI() + + +def on_connect(sid, data=None): + state = wave_gen.get_state() + ui.send_message("theremin:state", {"freq": state["frequency"], "amp": state["amplitude"]}) + ui.send_message("theremin:volume", {"volume": state["volume"]}) + + +def _freq_from_x(x): + return 20.0 * ((SAMPLE_RATE / 2.0 / 20.0) ** x) + + +def on_move(sid, data=None): + """Update desired frequency/amplitude. + + The WaveGenerator brick handles smooth transitions automatically using + the configured envelope parameters (attack, release, glide). + """ + d = data or {} + x = float(d.get("x", 0.0)) + y = float(d.get("y", 1.0)) + freq = d.get("freq") + freq = float(freq) if freq is not None else _freq_from_x(x) + amp = max(0.0, min(1.0, 1.0 - float(y))) + + logger.debug(f"on_move: x={x:.3f}, y={y:.3f} -> freq={freq:.1f}Hz, amp={amp:.3f}") + + # Update wave generator state + wave_gen.set_frequency(freq) + wave_gen.set_amplitude(amp) + + ui.send_message("theremin:state", {"freq": freq, "amp": amp}, room=sid) + + +def on_power(sid, data=None): + d = data or {} + on = bool(d.get("on", False)) + if not on: + wave_gen.set_amplitude(0.0) + + +def on_set_volume(sid, data=None): + d = data or {} + volume = int(d.get("volume", 100)) + volume = max(0, min(100, volume)) + wave_gen.set_volume(volume) + ui.send_message("theremin:volume", {"volume": volume}) + + +ui.on_connect(on_connect) +ui.on_message("theremin:move", on_move) +ui.on_message("theremin:power", on_power) +ui.on_message("theremin:set_volume", on_set_volume) + +# Run the app - WaveGenerator handles audio generation automatically +App.run() diff --git a/examples/vibration-anomaly-detection/app.yaml b/examples/vibration-anomaly-detection/app.yaml new file mode 100644 index 0000000..e5605ca --- /dev/null +++ b/examples/vibration-anomaly-detection/app.yaml @@ -0,0 +1,7 @@ +name: Fan Vibration Monitoring +icon: 🌀 +description: Monitor fan vibrations and detect anomalies + +bricks: + - arduino:web_ui + - arduino:vibration_anomaly_detection diff --git a/examples/vibration-anomaly-detection/assets/app.js b/examples/vibration-anomaly-detection/assets/app.js new file mode 100644 index 0000000..f2d324b --- /dev/null +++ b/examples/vibration-anomaly-detection/assets/app.js @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +// +// SPDX-License-Identifier: MPL-2.0 + +const fanLed = document.getElementById('fan-led'); +const fanText = document.getElementById('fan-text'); +let timeoutId; + +/* + * Socket initialization. We need it to communicate with the server + */ +const socket = io(`http://${window.location.host}`); // Initialize socket.io connection + +// Start the application +document.addEventListener('DOMContentLoaded', () => { + initSocketIO(); +}); + +function initSocketIO() { + socket.on('fan_status_update', (message) => { + updateFanStatus(message); + }); +} + +// Function to update LED status in the UI +function updateFanStatus(status) { + const isOn = status.anomaly; + + changeStatus(isOn); + + if (timeoutId) { + clearTimeout(timeoutId); + } + + // schedule reset + timeoutId = setTimeout(() => changeStatus(!isOn), 3000); +} + +function changeStatus(isOn) { + fanLed.className = isOn ? 'led-on' : 'led-off'; + fanText.textContent = isOn ? 'Anomaly detected' : 'No anomaly'; +} diff --git a/examples/vibration-anomaly-detection/assets/fonts/OFL.txt b/examples/vibration-anomaly-detection/assets/fonts/OFL.txt new file mode 100644 index 0000000..38d9750 --- /dev/null +++ b/examples/vibration-anomaly-detection/assets/fonts/OFL.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/vibration-anomaly-detection/assets/fonts/RobotoMono-VariableFont_wght.ttf b/examples/vibration-anomaly-detection/assets/fonts/RobotoMono-VariableFont_wght.ttf new file mode 100644 index 0000000..3a2d704 Binary files /dev/null and b/examples/vibration-anomaly-detection/assets/fonts/RobotoMono-VariableFont_wght.ttf differ diff --git a/examples/vibration-anomaly-detection/assets/fonts/roboto-mono.css b/examples/vibration-anomaly-detection/assets/fonts/roboto-mono.css new file mode 100644 index 0000000..5b62031 --- /dev/null +++ b/examples/vibration-anomaly-detection/assets/fonts/roboto-mono.css @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-display: swap; + src: url('RobotoMono-VariableFont_wght.ttf') format('truetype'); +} diff --git a/examples/vibration-anomaly-detection/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg b/examples/vibration-anomaly-detection/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg new file mode 100644 index 0000000..c942003 --- /dev/null +++ b/examples/vibration-anomaly-detection/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/vibration-anomaly-detection/assets/img/fan.png b/examples/vibration-anomaly-detection/assets/img/fan.png new file mode 100644 index 0000000..c8b0e6a Binary files /dev/null and b/examples/vibration-anomaly-detection/assets/img/fan.png differ diff --git a/examples/vibration-anomaly-detection/assets/index.html b/examples/vibration-anomaly-detection/assets/index.html new file mode 100644 index 0000000..320c84f --- /dev/null +++ b/examples/vibration-anomaly-detection/assets/index.html @@ -0,0 +1,39 @@ + + + + + + + + Fan Vibration Monitoring + + + + + +
+
+

Fan Vibration Monitoring

+ +
+ + +
+ +
+ +
+ No anomaly +
+
+
+
+ + + + + \ No newline at end of file diff --git a/examples/vibration-anomaly-detection/assets/libs/socket.io.min.js b/examples/vibration-anomaly-detection/assets/libs/socket.io.min.js new file mode 100644 index 0000000..530b185 --- /dev/null +++ b/examples/vibration-anomaly-detection/assets/libs/socket.io.min.js @@ -0,0 +1,6 @@ +/*! + * Socket.IO v4.8.1 + * (c) 2014-2024 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r1))return this.writeBuffer;for(var t,n=1,i=0;i=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i=0&&t.num1?e-1:0),s=1;s1?i-1:0),e=1;ei.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t})); \ No newline at end of file diff --git a/examples/vibration-anomaly-detection/assets/style.css b/examples/vibration-anomaly-detection/assets/style.css new file mode 100644 index 0000000..9d37cdc --- /dev/null +++ b/examples/vibration-anomaly-detection/assets/style.css @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@import url("fonts/roboto-mono.css"); + +/* + * This CSS is used to center the various elements on the screen + */ +* { + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background-color: #ECF1F1; + color: #2C353A; + padding: 24px 40px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; +} + +.arduino-text { + color: #008184; + font-family: "Roboto Mono", monospace; + font-size: 20px; + font-weight: 600; + margin: 0; + font-style: normal; + line-height: 170%; + letter-spacing: 0.28px; +} + +.arduino-logo { + height: 32px; + width: auto; +} + +.container { + text-align: center; +} + +/* + * LED Button styling + */ +.led-container { + display: flex; + justify-content: center; + margin-bottom: 32px; + padding-top: 40px; +} + +#fan-led { + width: 128px; + height: 128px; + border-radius: 50%; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + font-family: inherit; + font-weight: 600; + font-size: 14px; + text-align: center; + line-height: 1.2; + outline: none; + position: relative; + border: 2px solid #C9D2D2; +} + +#fan-led.led-off { + background: #008184; + color: #ffffff; + box-shadow: 0 0 20px #008184, 0 0 40px #008184, 0 0 60px #008184; + border-color: #008184; +} + +#fan-led.led-on { + background: #e00d0d; + color: #ffffff; + box-shadow: 0 0 20px #e00d0d, 0 0 40px #e00d0d, 0 0 60px #e00d0d; + border-color: #e00d0d; +} + +#fan-led:hover { + transform: scale(1.05); +} + +#fan-led:active { + transform: scale(0.95); +} + +.instruction-text { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.12px; + color: #2C353A; +} + +/* + * Responsive design + */ +@media (max-width: 768px) { + body { + padding: 12px 20px; + } + + .arduino-text { + font-size: 14px; + } + + .arduino-logo { + height: 20px; + width: auto; + } +} \ No newline at end of file diff --git a/examples/vibration-anomaly-detection/python/main.py b/examples/vibration-anomaly-detection/python/main.py new file mode 100644 index 0000000..f79b877 --- /dev/null +++ b/examples/vibration-anomaly-detection/python/main.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +# +# SPDX-License-Identifier: MPL-2.0 + +from arduino.app_utils import * +from arduino.app_bricks.web_ui import WebUI +from arduino.app_bricks.vibration_anomaly_detection import VibrationAnomalyDetection + +logger = Logger("vibration-detector") + +ui = WebUI() + +vibration_detection = VibrationAnomalyDetection(anomaly_detection_threshold=1.0) + +def get_fan_status(anomaly_detected: bool): + return { + "anomaly": anomaly_detected, + "status_text": "Anomaly detected!" if anomaly_detected else "No anomaly" + } + + +# Register action to take after successful detection +def on_detected_anomaly(anomaly_score: float, classification: dict): + print(f"Detected anomaly. Score: {anomaly_score}") + ui.send_message('fan_status_update', get_fan_status(True)) + +vibration_detection.on_anomaly(on_detected_anomaly) + +def record_sensor_movement(x: float, y: float, z: float): + logger.debug(f"record_sensor_movement called with raw g-values: x={x}, y={y}, z={z}") + try: + # Convert g -> m/s^2 for the detector + x_ms2 = x * 9.81 + y_ms2 = y * 9.81 + z_ms2 = z * 9.81 + + # Forward samples to the vibration_detection brick + vibration_detection.accumulate_samples((x_ms2, y_ms2, z_ms2)) + + except Exception as e: + logger.exception(f"record_sensor_movement: Error: {e}") + print(f"record_sensor_movement: Error: {e}") + +# Register the Bridge RPC provider so the sketch can call into Python +try: + logger.debug("Registering 'record_sensor_movement' Bridge provider") + Bridge.provide("record_sensor_movement", record_sensor_movement) + logger.debug("'record_sensor_movement' registered successfully") +except RuntimeError: + logger.debug("'record_sensor_movement' already registered") + +# Let the App runtime manage bricks and run the web server +App.run() diff --git a/examples/vibration-anomaly-detection/sketch/sketch.ino b/examples/vibration-anomaly-detection/sketch/sketch.ino new file mode 100644 index 0000000..47d24b5 --- /dev/null +++ b/examples/vibration-anomaly-detection/sketch/sketch.ino @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright (C) ARDUINO SRL (http://www.arduino.cc) +// +// SPDX-License-Identifier: MPL-2.0 + +#include +#include + +// Create a ModulinoMovement object +ModulinoMovement movement; + +float x_accel, y_accel, z_accel; // Accelerometer values in g + +unsigned long previousMillis = 0; // Stores last time values were updated +const long interval = 16; // Interval at which to read (16ms) - sampling rate of 62.5Hz and should be adjusted based on model definition +int has_movement = 0; // Flag to indicate if movement data is available + +void setup() { + Bridge.begin(); + + // Initialize Modulino I2C communication + Modulino.begin(Wire1); + + // Detect and connect to movement sensor module + while (!movement.begin()) { + delay(1000); + } +} + +void loop() { + unsigned long currentMillis = millis(); // Get the current time + + if (currentMillis - previousMillis >= interval) { + // Save the last time you updated the values + previousMillis = currentMillis; + + // Read new movement data from the sensor + has_movement = movement.update(); + if(has_movement == 1) { + // Get acceleration values + x_accel = movement.getX(); + y_accel = movement.getY(); + z_accel = movement.getZ(); + + Bridge.notify("record_sensor_movement", x_accel, y_accel, z_accel); + } + + } +} diff --git a/examples/vibration-anomaly-detection/sketch/sketch.yaml b/examples/vibration-anomaly-detection/sketch/sketch.yaml new file mode 100644 index 0000000..fc88d11 --- /dev/null +++ b/examples/vibration-anomaly-detection/sketch/sketch.yaml @@ -0,0 +1,17 @@ +profiles: + default: + fqbn: arduino:zephyr:unoq + platforms: + - platform: arduino:zephyr + libraries: + - MsgPack (0.4.2) + - DebugLog (0.8.4) + - ArxContainer (0.7.0) + - ArxTypeTraits (0.3.1) + - Modulino (0.5.0) + - Arduino_HS300x (1.0.0) + - Arduino_LPS22HB (1.0.2) + - Arduino_LSM6DSOX (1.1.2) + - STM32duino VL53L4CD (1.0.5) + - STM32duino VL53L4ED (1.0.1) +default_profile: default