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 0000000..c1d1968 Binary files /dev/null and b/apps/knobbutton/icon.png differ