From 8d08ad458faab0ca46521bfdeb6d7c1ed254fe2e Mon Sep 17 00:00:00 2001 From: grinningmosfet <95435436+grinningmosfet@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:58:09 +0200 Subject: [PATCH] first github release --- README.md | 33 +++++++++++++ tourneur.js | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 tourneur.js diff --git a/README.md b/README.md index f226b29..6414b2d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ # tourneur.js + Le Tourneur is a page turner app for Espruini. Perfectly suits musicians, translators or even cooks! + +## Operation + +### Setup + +Insert a CR2032 battery into your Puck.js, pair it with the Bluetooth device you'd like to control and you are ready to go! + +### Button interface + + - **Short** press to skip one page. (It simulates the press of the **right** keyboard key.) + - **Long** press to return one page. (It simulates the press of the **left** keyboard key.) + +The long press delay can be customized (see _Variables_ below). + +### Bilking indicators + + - **First blink**: no Bluetooth connection if **red**, skip if **green**, return if **yellow** (red + green). + - **Second blink** (red): battery level is under threshold. + +### Variables + +Som compile-time parameters are exposed. They are reported in the table below: + +| Variable | Default | Description | +|---------------------|---------|---------------------------------------------------------------------------| +| `EMPTY_BATT_THRESH` | `20` | Battery level in % below which low battery indicator turns on. | +| `LONG_PRESS_MILLIS` | `300` | Minimal press duration in milliseconds for a long press to be registered. | + +## Tips & Tricks + +Keep the virtual keyboard under Android +: As Le Tourneur acts like a keyboard, but without all the letters and numbers keys, you need to tell Android to keep showing the virtual keyboard in case you want to fill in a text field. This option exists in the Android settings. \ No newline at end of file diff --git a/tourneur.js b/tourneur.js new file mode 100644 index 0000000..2a623d3 --- /dev/null +++ b/tourneur.js @@ -0,0 +1,138 @@ +/** + * tourneur.js + * + * Le Tourneur is a page turner app for Espruini. + * + * @author Pierre "grinningmosfet" Guillod (maintainer) and contributors + * @version 1.2.0 + * + * This file is part of Le Tourneur. + * + * Le Tourneur is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * Le Tourneur is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with Le Tourneur. If not, see . + */ + +var EMPTY_BATT_THRESH = 20; +var LONG_PRESS_MILLIS = 300; + +var kb = require("ble_hid_keyboard"); +NRF.setServices(undefined, { hid : kb.report }); + +var timoutObj = null; + +/** + * btnPressed + * Callback for button press + * + * Starts a timeout that will issue + * a long press action after timing + * out. + * + * \return void + */ + +function btnPressed() { + timoutObj = setTimeout(function(){ + timoutObj = null; + try { + kb.tap(kb.KEY.LEFT,0); + ledsAnim("rev"); + } catch (error) { + ledsAnim("error"); + } + }, LONG_PRESS_MILLIS); +} + +/** + * btnReleased + * Callback for button release + * + * If the timeout us still running, + * the long press delay was not reached. + * Therefore, the timeout needs to be + * stopped and a forward action must + * be issued. + * + * \return void + */ + +function btnReleased() { + if (timoutObj) { + clearTimeout(timoutObj); + timoutObj = null; + try { + kb.tap(kb.KEY.RIGHT,0); + ledsAnim("fwd"); + } catch (error) { + ledsAnim("error"); + } + } +} + +setWatch(btnPressed, BTN, {edge:"rising", repeat:true,debounce:50}); +setWatch(btnReleased, BTN, {edge:"falling",repeat:true,debounce:50}); + +/** + * ledsAnim + * LEDs animations handler + * + * \param level: characterizes the animation <"feedback"|"error"> + * \return void + */ + +function ledsAnim(level) { + var BLACK = 0; + var RED = 1; + var GREEN = 2; + var BLUE = 4; + var WHITE = RED + GREEN + BLUE; + var YELLOW = RED + GREEN; + + switch(level) { + case "fwd": + digitalWrite([LED3,LED2,LED1],GREEN); + setTimeout(function(){digitalWrite([LED3,LED2,LED1],BLACK);}, 5); + if(isBattUnderThresh(EMPTY_BATT_THRESH)) { + setTimeout(function(){digitalWrite([LED3,LED2,LED1],RED);}, 200); + setTimeout(function(){digitalWrite([LED3,LED2,LED1],BLACK);}, 205); + } + break; + case "rev": + digitalWrite([LED3,LED2,LED1],YELLOW); + setTimeout(function(){digitalWrite([LED3,LED2,LED1],BLACK);}, 5); + if(isBattUnderThresh(EMPTY_BATT_THRESH)) { + setTimeout(function(){digitalWrite([LED3,LED2,LED1],RED);}, 200); + setTimeout(function(){digitalWrite([LED3,LED2,LED1],BLACK);}, 205); + } + break; + case "error": + digitalWrite([LED3,LED2,LED1],RED); + setTimeout(function(){digitalWrite([LED3,LED2,LED1],BLACK);}, 5); + if(isBattUnderThresh(EMPTY_BATT_THRESH)) { + setTimeout(function(){digitalWrite([LED3,LED2,LED1],RED);}, 200); + setTimeout(function(){digitalWrite([LED3,LED2,LED1],BLACK);}, 205); + } + break; + } +} + +/** + * isBattUnderThresh + * Compares battery level to arbitrary threshold. + * + * \param thresh: threshold + * \return boolean: if the current battery level is under the threshold + */ +function isBattUnderThresh(thresh) { + return E.getBattery() < thresh; +} \ No newline at end of file