├── LICENSE ├── MMM-Navigate.css ├── MMM-Navigate.js ├── MMM-Navigate_fritzing_rotaryencoder.jpg ├── MMM-Navigate_screenshot1.jpg ├── MMM-Navigate_screenshot2.jpg ├── README.md ├── node_helper.js ├── package.json └── postinstall /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ax-LED 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MMM-Navigate.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | li { 6 | list-style-type: none; 7 | padding: 0; 8 | margin: 0; 9 | } 10 | 11 | li.selected { 12 | background: grey; 13 | } 14 | 15 | 16 | li.locked { 17 | border: 2px; 18 | border-color: red; 19 | border-style: solid; 20 | } 21 | 22 | .fa-lock1:after { 23 | content: "\f023"; 24 | margin-left: 5px; 25 | font-family: "Font Awesome 5 Free"; 26 | font-weight: 900; /*900 is needed since fontawesome 5*/ 27 | } 28 | 29 | a:link { 30 | text-decoration: none; 31 | color: white; 32 | } 33 | 34 | a:visited { 35 | color: white; 36 | } 37 | 38 | a:hover { 39 | color: white 40 | } -------------------------------------------------------------------------------- /MMM-Navigate.js: -------------------------------------------------------------------------------- 1 | //MMM-Navigate.js: 2 | 3 | var locked = false; 4 | var vconfirm = 0; 5 | 6 | Module.register("MMM-Navigate",{ 7 | // Default module config. 8 | defaults: { 9 | Alias: [ 'Seite vorwärts','Seite zurück'], 10 | Action: [{type: "notification", title: 'Good morning!'},{type: "notification", title: 'Good morning!'}], 11 | GPIOPins: [26,20,19]//rotary cw, rotary ccw, rotary press (BCM Numbering) 12 | }, 13 | 14 | getStyles: function() { 15 | return [ 16 | this.file('MMM-Navigate.css'), //load css 17 | ]; 18 | }, 19 | 20 | sendAction: function(description) { 21 | this.show(0,{force: true}); 22 | 23 | if((description.notification == "SHELLCOMMAND") && (vconfirm==0)){ 24 | vconfirm = 1; 25 | this.sendNotification("SHOW_ALERT",{type:"notification",message:"Ausführen von SHELLCOMMAND "+ description.payload +" bitte durch 2.Klick bestätigen"}); 26 | }else if((description.notification == "SHELLCOMMAND") && (vconfirm==1)){ 27 | vconfirm = 0; 28 | this.sendSocketNotification(description.notification, description.payload); 29 | }else{ 30 | vconfirm = 0; 31 | this.sendNotification(description.notification, description.payload); 32 | } 33 | 34 | this.hide(10000); 35 | }, 36 | 37 | // Define start sequence. 38 | start: function() { 39 | Log.info("Starting module: " + this.name); 40 | this.sendConfig();//pass config to node_helper.js 41 | }, 42 | 43 | // Override dom generator. 44 | getDom: function() { 45 | //Div for loading 46 | if (this.loading) { 47 | var loading = document.createElement("div"); 48 | loading.innerHTML = this.translate("LOADING"); 49 | loading.className = "dimmed light small"; 50 | wrapper.appendChild(loading); 51 | return wrapper; 52 | } 53 | 54 | var self = this;//makes variables usable in functions 55 | 56 | //Div after loading 57 | var parent = document.createElement("div"); 58 | parent.className = "xsmall bright"; 59 | 60 | //build navigation from array 61 | for (let index = 0; index < this.config.Action.length; index++) { 62 | var naviItem = document.createElement("li"); 63 | var link = document.createElement('a'); 64 | link.setAttribute('href', ''); 65 | link.innerHTML = this.config.Alias[index]; 66 | naviItem.setAttribute('id', index); 67 | if(index==0){//first li gets class="selected" 68 | naviItem.setAttribute('class', 'selected'); 69 | } 70 | naviItem.appendChild(link); 71 | parent.appendChild(naviItem); 72 | } 73 | return parent; 74 | }, 75 | 76 | sendConfig: function() { 77 | this.sendSocketNotification("BUTTON_CONFIG", { 78 | config: this.config 79 | }); 80 | }, 81 | 82 | naviaction: function(payload){ 83 | var self = this; 84 | 85 | if(payload.inputtype === 'CW' || payload.inputtype === 'CCW' || payload.inputtype === 'PRESSED'){ 86 | navigationmove(payload.inputtype); 87 | } 88 | 89 | function fselectedid(){//get ID and return it 90 | for (let index = 0; index < self.config.Action.length; index++) { 91 | var test = document.getElementsByTagName('li')[index].getAttribute('class'); 92 | 93 | if(test=='selected' || test=='selected locked' || test=='selected locked fa-lock1'){//axled lock icon 94 | var selectedid = document.getElementsByTagName('li')[index].getAttribute('id'); 95 | return selectedid; 96 | } 97 | } 98 | } 99 | 100 | function navigationmove(input){ 101 | self.show(0); 102 | selectedid = fselectedid(); 103 | if(input==='CW' || input==='CCW'){ 104 | vconfirm = 0; 105 | 106 | if(input==='CW'){ 107 | navistep = 1; 108 | actionstep = 0; 109 | }else if(input==='CCW'){ 110 | navistep = -1; 111 | actionstep = 1; 112 | } 113 | 114 | if(locked==true){ 115 | self.sendAction(self.config.Action[selectedid][parseInt(actionstep)]); 116 | }else if(locked==false){ 117 | 118 | document.getElementsByTagName('li')[selectedid].setAttribute('class', '');//CW&CCW 119 | 120 | if(selectedid==0 && input==='CW'){//mark next row 121 | document.getElementsByTagName('li')[parseInt(selectedid)+1].setAttribute('class', 'selected');//CW 122 | }else if(selectedid==0 && input==='CCW'){//mark last row 123 | document.getElementsByTagName('li')[self.config.Action.length-1].setAttribute('class', 'selected');//CCW 124 | }else if(selectedid==self.config.Action.length-1 && input==='CW'){//mark first row 125 | document.getElementsByTagName('li')[0].setAttribute('class', 'selected');//CW 126 | }else if(selectedid==self.config.Action.length-1 && input==='CCW'){//mark prev row 127 | document.getElementsByTagName('li')[parseInt(selectedid)-1].setAttribute('class', 'selected');//CCW 128 | }else{//mark next one in selected direction 129 | document.getElementsByTagName('li')[parseInt(selectedid)+navistep].setAttribute('class', 'selected'); 130 | } 131 | } 132 | }else if(input === 'PRESSED'){ 133 | if(locked==false){//Menu not locked so ... (see below) 134 | if(Array.isArray(self.config.Action[selectedid])){//if selected entry Action is array - lock it 135 | locked = true; 136 | document.getElementsByTagName('li')[selectedid].setAttribute('class', 'selected locked fa-lock1');//axled lock icon 137 | }else{//if selected entry Action is object - so there is nothing to lock - execute it 138 | self.show(0,{force: true}); 139 | self.sendAction(self.config.Action[selectedid]); 140 | } 141 | }else{//Menu locked so unlock it 142 | locked = false; 143 | document.getElementsByTagName('li')[selectedid].setAttribute('class', 'selected'); 144 | } 145 | } 146 | } 147 | return parent; 148 | }, 149 | 150 | //Helper, to use module without Rotary Encoder and without GPIO Pins, like developing in Pixel VM 151 | notificationReceived: function(notification, payload) { 152 | if(notification === "CW" || notification === "CCW" || notification === "PRESSED"){ 153 | this.naviaction({inputtype: ""+ notification +""}); 154 | } 155 | 156 | if(notification === "SHELLCOMMAND"){ 157 | this.sendSocketNotification(notification, payload); 158 | } 159 | }, 160 | 161 | // socketNotificationReceived from helper 162 | socketNotificationReceived: function (notification, payload) { 163 | if(notification === 'CW' || notification === 'CCW' || notification === 'PRESSED'){ 164 | this.naviaction(payload); 165 | } 166 | }, 167 | }); -------------------------------------------------------------------------------- /MMM-Navigate_fritzing_rotaryencoder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ax-LED/MMM-Navigate/1e7cd05167961e6fe98eec5a6b08bf7967bf82f7/MMM-Navigate_fritzing_rotaryencoder.jpg -------------------------------------------------------------------------------- /MMM-Navigate_screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ax-LED/MMM-Navigate/1e7cd05167961e6fe98eec5a6b08bf7967bf82f7/MMM-Navigate_screenshot1.jpg -------------------------------------------------------------------------------- /MMM-Navigate_screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ax-LED/MMM-Navigate/1e7cd05167961e6fe98eec5a6b08bf7967bf82f7/MMM-Navigate_screenshot2.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-Navigate 2 | A module to connect a rotary encoder to MagicMirror and use it for Navigation inside of MagicMirror 3 | I wanted to use interaction to the MagicMirror and decided to use a rotary encoder, which has 3 functions: Clockwise, Counterclockwise and Press. 4 | These functions where combined to a navigation, so you have some possibilities, f.e.: Page increment/decrement, Newsfeed Article more/less details and actions for notification system. 5 | The navigation fades out, if not used. 6 | 7 | ![Magic-Mirror Module MMM-Navigate screenshot1](https://raw.githubusercontent.com/Ax-LED/MMM-Navigate/master/MMM-Navigate_screenshot1.jpg) 8 | 9 | ![Magic-Mirror Module MMM-Navigate screenshot2](https://raw.githubusercontent.com/Ax-LED/MMM-Navigate/master/MMM-Navigate_screenshot2.jpg) 10 | 11 | ## Connect rotary encoder to raspberry pi 12 | Using capacitors for CLK, DT and SW Pin can be useful for debouncing. 13 | ![Magic-Mirror Module MMM-Navigate rotary encoder](https://raw.githubusercontent.com/Ax-LED/MMM-Navigate/master/MMM-Navigate_fritzing_rotaryencoder.jpg) 14 | 15 | ## Installing the module 16 | Clone this repository in your `~/MagicMirror/modules/` folder `( $ cd ~MagicMirror/modules/ )`: 17 | ````javascript 18 | git clone https://github.com/Ax-LED/MMM-Navigate 19 | cd MMM-Navigate 20 | npm install # this can take a while 21 | ```` 22 | 23 | ## Using the module 24 | 25 | To use this module, add it to the modules array in the `config/config.js` file: 26 | ````javascript 27 | { 28 | module: "MMM-Navigate", 29 | header: "Navigation", 30 | position: "top_left", 31 | config: { 32 | Alias: [ 33 | 'Seiten blättern', 34 | 'News (mehr/weniger Details)', 35 | 'Test notification', 36 | 'News - mehr Details', 37 | 'News - weniger Details', 38 | 'Neustart MagicMirror (PM2)', 39 | 'Neustart', 40 | 'Herunterfahren' 41 | ], 42 | Action: [ 43 | [{notification:'PAGE_INCREMENT',payload:''},{notification:'PAGE_DECREMENT',payload:''}],//action array, first press locks menu, after this rotation CW/CCW executes, second press release lock mode 44 | [{notification:'ARTICLE_MORE_DETAILS',payload:''},{notification:'ARTICLE_LESS_DETAILS',payload:''}], 45 | {notification: 'SHOW_ALERT', payload: {type:'notification',message:'Dies ist eine Testnachricht'}},//single action, execute on press 46 | {notification:'ARTICLE_MORE_DETAILS',payload:''}, 47 | {notification:'ARTICLE_LESS_DETAILS',payload:''}, 48 | {notification: 'SHELLCOMMAND', payload:'pm2 restart mm'}, 49 | {notification: 'SHELLCOMMAND', payload:'sudo reboot'}, 50 | {notification: 'SHELLCOMMAND', payload:'sudo shutdown -h now'} 51 | ], 52 | GPIOPins: [26,20,19]//rotary cw, rotary ccw, rotary press (BCM Numbering) 53 | }, 54 | }, 55 | ```` 56 | On some Raspberry Pis it is neccesary to put the following line to /boot/config.txt for the GPIO where "rotary press (SW)" is connected: 57 | ```` 58 | gpio=19=ip,pu 59 | ```` 60 | Background: Sets your GPIO 19 as input (ip) and pull up (pu) 61 | Change the entry according to the GPIO pin you use. 62 | 63 | 64 | ## Configuration options 65 | 66 | The following properties can be configured: 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 |
OptionDescription
AliasAn Array of the Alias for the navigation entries.
ActionAn Array of Action of the Alias. There are two modes:
1. Execution of a single action, for Example {notification:'PAGE_INCREMENT',payload:''} to send page increment to MMM-Pages Module.
84 | 2. Execution of an array of action, as there are some actions, which belong together (like PAGE_INCREMENT and PAGE_DECREMENT), Example Config: [{notification:'PAGE_INCREMENT',payload:''},{notification:'PAGE_DECREMENT',payload:''}]
85 | Behavior: First press locks menu (can be identified by the red css frame), after this rotation CW/CCW executes actions from config, second press release lock mode so you can select another navigation entry.
GPIOPinsArray for Definition of GPIO-Pins (BMC) to connect the rotary encoder for the following actions: Clockwise, Counterclockwise and Press
SHELLCOMMANDExecutes code in a terminal of you pi, so you can do almost everything you want. Example you want to shutdown your pi; Config: 94 | {notification: 'SHELLCOMMAND', payload:'sudo shutdown -h now'}
95 |
98 | Further information:
In version 1.1 of the module, i added a 'second click confirmation notification' for the following SHELLCOMMAND.
99 | This means, if you are using SHELLCOMMAND and press the rotary, you get a notification to do a second press to execute the selected entry. 100 | 101 | ## Further options 102 | You can communicate with this module also by sending notifications. 103 | Examples:
104 | yourmmip:8080/MMM-Navigate/remote?action=NOTIFICATION¬ification=CCW emulates turning rotary counterclockwise
105 | yourmmip:8080/MMM-Navigate/remote?action=NOTIFICATION¬ification=CW emulates turning rotary clockwise
106 | yourmmip:8080/MMM-Navigate/remote?action=NOTIFICATION¬ification=PRESSED emulates pressing rotary encoder
107 | yourmmip:8080/MMM-Navigate/remote?notification=SHELLCOMMAND&action=sudo%20shutdown%20-h%20now send command to shutdown your pi 108 | 109 | ## Version 110 | 1.6 Changelog: 111 | - fix rebuild step process (thanks to sdetweil) 112 | 1.5 Changelog: 113 | - minor bugfixes 114 | 115 | 1.4 Changelog: 116 | - changed code for better recognition of the rotary encoder 117 | - changed code enables faster movement of the rotary 118 | 119 | 1.3 Changelog: 120 | - added some functions to no longer have a dependency to MMM-Remote-Control 121 | - SHELLCOMMAND 122 | - module is now listening to yourmmip:8080/MMM-Navigate/remote? 123 | 124 | 1.2 Changelog: 125 | - added lock icon next to navigation alias, if locked 126 | - code cleaned 127 | 128 | 1.1 129 | Changelog: 130 | - added ability to send notifications to MMM-Navigate by other modules 131 | - added locked mode, so you can put two(2) actions in one(1) navigation link which belong together (like PAGE_INCREMENT and PAGE_DECREMENT). More details see Configuration options (Action). 132 | - modified css, so locked mode is visual (red frame when locked) in MM 133 | - added second click confirmation notification for SHELLCOMMAND 134 | 135 | 1.0 initial release 136 | -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | /* Magic Mirror 2 | * Node Helper: {MMM-Navigate} 3 | * 4 | * By {AxLED} 5 | * {MIT} Licensed. 6 | */ 7 | 8 | //Debugging 9 | //tail -f ~/.pm2/logs/mm-out-0.log 10 | //tail -f ~/.pm2/logs/mm-error-0.log 11 | 12 | const Gpio = require('onoff').Gpio; 13 | var NodeHelper = require("node_helper"); 14 | const exec = require("child_process").exec; 15 | const url = require("url"); 16 | 17 | //Variables 18 | var lastStateCLK = 0; 19 | var lastdir = ''; 20 | 21 | module.exports = NodeHelper.create({ 22 | // Subclass start method. 23 | start: function() { 24 | var self = this; 25 | this.loaded = false; 26 | this.createRoutes(); 27 | }, 28 | 29 | intializeRotary: function() { 30 | 31 | //Rotary Code.. 32 | this.loaded = true; 33 | var self = this; 34 | 35 | console.log('MMM-Navigate, listen on GPIO PINs (BCM): '+self.config.GPIOPins[0]+','+self.config.GPIOPins[1]+','+self.config.GPIOPins[2]); 36 | const CLK = new Gpio(self.config.GPIOPins[1], 'in', 'both',{debounceTimeout : 0 }); //BCM Pin 20 37 | const DT = new Gpio(self.config.GPIOPins[0], 'in', 'both',{debounceTimeout : 0 }); //BCM Pin 26 38 | const SW = new Gpio(self.config.GPIOPins[2], 'in', 'both',{debounceTimeout : 20 }); //BCM Pin 19 39 | 40 | CLK.read(function (err, value) { 41 | if (err) { 42 | throw err; 43 | } 44 | this.lastStateCLK = value; 45 | this.a = value; 46 | }); 47 | 48 | DT.read(function (err, value) { 49 | if (err) { 50 | throw err; 51 | } 52 | this.b = value; 53 | }); 54 | 55 | CLK.watch(function (err, value) { 56 | if (err) { 57 | throw err; 58 | } 59 | this.a = value; 60 | }); 61 | 62 | DT.watch(function (err, value) { 63 | if (err) { 64 | throw err; 65 | } 66 | this.b = value; 67 | tick(); 68 | }); 69 | 70 | SW.watch(function (err, value) { 71 | if (err) { 72 | throw err; 73 | } 74 | if(value == 0){ 75 | self.sendSocketNotification('PRESSED',{inputtype: 'PRESSED'}); 76 | } 77 | }); 78 | 79 | function tick() { 80 | const { a, b } = this; 81 | 82 | if (a != lastStateCLK && a == 1){//only do action, if rotary was moved and only count one step 83 | if (b != a){ 84 | self.sendSocketNotification('CW',{inputtype: 'CW'}); 85 | lastdir = 'CW'; 86 | } else { 87 | self.sendSocketNotification('CCW',{inputtype: 'CCW'}); 88 | lastdir = 'CCW'; 89 | } 90 | } 91 | 92 | //catch missing count when changing from CCW to CW 93 | if (a == lastStateCLK && b == 0 && lastdir == 'CCW') { 94 | self.sendSocketNotification('CW',{inputtype: 'CW'}); 95 | lastdir = 'CW'; 96 | } 97 | 98 | lastStateCLK = a; 99 | return this; 100 | } 101 | 102 | }, 103 | 104 | // Override socketNotificationReceived method. 105 | socketNotificationReceived: function(notification, payload) { 106 | if (notification === 'BUTTON_CONFIG') { 107 | this.config = payload.config; 108 | 109 | if (this.loaded === false) {//AxLED 2020-04 110 | this.intializeRotary(); 111 | } 112 | 113 | } 114 | 115 | if (notification === 'SHELLCOMMAND') { 116 | console.log("MMM-Navigate, received Shellcommand:", payload); 117 | exec(payload, null); 118 | } 119 | }, 120 | 121 | createRoutes: function() { 122 | var self = this; 123 | 124 | this.expressApp.get("/MMM-Navigate/remote", function(req, res) { 125 | var query = url.parse(req.url, true).query; 126 | if(query.notification=='CW' || query.notification=='CCW' || query.notification=='PRESSED'){ 127 | self.sendSocketNotification(query.notification,{inputtype: ""+query.notification+""}); 128 | } 129 | res.send("MMM-Navigate, data received: "+ JSON.stringify(query)); 130 | }); 131 | }, 132 | }); 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MagicMirror-Module-Navigate", 3 | "version": "1.6.0", 4 | "description": "A module to connect a rotary encoder for MM Navigation", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/Ax-LED/MMM-Navigate" 8 | }, 9 | "main": "MMM-Navigate.js", 10 | "author": "AxLED", 11 | "license": "MIT", 12 | "scripts": { 13 | "postinstall": "./postinstall" 14 | }, 15 | "dependencies": { 16 | "onoff": "latest" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f ../../node_modules/.bin/electron-rebuild ]; then 4 | cd ../.. 5 | npm install electron-rebuild >/dev/null 2>&1 6 | cd - 7 | fi 8 | ../../node_modules/.bin/electron-rebuild --------------------------------------------------------------------------------