├── 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 |  8 | 9 |  10 | 11 | ## Connect rotary encoder to raspberry pi 12 | Using capacitors for CLK, DT and SW Pin can be useful for debouncing. 13 |  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 |
Option | 73 |Description | 74 |
---|---|
Alias |
79 | An Array of the Alias for the navigation entries. | 80 |
Action |
83 | An 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. |
86 |
GPIOPins |
89 | Array for Definition of GPIO-Pins (BMC) to connect the rotary encoder for the following actions: Clockwise, Counterclockwise and Press | 90 |
SHELLCOMMAND |
93 | Executes 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 | |
96 |
SHELLCOMMAND
.yourmmip:8080/MMM-Navigate/remote?action=NOTIFICATION¬ification=CCW
emulates turning rotary counterclockwiseyourmmip:8080/MMM-Navigate/remote?action=NOTIFICATION¬ification=CW
emulates turning rotary clockwiseyourmmip:8080/MMM-Navigate/remote?action=NOTIFICATION¬ification=PRESSED
emulates pressing rotary encoderyourmmip: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
--------------------------------------------------------------------------------