-
• #2
1. For a first step, we develop the module code and usage in single file TemperatureDevTest.js:
The code below is working and covers basically the above requirements for the module:
// TemperatureDevTest.js // 'class' (Prototype definition) var Temperature = function // module (class), for multiple instances ( name // name hinting room / location of sensor\ , oneWire // one-wire; for example: new OneWire(B8); , addr // address on the one-wire (required for multiples) , interval // interval in milliseconds of measurements , preferred // optional, unit - "C" or "F" (default and not F is C) ) { // set givens this.name = name; this.oneWire = oneWire; this.addr = addr; this.interval = interval; this.preferred = (preferred === "F") ? "F" : "C"; this.enabled = (typeof enabled === "undefined") || enabled; this.t = (this.preferred==="C") ? 0 : 32; // set current to 'frozen' this.intervalId = null; // later also used as indicator for enabled this.sensor = null; // laster also used as indicatore for connected // get going this.sensor = require("DS18B20").connect(this.oneWire); this.intervalId = setInterval(function(_this) { _this.sensor.getTemp(function(t) { _this.t = t; }); }, this.interval, this); }; Temperature.prototype.getTemp = function ( unit // optional, unit "F" or "C", default "C" ) { var u = (typeof unit === "undefined") ? this.preferred : (unit === "F" ) ? "F" : "C" ; var d = (u === "F") ? (this.t * 1.8 + 32) + " Fahrenheit" : this.t + " Celsius"; return d; }; // usage // setup oneWire var oneWire = new OneWire(B8); // setup 1st temperature sensor ts1 var ts1 = new Temperature ( "office" // name / location of temperatre sensor 1 , oneWire , null // addr on one-wire currently not implemented / used , 5000 // every 5 secs make a read (to keep it not booring) , "F" // preferred unit is Fahrenheit ); // for sample's sake, do everhthing deferred for more // than 5 seconds in order to have value(s) to display setTimeout(function(){ // get temp in preferred and ad-hoc specified units console.log(ts1.getTemp()); console.log(ts1.getTemp("C")); },6000);
The output - in the console - is as expected (code unrelated: unusually hot... a heat wave... usually it is around 50 F or even less this early in the morning...)
_____ _ | __|___ ___ ___ _ _|_|___ ___ | __|_ -| . | _| | | | | . | |_____|___| _|_| |___|_|_|_|___| |_| http://espruino.com 1v94 Copyright 2016 G.Williams > =undefined 83.1875 Fahrenheit 28.4375 Celsius >
-
• #3
2. For the second step, we want to use the code like a module.
*** EDIT No 'hands-on' for this 2nd step as outlined in posts #3 thru #7. Following it in thoughts and understanding the concepts is sufficient. Hands-on is again asked for post #8 and beyond. ***
To use the code like a module, in other words, use the
require(moduleName)
, we have to shove the code as module into theModules
cache: see [espruino.com/Reference#Modules]http://www.espruino.com/Reference#Modules). TheModules
is a global JavaScript object that holds on to the uploaded modules. The modules are uploaded, stored and retrieved by the 'moduleName' (also called 'moduleId'), which is at the same time the file name for local and official modules, and the url for a module from anywhere of the web.Usually, the upload retrieves the module from either the local sandbox, the official espruino.com/modules library of the Web, or any other place of the Web by supplying the full url as 'moduleName'. How code and modules are loaded is touched on in the conversation about simple explanation how to save code that espruino run on start?.
The same means that upload uses to upload modules to the board are available to us in either the console or the already uploaded to the board. The
Modules
as global JavaScript object has behavior - methods / functions - which we can use to store a module. The method looks like this in the complete JavaScript expression:Modules..addCached(id, sourcecode)
, whereid
is themoduleName
as string andsourcecode
is the complete source code as a single string.To make the code work like a module (on retrieval or add and retrieval), we must add
exports = objectToBeExported
(or ...toBeExposed) at the end of the the code. The objectToBeExported is for most modules a simple object with one method / function connect, which uses module code to connect to and sometimes initialize the device the module is all about. The nice thing about this export / expose approach is to intentionally make to the application available only what is needed and hide and protect other things. Hiding thins also avoids naming conflicts between application and modules and among modules that would make use and reuse of modules a very difficult not to say impossible task.Looking at some existing modules, you find that export does this - for example, for the DS18B20 Temperature Sensor:
... exports.connect = function(oneWire, device) { return new DS18B20(oneWire, device);
The
exports
object is already there and sometimes the module adds to the object the properties / methods / functions it wants to export/expose or it set the exports object all together to point to a new object. In 'DS18B20' case, the.connect(...)
method is added which uses the particular module code for implementation. In our 'class' / prototype case, we make the 'exports' point to the 'class' / prototype function, which is the constructor function that we will use in the application withnew
:... exports = Temperature;
With the module code now complete, we have to feed it into the
Modules
cache as a single string. There are multiple ways to do that as described in conversation about Module creation at runtime:- most obvious is to make each line of the source a string and concatenate them all...
- use ES6 multi-line strings that are supported by Espruino.
Making the source code lines all string and concatenate them is though quite some work and will need more work when further down the development process the will be stored as separate module file. Therefore we will use the ES 6 multi-line string to achieve the change very quickly:
- before the first line of module code we add
Modules.addCached("Temperature",backTick
- after the last line we add
backTick);
...where 'backTick' is the backwards-looking single quote (`), also called (in French) 'accent-grave'.
Putting backTicks around multiple lines makes JavaScript to look at it these a single string ignoring the line ends (CR / CRLF / LF). Using this string in the
Modules.addCached(...)
methods as parameter, Espruino does not anymore look at it as code that is to be processed (compiled, interpreted, and executed). Latter is a major shift in regard how and when module code is looked at as code. So far, it was looked at as code right on arrival at Espruino on upload. backTicked, it is looked at as plain string on arrival on the board and is looked at as code and processed as such (compiled) when stored inModules
cache.Note: When the module code is very large and so the source as string will be too, you may ran out of memory... But to that, there is a solution as shown in the next post.
Last change is in the application part where we now have to retrieve the the
Temperature
'class'/prototype definition as module instead of direct reference the already available constructor function. Since - for what ever reason... = new require("Temperature")(...)
construct is not working in Espruino, we add an extra line just before the line with ...new...':var Temperature = require("Temperature");
.Since
Temperature
is a 'class'/prototype, it is appropriate to name/spell it so. If it would be another module, for exmple the one for the 'DS18B20' temperature sensor, we would name it lower case beginning likevar ds18b20Mod = require("DS18B20");
and later usevar tempSensor = ds18b20Mod.connect(...);
to ('create' a sensor and) connect to it.Even though we are done with 'modularizing', it is not good enough: Since we do not go through the usual upload process that parses any required module for nested 'requireds', our module will fail on usage. Why? Answer: From where should Espruino know that it should have retrieved the module(s) referenced in the module as well?
Solution to the missing nested module 'DS18B20' is to upload the referenced module(s) 'upfront' - if known at that time - or retrieved and added to the cache 'now' dynamically as well, either from included code or code read from 'somewhere', such as SD Card... or the Web.
For our simple example - and because we know what will be used, we add this
require("DS18B20");" to the code... almost as a dummy,... but because it is in the active, uploaded code (and not in a string), it is detected at the beginning of the upload and retrieved and uploaded by the uploader (...may be @Gordon may change this and let pass strings in the code as well through the recursive module detector and uploader. Until then, we just do it in our application code... and it will not hurt even when the function becomes available). It does not really matter where we place this
require("DS18B20");` - before of after the dynamically loading of the module or somewhere else. Useful is to put it close, and I decided it to put it just before. I will have to remove it when the module will be completed and statically availableThe final code looks like below... where the
Temperature
'class'/prototype definition is handled and used as a module:// TemperatureDevTest2.js require("DS18B20"); // used by the dynamically loaded module Modules.addCached("Temperature",` // Temperature.js // 'class' (Prototype definition) var Temperature = function // module (class), for multiple instances ( name // name hinting room / location of sensor\ , oneWire // one-wire; for example: new OneWire(B8); , addr // address on the one-wire (required for multiples) , interval // interval in milliseconds of measurements , preferred // optional, unit - "C" or "F" (default and not F is C) ) { // set givens this.name = name; this.oneWire = oneWire; this.addr = addr; this.interval = interval; this.preferred = (preferred==="F") ? "F" : "C"; this.enabled = (typeof enabled === "undefined") || enabled; this.t = (this.preferred==="C") ? 0 : 32; // set current to 'frozen' this.intervalId = null; // later also used as indicator for enabled this.sensor = null; // laster also used as indicatore for connected // get going this.sensor = require("DS18B20").connect(this.oneWire); this.intervalId = setInterval(function(_this) { _this.sensor.getTemp(function(t) { _this.t = t; }); }, this.interval, this); }; Temperature.prototype.getTemp = function ( unit // optional, unit "F" or "C", default "C" ) { var u = ((typeof unit !== "undefined") && (unit === "F")) ? "F" : "C"; var d = (u === "F") ? (this.t * 1.8) + " Fahrenheit" : this.t + " Celsius"; return d; }; exports = Temperature; `); // usage // setup oneWire var oneWire = new OneWire(B8); // setup 1st temperature sensor ts1 var Temperature = require("Temperature"); var ts1 = new Temperature ( "office" // name / location of temperatre sensor 1 , oneWire , null // addr on one-wire currently not implemented / used , 5000 // every 5 secs make a read (to keep it not booring) , "F" // preferred unit is Fahrenheit ); // for sample's sake, do everhthing deferred for more // than 5 seconds in order to have value(s) to display setTimeout(function(){ // get temp in preferred unit console.log(ts1.getTemp()); },6000);
It works as expected and shows in the console:
_____ _ | __|___ ___ ___ _ _|_|___ ___ | __|_ -| . | _| | | | | . | |_____|___| _|_| |___|_|_|_|___| |_| http://espruino.com 1v94 Copyright 2016 G.Williams > =undefined 31.375 Celsius >
Pretty hot...is it! - I mean also literally...
- most obvious is to make each line of the source a string and concatenate them all...
-
• #4
As mentioned in the previous post, the string containing a complete module can be pretty big and could lead to run out of memory. Therefore the question:
How can a module be broken up into small(er) pieces and composed at runtime from such pieces.
In order to have the a piece of code working as a module, it is good enough to have a 'hock object' as the first or core component in the
Modules
cache to which more components can 'be attached' to. To attache components to a module core component, the module has to be required from the cache and then the components can 'be attached' to it. After all, the module - in this case the 'class'/prototype definition - is just a function object - one and the same whether in the cache or in the application code.Since the constructor function is such a first and core component, let's try to attache a the
.getTemp()
as a component.Intentionally, the module was retrieved into a temporary variable
tempTemp
- line 37 - to prove that the module is the very same object that is later retrieved into the original variableTemperature
- line 57 - and includes now the complete Temperature 'class'/prototype definition.The split up module code into core component - lines 4..34 - and other component(s) - lines 49/41..48 - and attaching the other component(s), such as a method method - line 40 - represents composing a complete module from components component by component. The code for it looks like this:
// TemperatureDevTest2.js require("DS18B20"); // used by the dynamically loaded module Modules.addCached("Temperature",` // Temperature.js // 'class' (Prototype definition) var Temperature = function // module (class), for multiple instances ( name // name hinting room / location of sensor\ , oneWire // one-wire; for example: new OneWire(B8); , addr // address on the one-wire (required for multiples) , interval // interval in milliseconds of measurements , preferred // optional, unit - "C" or "F" (default and not F is C) ) { // set givens this.name = name; this.oneWire = oneWire; this.addr = addr; this.interval = interval; this.preferred = (preferred==="F") ? "F" : "C"; this.enabled = (typeof enabled === "undefined") || enabled; this.t = (this.preferred==="C") ? 0 : 32; // set current to 'frozen' this.intervalId = null; // later also used as indicator for enabled this.sensor = null; // laster also used as indicatore for connected // get going this.sensor = require("DS18B20").connect(this.oneWire); this.intervalId = setInterval(function(_this) { _this.sensor.getTemp(function(t) { _this.t = t; }); }, this.interval, this); }; exports = Temperature; `); // require the Temperature module into a temporary tempTemp var: var tempTemp = require("Temperature"); // attach the .getTemp() method tempTemp.prototype.getTemp = function ( unit // optional, unit "F" or "C", default "C" ) { var u = ((typeof unit !== "undefined") && (unit === "F")) ? "F" : "C"; var d = (u === "F") ? (this.t * 1.8) + " Fahrenheit" : this.t + " Celsius"; return d; }; // usage // setup oneWire var oneWire = new OneWire(B8); // setup 1st temperature sensor ts1 var Temperature = require("Temperature"); var ts1 = new Temperature ( "office" // name / location of temperatre sensor 1 , oneWire , null // addr on one-wire currently not implemented / used , 5000 // every 5 secs make a read (to keep it not booring) , "F" // preferred unit is Fahrenheit ); // for sample's sake, do everhthing deferred for more // than 5 seconds in order to have value(s) to display setTimeout(function(){ // get temp in preferred unit console.log(ts1.getTemp()); },6000);
There is one comment to make: if the to-attach module component is retrieved over the network or read from a storage it is retrieved as a string and not as an object... but that is no issue what so ever... we just 'ab-use' the
Modules.addCached(...)
andrequire(...)
infrastructure as a stepping stone:- add the to-attach component to the
Modules
cache as a module withModules.addCached(...)
- lines 36..49. - retrieve the to-attach component as module from the
Modules
cache withrequire()
- line 55. - attach it to the core module retrieved from the
Modules
cache as done before - line 55.
The code with all module components as strings looks then like:
// TemperatureDevTest2.js require("DS18B20"); // used by the dynamically loaded module Modules.addCached("Temperature",` // Temperature.js // 'class' (Prototype definition) var Temperature = function // module (class), for multiple instances ( name // name hinting room / location of sensor\ , oneWire // one-wire; for example: new OneWire(B8); , addr // address on the one-wire (required for multiples) , interval // interval in milliseconds of measurements , preferred // optional, unit - "C" or "F" (default and not F is C) ) { // set givens this.name = name; this.oneWire = oneWire; this.addr = addr; this.interval = interval; this.preferred = (preferred==="F") ? "F" : "C"; this.enabled = (typeof enabled === "undefined") || enabled; this.t = (this.preferred==="C") ? 0 : 32; // set current to 'frozen' this.intervalId = null; // later also used as indicator for enabled this.sensor = null; // laster also used as indicatore for connected // get going this.sensor = require("DS18B20").connect(this.oneWire); this.intervalId = setInterval(function(_this) { _this.sensor.getTemp(function(t) { _this.t = t; }); }, this.interval, this); }; exports = Temperature; `); // add .getTemp() method as moduleComponent to the Modules cache Modules.addCached("moduleComponent",` var moduleComponent = function ( unit // optional, unit "F" or "C", default "C" ) { var u = ((typeof unit !== "undefined") && (unit === "F")) ? "F" : "C"; var d = (u === "F") ? (this.t * 1.8) + " Fahrenheit" : this.t + " Celsius"; return d; }; exports = moduleComponent; `); // require the Temperature module into a temporary tempTemp var: var tempTemp = require("Temperature"); // require and attach the .getTemp() method as module component tempTemp.prototype.getTemp = require("moduleComponent"); // usage // setup oneWire var oneWire = new OneWire(B8); // setup 1st temperature sensor ts1 var Temperature = require("Temperature"); var ts1 = new Temperature ( "office" // name / location of temperatre sensor 1 , oneWire , null // addr on one-wire currently not implemented / used , 5000 // every 5 secs make a read (to keep it not booring) , "F" // preferred unit is Fahrenheit ); // for sample's sake, do everhthing deferred for more // than 5 seconds in order to have value(s) to display setTimeout(function(){ // get temp in preferred unit console.log(ts1.getTemp()); },6000);
...and works - and we get two (2) warnings about module not found on upload as expected... of course it runs... no magic... just JavaScript brilliantly and to it's true nature implemented by @Gordon.
- add the to-attach component to the
-
• #5
Two final challenges remain to resolve - even though they are not part of the overall topic of this conversation:
See something like this at runtime where the core of the application - internet connectivity - is uploaded to the board and the rest of the application is pulled in dynamically over the Web in the start-up of the application.
See an application at runtime replacing a module on predefined expiration or server notification over the Web by shutting down the module, replacing it with the new, updated version over the Web, and restarting it. Depending on the dependencies, it may be easier to just restart the whole application, and the solution of challenge 1. solves the 'logical' challenge 2.
But what we have shown so far is how we can develop even large module(s) within the same source file as the application through the module(s)'s early stages.
Next post returns back to the topic and focus of this conversion: Module development(process).
-
• #6
Looks good - I'd say if you're running into out of memory errors, you should probably just do what is described in the first step - without using
Modules.addCached
.Pulling modules over the internet is very possible - it's actually quite easy (download as a string, and use
Modules.addCached
). The problem is that it needs to be asynchronous, andrequire
is synchronous - so it can't be built in as part of the standardrequire
function.For replacing a module, you'd have to have a carefully designed module that could fully unload itself - and that you kept no references to. However if that is done then yes, replacing it on the fly could definitely be done.
-
• #7
The problem is that it needs to be asynchronous, and require is synchronous
...with that comment we come full circle with #4 and #5 of the conversation about How to create a module that is a 'Class' - aka a function usable with new - or: How to make require() return a function and not an object.
A nice historical event...
At that time you posted '...That one's definitely not in my plans at all I'm afraid!'. With promises now implemented, I'm not so sure this statement of your's still stands firm... ;-) ---
*** Espruino is now closer to the require.js / AMD as it ever was... and kind of an M2M Browser Eco System vs. a Web Browser... ***
(Just a little bit food for thought: enabling require to detect the invocation pattern/method signature - single string vs. array - would allow the async approach, of course it also requires some flanking support actions, like a way to define how and where to pull the modules asynchronously. This food for thought is though a rabbit trail of this conversation and will have to be in its own conversation).
-
• #8
In the related conversation about Module creation at runtime I learned about a superior solution for step 2:
Modules.addCached(...)
accepts the module code also as a function, something like this:Modules.addCached("MyObject", function(){ // MyObject.js // Code of MyObject module implementing MyObject class/prototype. var MyObject = function(instanceName) { // constructor this.instanceName = instanceName; this.creationTimestamp = new Date().getTime(); } MyObject.prototype.getInfo() { return this.instanceName + " created at " + this.creationTimestamp; } exports = MyObject; });
Lines 2..11 represent the exact code that will become the 'outfactored' module. Keeping the module code as 'active' code in the wrapping, anonymous function passed into
.addCached(..)
keeps the code visible for the Espruino Web IDE editor and with that we get syntax coloring and code the Espruino specific code completion and easier debug-ability. With code as string none of these feature are available to the developer.The final code using this more useful approach looks then like this:
// TemperatureDevTest2.js require("DS18B20"); // used by the dynamically loaded module Modules.addCached("Temperature", function() { // Temperature.js // 'class' (Prototype definition) var Temperature = function // module (class), for multiple instances ( name // name hinting room / location of sensor\ , oneWire // one-wire; for example: new OneWire(B8); , addr // address on the one-wire (required for multiples) , interval // interval in milliseconds of measurements , preferred // optional, unit - "C" or "F" (default and not F is C) ) { // set givens this.name = name; this.oneWire = oneWire; this.addr = addr; this.interval = interval; this.preferred = (preferred==="F") ? "F" : "C"; this.enabled = (typeof enabled === "undefined") || enabled; this.t = (this.preferred==="C") ? 0 : 32; // set current to 'frozen' this.intervalId = null; // later also used as indicator for enabled this.sensor = null; // laster also used as indicatore for connected // get going this.sensor = require("DS18B20").connect(this.oneWire); this.intervalId = setInterval(function(_this) { _this.sensor.getTemp(function(t) { _this.t = t; }); }, this.interval, this); }; Temperature.prototype.getTemp = function ( unit // optional, unit "F" or "C", default "C" ) { var u = ((typeof unit !== "undefined") && (unit === "F")) ? "F" : "C"; var d = (u === "F") ? (this.t * 1.8) + " Fahrenheit" : this.t + " Celsius"; return d; }; exports = Temperature; }); // usage // setup oneWire var oneWire = new OneWire(B8); // setup 1st temperature sensor ts1 var Temperature = require("Temperature"); var ts1 = new Temperature ( "office" // name / location of temperatre sensor 1 , oneWire , null // addr on one-wire currently not implemented / used , 5000 // every 5 secs make a read (to keep it not booring) , "F" // preferred unit is Fahrenheit ); // for sample's sake, do everhthing deferred for more // than 5 seconds in order to have value(s) to display setTimeout(function(){ // get temp in preferred unit console.log(ts1.getTemp()); },6000);
Lines 3..42 will become the - un-minified - source of the module file placed into the modules folder of the sandbox (or any other module repository).
-
• #9
3. For the third step, we tidy up the module code with some (extra) documentation.
It is very useful to include in the module code - as comment - some terse documentation about the usage of the module, see example in DS18B20 temperature sensor module. The embedded module documentation/information is at the beginning in block comment.
The usage example is taken from current code. The code to be and to be used as a module -
lines 5..65 - looks like this:// TemperatureDevTest2.js require("DS18B20"); // used by the dynamically loaded module Modules.addCached("Temperature", function() { /* Copyright (c) 2017 ... */ /* Module to wrap application of DS18B20 temp sensor module in Temperature class ` ` ` var oneWire = new OneWire(B8); var Temperature = require("Temperature"); var ts1 = new Temperature ( "office" // name / location of temperatre sensor 1 , oneWire , null // addr on one-wire currently not implemented / used , 5000 // every 5 secs make a read (to keep it not booring) , "F" // preferred unit is Fahrenheit ); var temp = 0; setTimeout() { function() { var tempTemp = ts1.getTemp(); // get temperature,... if (tempTemp != temp) { // ... if changed from last time,... temp = tempTemp; // ... hold on to it and... console.log(new Date() + ": " + temp; // ...print it;... } }, 10000); // ...every ten seconds // ...do this every 10 seconds ` ` ` */ // Temperature 'class'/prototype definition var Temperature = function // module (class), for multiple instances ( name // name hinting room / location of sensor\ , oneWire // one-wire; for example: new OneWire(B8); , addr // address on the one-wire (required for multiples) , interval // interval in milliseconds of measurements , preferred // optional, unit - "C" or "F" (default and not F is C) ) { // set givens this.name = name; this.oneWire = oneWire; this.addr = addr; this.interval = interval; this.preferred = (preferred==="F") ? "F" : "C"; this.enabled = (typeof enabled === "undefined") || enabled; this.t = (this.preferred==="C") ? 0 : 32; // set current to 'frozen' this.intervalId = null; // later also used as indicator for enabled this.sensor = null; // laster also used as indicatore for connected // get going this.sensor = require("DS18B20").connect(this.oneWire); this.intervalId = setInterval(function(_this) { _this.sensor.getTemp(function(t) { _this.t = t; }); }, this.interval, this); }; Temperature.prototype.getTemp = function ( unit // optional, unit "F" or "C", default "C" ) { var u = ((typeof unit !== "undefined") && (unit === "F")) ? "F" : "C"; var d = (u === "F") ? (this.t * 1.8) + " Fahrenheit" : this.t + " Celsius"; return d; }; exports = Temperature; }); // usage // setup oneWire var oneWire = new OneWire(B8); // setup 1st temperature sensor ts1 var Temperature = require("Temperature"); var ts1 = new Temperature ( "office" // name / location of temperatre sensor 1 , oneWire , null // addr on one-wire currently not implemented / used , 5000 // every 5 secs make a read (to keep it not booring) , "F" // preferred unit is Fahrenheit ); // for sample's sake, do everhthing deferred for more // than 5 seconds in order to have value(s) to display setTimeout(function(){ // get temp in preferred unit console.log(ts1.getTemp()); },6000);
While developing a module, it is convenient in early stages to have all code in one and the same file, that is:
For a module example, I want to have a module that
This is good enough for now, because:
The focus of this conversation is less on the module and its functions but more so on the development process within the Espruino Eco System - IDE, Project, Sandbox, Module Compiler (js minifier), etc.
Subsequent post will walk you through the steps to build a module for yourself... and may be for others too.