Read the statement by Michael Teeuw here.
How to create your own module? For beginners, by a beginner
-
Hello everyone! For an internship, I had to develop a custom module for the Magic Mirror, whilst having no experience whatsoever with it, nor with NodeJS, JS or HTML for that matter. Thanks to the numerous forum posts and available modules, I was able to develop my module in just a couple of days. As a way to say thank you, I wanted to share my ‘How to write your own module?’ for beginners, where I address the topics that were not very clear to me in the beginning.
Unfortunately, I cannot share my module (called MMM-PaintMe), because of the adaptations that I did for our specific Mirror installation, hence rendering it useless for anyone else. Additionally, it requires the full strength of a Jetson Nano to run (at a mere 4FPS), so it would probably not be interesting for most.
Maybe also important (as it is not mentioned in the document) is that our Mirror uses a page system, with different modules being shown on each page. Also, it’s quite important that you understand what ‘this’ and ‘self’ mean in programming, so if you don’t, be sure to look it up first. But I think that’s pretty much all you need to know to be able to understand the guide.
I hope this post can help some people to get started with their own module. To be honest, once you get the hang of it, it’s really not that difficult and you can do pretty much anything you want.
Enjoy the read:
How to write your own module?
This tutorial on how to make your own module should be seen as an extended summary of The Core module file, Head first developing MM module for extreme beginners, How to develop modules and Python and the Magic Mirror. Notice that they are all coming from the Magic Mirror forum or documents. You will probably get most of your information through these channels.
How to search things on Google?
Developing anything is all about being able to search for the right things on Google. The first thing that you should know, is that the Magic Mirror modules are written in a custom wrapper of NodeJS, which in turn is a wrapper of JavaScript and can be a wrapper of HTML. That means that sometimes the syntax can be difficult, and you need to watch out what programming language you’re in when looking up answers.
If you have a question about a specific function, put
magic mirror
in front of your Google query. If you didn’t find an answer there, you can always try withnodejs
. For example, the node_helper is a NodeJS package, but it is not used that often, whilst it is a standard practice in developing Magic Mirror modules.If you have specific code questions, look them up with
nodejs
first, and if you’re out of luck, try again with justjs
. If you have questions about anything related to the DOM (more on that later), look them up withhtml
in your query.Communication in the Mirror
The most important part to understand is communcication. There are two types of communication within the Mirror: intermodule communication and intramodule communication.
Intermodule communication
Different modules can communicate with each other through ‘notifications’. They can send notifications through the
sendNotification
function, and they can listen for notifications via thenotificationReceived
function. The Mirror itself also sends out some notifications in the beginning, which you can use to find out when your module has been started up and rendered. More on those notifications can be found here.For example, the Face-Reco-DNN module works closely together with the Face-Multi-User-Recognition-SMAI module, and manages to pull it off thanks to notifications. When the DNN module detects a person, it send a notification with the username in it. Specifically, the code look like this (literally copied):
if (loginCount > 0) { this.sendNotification("USERS_LOGIN", payload.users); }
A notification consists of a ‘title’ (in the example above it’s ‘USERS_LOGIN’) and a ‘payload’. The title is used for filtering in an ‘if’ or ‘case’ statement, and then the payload is used as actual data. For example, the code at SMAI looks like this:
notificationReceived: function(notification, payload, sender) { var self = this; switch (notification) { case "USERS_LOGIN": { if (this.config.useMMMFaceRecoDNN === true && this.loggedIn == false ) { Log.log("Notification: " + notification + " from Mirror. Logging in " + payload); // Fetch the users image. this.loggedIn = true; this.userName = payload; this.userImage = payload + ".jpg"; //Assume for now. self.updateDom(100); } break; //skip the other cases } //more cases, obviously } },
That’s pretty readable, right? The first thing that you should notice, is the use of
var self = this;
. It’s quite self-explanatory: store the pointer to your current object in a new variable called ‘self’. But it does not make a lot of sense: why not just use ‘this’ all the time? Well, I don’t actually have an answer to that, but it appears that the value of ‘this’ changes sometimes throughout functions. However, strangely, you see that just before the call toupdateDom
(more info on that later), he still uses ‘this’! I don’t think the ‘self’ is necessary here, but I’d say there are two ways to go about coding:- Always use self instead of this
- Always use this, and fix it where it breaks
I went for the second approach, because it is cleaner than always defining a new variable. However, do as you please!
Intramodule communication
The second type of communication is intramodule communication. And then I’m not talking about shared variables, that’s too easy.
Generally speaking, a Magic Mirror module consists of two files: the main file
ModuleName.js
and a helper file callednode_helper.js
. The main file is responsible for rendering the module on screen (with the use of a DOM, more on that later). The helper helps the main file by doing things the main file cannot do, such as running a Python script. When browsing through the different modules that exist, you will see that often thenode_helper.js
is missing. This is because the helper is optional, and should be avoided when all calculations can be done in the main file.If you have decided that you will indeed use a node_helper, you will need intramodule communication between the main file and the node_helper. This is done with so called ‘socket notifications’. The functions are
socketNotificationReceived
andsendSocketNotification
.For example, the MMM-PaintMe module uses the node_helper to set up and run a Python script. The code looks like this:
//MMM-PaintMe.js start: function () { this.sendSocketNotification("CONFIG", this.config); Log.log("Starting module: " + this.name); },
//node_helper.js socketNotificationReceived: function (notification, payload) { // Configuration are received if (notification === "CONFIG") { this.config_main = payload; //store the config of the main app if (!this.pythonStarted) { this.python_start(); } } //some more code },
When the main module is started, the
start()
function is called. This sends the module’sconfig
to the node_helper. The node_helper can then store this data, and use it to get arguments, such as image width and height, which it passes on to the Python script when calling it.Intermezzo: What is the
config
, you ask? It’s the configuration file of a module, and every module has it. It is how modules are meant to be setup, and you can change these values in theMagicMirror/config/config.js
file. In the module’s main file, you can also set somedefaults
for the configuration.Why you’ll probably want a node_helper: for some weird reason, the main file cannot directly print anything to the terminal screen, only the node_helper can. The main file can make logs, which are visible when you run
npm start dev
instead ofnpm start
. The code for these logs was already shown in the example, and looks like this://MMM-PaintMe.js Log.log("Starting module: " + this.name);
Check out Logger | MagicMirror² Documentation for more info, though it seems incorrect to state that it replaces
console.log
, as you don’t see any output in normal starting conditions.The node helper can print directly to the terminal. To do so, simply write:
//node_helper.js console.log(message);
How to show anything on the screen?
Showing stuff on the screen requires you to make a DOM. From Wikipedia:
The Document Object Model (DOM) is a cross-platform and language-independent interface that treats an XML or HTML document as a tree structure wherein each node is an object representing a part of the document. The DOM represents a document with a logical tree.
Basically, you write HTML code without literally writing HTML code. Let’s look at an example, the (slightly redacted)
getDom
function fromMMM-PaintMe
:getDom: async function () { const { image, isError } = await loadImage( "modules/MMM-PaintMe/tools/loading.gif" + "?" + new Date().getTime() ); //loading screen image if (!isError) { // If the image loaded, show it var wrapper = document.createElement("div"); wrapper.className = this.config.classes ? this.config.classes : "thin medium grey pre-line"; //Set image image.width = this.config.width.toString(); image.height = this.config.height.toString(); image.id = "IMG"; wrapper.appendChild(image); //Set text (for loading screen and for style) // Create a span to hold it all const text = document.createElement("span"); text.style.textAlign = "center"; text.style.display = "block"; //place text below img text.id = "loadingtxt"; text.innerHTML = "Loading Paint Me module..."; wrapper.appendChild(text); return wrapper; } //some more code },
If you know HTML, this code is pretty easy to read. Instead of literally writing your HTML code, you use Object-Oriented programming in a tree-like structure to get your stuff done. If you don’t know HTML, don’t worry! I didn’t either! When you want to know anything (like ‘How do I show text on my screen?’), just look it up and start your query with
html
. Easy peasy! Or just look at other modules and see how they did it, that works pretty well too.What you’ll basically want to do, is create a single
div
. This can be seen as the ‘body’ of your module output. Then, you can add things to yourdiv
.The DOM will always be a static output, unless you use the
updateDom
function. As an argument to that function, you can insert the fading speed in milliseconds (most use 1000). As far as I can tell,updateDom
simply reruns thegetDom
function. If instead oftext.innerHTML = "Same constant string";
, you would usetext.innerHTML = this.randomStringGeneratorFunction();
, it is obvious that you will see a different text on your screen every time the DOM is regenerated.However, there is a second way. A better way. Since calling
updateDom
will callgetDom
, which is meant to generate your HTML code as if nothing existed yet, you will always be generating new elements to render on-screen, which is obviously less efficient than changing already existing elements. The way to do the latter, is by settingid
’s to your elements, just like in the example. (As you can tell, I’m a fan of this second way)To update what you see on your screen, you can use the following code (copied from
MMM-PaintMe.js
):notificationReceived: function (notification, payload, sender) { //notifications from MM and other modules var self = this; switch (notification) { //all dom objects created, so we can start changing our text case "DOM_OBJECTS_CREATED": var timer = setInterval(function () { if (self.loading) //only give loading text when it's actually loading document.getElementById("loadingtxt").innerHTML = self.getText(); }, self.config.updateIntervalText); break; } },
You can tell I was kind of scared to use ‘this’ here, but stop judging. Now, to the code. The notification “DOM_OBJECTS_CREATED” is one of the three sent out by the Mirror. Specifically, it says that all modules had their chance to give their first DOM, so that means that the Mirror is ready to receive DOM updates.
The
setInterval
function is a simple infinite loop function with a waiting time, determined by the second argument. You’ll probably be using this sometime.Now, how do we update our DOM with this code? Remember those
id
’s that we set in thegetDom
function? We can use them to retrieve our element and make changes to it, such as changing itsinnerHTML
to a text-generating function. Super easy, and efficient! The only downside is that we don’t get the cool fading effect that you would get withupdateDom
. Yeah, that sucks, I know, but you have to make some hard sacrifices every now and then.Stopping/restarting hidden/visible modules
With the Hello-Lucy module, you can manage which modules should be shown on which page and which ones should be hidden. However, the Mirror is a bit lazy here: he still keeps the hidden modules running, so that he can show them at all times. Overall, this has a minor impact, since most modules are very lightweight.
However, the MMM-PaintMe module is definitely heavyweight, as it utilizes the GPU for 100% and occupies around 2GB of RAM. Additionally, it needs to use the webcam, which is already in use by Face-Reco-DNN, which loads a lot faster since it is lighter. A webcam can only be accessed by a single thread (unless you do some magic of course).
So, since we are not wizards and we do not like to run heavy modules in the background, we need to find a fix. Luckily, the Mirror provides some functions for this, after an issue was filed.
The two functions that you can use, are
suspend
andresume
. The example below shows how to use them://MMM-Face-Reco-DNN.js //will be called when the module is hidden suspend: function () { Log.log(this.name + ' is suspended.'); this.sendSocketNotification('SUSPEND', 'Face-Reco-DNN suspended'); }, //will be called when the module is shown resume: function () { Log.log(this.name + ' is resumed.'); this.sendSocketNotification('RESUME', 'Face-Reco-DNN resumed'); },
If you have been paying attention, you should know that we’re not done yet. We have sent a socket notification, so we need to change our node_helper to receive it and do something with it. The resulting code looks like this:
//node_helper.js socketNotificationReceived: function (notification, payload) { // Configuration are received if (notification === 'CONFIG') { //DO SOMETHING IN THE BEGINNING } else if (notification === 'SUSPEND') { if (pythonStarted) { this.python_stop(); console.log('[' + this.name + '] ' + 'Hidden, so stopped node_helper'); } } else if (notification === 'RESUME') { if (!pythonStarted) { this.python_start(); console.log('[' + this.name + '] ' + 'Visible, so restarted node_helper'); } } },
This means that the Python code will be stopped whenever the module is hidden, and restarted whenever the module is visible again. By shutting down the Python script, we are releasing the webcam, and so there will not be a problem as long as Face-Reco-DNN and MMM-PaintMe are not visible on the same page. Note how important it is that your Python script can get up-and-running as fast as possible. For example, in the case of Face-Reco-DNN, the proper choice of a OpenCV Video backend can save you up to 10 seconds of webcam start-up time!
Final comments
That was basically all. Sure, there are some more aspects left, like how to run a Python script, but just take a look at the code from MMM-PaintMe, it’s pretty self-explanatory. And otherwise, you can take a look at python-shell - npm.
The best thing you can do when developing a module is trying to find a module that does something similar to what you want to do. For example, when developing the MMM-PaintMe module, there were two main aspects: running a Python script and displaying an image. I found two totally unrelated modules that each did one aspect of what I wanted to do (this one for Python and this one for displaying an image). Mix them together, and you have MMM-PaintMe! There are tons of modules out there, so I’m sure you can find one that fits your needs. And if you really have a hard time finding anything, you can always ask a question on the MagicMirror Forum, the people there are super helpful! Good luck!
Oh, and one more thing. If you ran into some issues and managed to solve them with great effort, keep in mind that the person who comes next, will probably face the same issues and go through the same troubles. Help each other out and keep your problems and solutions in a log. When the whole problem has been solved and you have some time, write down your methodology in a README or another Markdown file, like I did. Sure, I spent a couple hours on this file, but I like to think I was able to save you some more time and, more importantly, some frustration. I wrote this guide with StackEdit, which is a super useful online Markdown editor, so now you don’t have an excuse to not do it anymore. You’re welcome!
THE END
I ment for this post to be a beginner’s guide, as there aren’t many of those around. So if you see anything incorrect or if you want to clear things up, be sure to correct me!