From a27eef0a3e1ce85b0cbfa910ed5d3098cb8adc49 Mon Sep 17 00:00:00 2001 From: jeffyactive Date: Fri, 9 Aug 2024 17:44:15 -0400 Subject: [PATCH] Added Knob Button app for Puck.js (v2) --- apps.json | 12 +++++ apps/knobbutton/ChangeLog | 1 + apps/knobbutton/README.md | 26 ++++++++++ apps/knobbutton/app.js | 103 ++++++++++++++++++++++++++++++++++++++ apps/knobbutton/icon.png | Bin 0 -> 1282 bytes 5 files changed, 142 insertions(+) create mode 100644 apps/knobbutton/ChangeLog create mode 100644 apps/knobbutton/README.md create mode 100644 apps/knobbutton/app.js create mode 100644 apps/knobbutton/icon.png diff --git a/apps.json b/apps.json index 18342c7..76e53e0 100644 --- a/apps.json +++ b/apps.json @@ -277,5 +277,17 @@ "storage": [ {"name":".bootcde","url":"app.js"} ] + }, + { "id": "knobbutton", + "name": "Knob Button", + "icon": "icon.png", + "version":"0.01", + "description": "Use the Puck.js (v2) as an anywhere knob: push the button to transmit the angle of rotation in BLE advertising packets.", + "tags": "bluetooth", + "readme": "README.md", + "needsFeatures":["BLE","ACCEL"], + "storage": [ + {"name":".bootcde","url":"app.js"} + ] } ] diff --git a/apps/knobbutton/ChangeLog b/apps/knobbutton/ChangeLog new file mode 100644 index 0000000..5560f00 --- /dev/null +++ b/apps/knobbutton/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/knobbutton/README.md b/apps/knobbutton/README.md new file mode 100644 index 0000000..778003d --- /dev/null +++ b/apps/knobbutton/README.md @@ -0,0 +1,26 @@ +# Knob Button + +Use the Puck.js (v2) as an on-demand knob: rotate to the desired angle and press the button. The angle of rotation will be advertised to any Bluetooth Low Energy receivers in range. + +## Usage + +Load the app onto the Puck.js (v2) and then: +- hold the Puck.js in a vertical position (ex: against a wall) +- rotate to the desired angle +- press the button +- observe the green LED flash +- observe BLE advertising packets with the angle of rotation data for ~5 seconds + +## How it works + +The Puck.js (v2) will wake on button press and read the accelerometer. The angle of rotation will be calculated based on the accelerometer's X-axis and Y-axis readings. The value is advertised as a JSON string in a manufacturer-specific data packet using the Espruino company code (0x590), for example: + + {angleOfRotation:123} + +[Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source IoT middleware will automatically interpret these packets using its [advlib-ble-manufacturers](https://github.com/reelyactive/advlib-ble-manufacturers) library which supports Espruino advertising packets. + +Following a button press sequence, the Puck.js (v2) will return to low-power sleep, waking again on any subsequent button press. It will also periodically advertise the name "Knob.js". + +## Adapt the code + +See the reelyActive's [Puck.js Development Guide](https://reelyactive.github.io/diy/puckjs-dev/) to load the source code in the Espruino IDE and adapt it to meet your needs! diff --git a/apps/knobbutton/app.js b/apps/knobbutton/app.js new file mode 100644 index 0000000..a95191f --- /dev/null +++ b/apps/knobbutton/app.js @@ -0,0 +1,103 @@ +/** + * Copyright reelyActive 2022-2024 + * We believe in an open Internet of Things + */ + + +// User-configurable constants +const LED_BLINK_MILLISECONDS = 50; +const STABLE_ACCELERATION_TOLERANCE_G = 0.1; +const ANGLE_ADVERTISING_DURATION_MILLISECONDS = 5000; +const ANGLE_ADVERTISING_PERIOD_MILLISECONDS = 500; +const NAME_ADVERTISING_PERIOD_MILLISECONDS = 5000; + + +// Non-user-configurable constants +const ACC_SAMPLE_RATE_HZ = 12.5; // Valid values are 1.6, 12.5, 26, 52, 104, 208 +const ACC_PER_G = 8192; +const DEG_PER_RAD = 180 / Math.PI; + + +// Global variables +let advertisingTimeoutId; + + +// Calculate the angle of rotation based on the given accelerometer reading +function calculateAngleOfRotation(acc) { + let ratioXY = ((acc.y === 0) ? Infinity : Math.abs(acc.x / acc.y)); + let ratioYX = ((acc.x === 0) ? Infinity : Math.abs(acc.y / acc.x)); + + if((acc.x >= 0) && (acc.y >= 0)) { + return Math.round(Math.atan(ratioYX) * DEG_PER_RAD); + } + if((acc.x <= 0) && (acc.y >= 0)) { + return Math.round(90 + (Math.atan(ratioXY) * DEG_PER_RAD)); + } + if((acc.x <= 0) && (acc.y <= 0)) { + return Math.round(180 + (Math.atan(ratioYX) * DEG_PER_RAD)); + } + if((acc.x >= 0) && (acc.y <= 0)) { + return Math.round(270 + (Math.atan(ratioXY) * DEG_PER_RAD)); + } +} + + +// Advertise the name "Knob.js" +function advertiseName() { + NRF.setAdvertising({}, { + showName: false, + manufacturer: 0x0590, + manufacturerData: JSON.stringify({ name: "Knob.js" }), + interval: NAME_ADVERTISING_PERIOD_MILLISECONDS + }); +} + + +// Advertise the angle of rotation for a specific period +function advertiseAngleOfRotation(angleOfRotation) { + if(advertisingTimeoutId) { + clearTimeout(advertisingTimeoutId); + } + + NRF.setAdvertising({}, { + showName: false, + manufacturer: 0x0590, + manufacturerData: JSON.stringify({ angleOfRotation: angleOfRotation }), + interval: ANGLE_ADVERTISING_PERIOD_MILLISECONDS + }); + + advertisingTimeoutId = setTimeout(advertiseName, + ANGLE_ADVERTISING_DURATION_MILLISECONDS); +} + + +// Handle a button press: blink green LED and initiate accelerometer readings +function handleButton() { + Puck.accelOn(ACC_SAMPLE_RATE_HZ); + LED2.write(true); + setTimeout(function() { LED2.write(false); }, LED_BLINK_MILLISECONDS); +} + + +// Handle accelerometer reading: terminate accelerometer readings and advertise +// angle of rotation once magnitude is stable +function handleAcceleration(data) { + let magnitude = Math.sqrt((data.acc.x * data.acc.x) + + (data.acc.y * data.acc.y) + + (data.acc.z * data.acc.z)) / ACC_PER_G; + let isStableMagnitude = (magnitude < 1.0 + STABLE_ACCELERATION_TOLERANCE_G) && + (magnitude > 1.0 - STABLE_ACCELERATION_TOLERANCE_G); + + if(isStableMagnitude) { + let angleOfRotation = calculateAngleOfRotation(data.acc); + + Puck.accelOff(); + advertiseAngleOfRotation(angleOfRotation); + } +} + + +// Advertise "Knob.js", wake on button press and handle accelerometer readings +advertiseName(); +Puck.on('accel', handleAcceleration); +setWatch(handleButton, BTN, { edge: "rising", repeat: true, debounce: 50 }); \ No newline at end of file diff --git a/apps/knobbutton/icon.png b/apps/knobbutton/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1d1968c3e511b18ecbf5e6e8dd2c5d218321199 GIT binary patch literal 1282 zcmV+d1^xPoP)n-!TRQX2+cmwcU(0 zGhVPgf4N*UQmge(cdONsliu<8_*gENx3{-fR#xWc=VxbU|9S9GC=_U<3jxH!kzK%r z4e4Nlqobq6#YLTdx`~-qSUWOHOG^ytiCI^K?(grnwzhN*W01?`^68-0`0R&C>WLH<>mA9^Lo7=ilzf$ zhG4TOE&fp+iZ+o<=-}XBYHCW^s8vwW6O||!$Z)fkn(FZIFqHF1AY^RRFl%dTJuQK&FMz&CQL5u(C$oW*i@dU`U6K2ewYH~Hqo>F=X_{3$)lqf*J5KR(OnIH55a)85O=OEZ!#>Q_QEsd;efU}G7K%Xpm75BZ_JOh<{LMk01; zarjF!Av6Ezq02z03a$^3vy1I&}8W<*J@{4?d$_M!{ zDx)PSMSHKWugz<0d5-W_LbP@)R{B9zs&~>@+fgtnn>SvA17)D2sL%kSNC;)Kf~_^y zxZ_C)!7dcc3KY2b2M%l_POB4KNF5joW(Bqm4I|(*CFX~`o3@87G_2#MJ@UTv2?etP z>Bb5YrwM?6cKc8;D-bP8h<`_Z)S#y12nDkOrHBSKTZMy%RUM&VR$BE9ij&iviiVq)DMG(>@1{p`@o^GDMuvjQQd}sh^5`xTY`C(y% z-MYLI7!;!Zf|+PNL{Vm5%QzbmH0CdgHL;qSR?S8{WM(8LVri?tY>hVv>D5BcpAW6x z9yZv{)jZ5=>#0GAQc^Qy-e3G=u-tTU>CuclWTo@lL6OSMA_>vk+gs}zI8biRnrS^v zx18;xNs=r+q&$gQs=_&c%f?(mmC-^7yHPh9Cx+R9a!^1aYHg`gA_nK27DD1rxEOQB z-*c&*$ew}ByMZbc8XH0LW-Np&qk}cZYD0TyeqXf0)DLy+fza$V6f7=Cp z<&^hj3a`ChFe>z#QQc%!7=aPDjUt(l_|u8rO)Dog6?%`4j}yNv4#YV085LdOIe(gH zbKXiNq|gfrGnLX}0uC;?fL>NC6F)Y=D~)s^fP(@mymq`==|D)l`zbMV%^O{#rOeWx z0!Ah8Pc_{HVrN1MTu#DZCZna8O@j5RRT!8SF>!QD$iUL&TQ%gBTpY9$I*7gFvY=-| s1_HBIt5qr$HojvI9(bj(P0V)0e;l)JEgk?80000007*qoM6N<$f?9i6*8l(j literal 0 HcmV?d00001