How to access a JSON object?

Posted on
  • My goal is to calculate sensor readings and then save them to a JSON object then console.log output a specific value. I'm not sure why I'm not able to access my json value. I keep getting error:

    Uncaught Error: Field or method "restempvalue" does not already exist, and can't create it on undefined
     at line 153 col 10
    sensorObj.restempvalue = val;
             ^
    

    code:

    var sensorReadings = { "restempvalue" : 0, "phvalue" : 0, "ecvalue" : 0 };
    var sensorObj = JSON.parse(sensorReadings);
    
    function getAllSensorData() {
      /*restemp.takeReading(function(val) {
        console.log(val);
      });
    
      ph.takeReading(function(val) {
        console.log(val);
      });*/
    
      ec.takeReading(function(val) {
        sensorObj.restempvalue = val;
        console.log(val);
      });
    }
    
  • sensorReadings is already an object... and JSON.parse(aJSONString) expects a string as argument... ;-), therefore, expression var sensorObj = JSON.parse(sensorReadings); throws error (in Browser: ...Uncaught SyntaxError: Unexpected token o in JSON at position 1(…); what error is thrown in Espruino I cannot figure out right now, because have no Espruino (or emulator) at hand).

    This code (see second line)

    var sensorReadings = { "restempvalue" : 0, "phvalue" : 0, "ecvalue" : 0 };
    var sensorObj = JSON.parse(JSON.stringify(sensorReadings­));
    

    fixes your issue, but there is not point to first build a JSON string from sensorReadings object and second - right away - parse that JSON string back into an object... ;-)

    Plain assignment gets you there - because it is the same:

    var sensorObj = sensorReadings;
    

    Therefore, you can drop sensorReadings completely and just use:

    var sensorObject = { "restempvalue" : 0, "phvalue" : 0, "ecvalue" : 0 };
    

    It is a different story though when you want to write you readings onto a SD memory card, or transmit to wherever. For that you would

    A) for initialization

    1. create a sensorReadings object
    2. stringify the object using 'JSON.stringify() to a sensorReadingsAsString string
    3. write or send that sensorReadingsAsString string to the storage;

    B) for update that readings on the storage, you

    1. retrieve them from storage as sensorReadingsAsString
    2. parse it into the `sensorReadings' object
    3. update the desired property with value
    4. stringify the object using 'JSON.stringify() to a sensorReadingsAsString string
    5. write or send that sensorReadingsAsString string 'back' to the storage
  • ...with that said - and to come full circle - your initial code is missing only two (2) single quotes in the first line...

    var sensorReadings = '{ "restempvalue" : 0, "phvalue" : 0, "ecvalue" : 0 }';
    var sensorObj = JSON.parse(sensorReadings); // Object from JSON
    function getAllSensorData() {
      /*restemp.takeReading(function(val) {
        console.log(val);
      });
      ph.takeReading(function(val) {
        console.log(val);
      });*/
      ec.takeReading(function(val) {
        sensorObj.restempvalue = val;
        console.log(val);
      });
    }
    

    Note: JSON is always a String (object)... JS Object Notated (as a string). It is the literal representation of an object, like the literal representation of a simple string is always in "DoubleQuotes" or 'SingleQuotes', or like literal number, for example 6, is just the 'naked' number, or like the literal booleans true and false ('naked' symbols / reserved words), or the null and undefined object null and undefined (also 'naked' symbols / reserved words)... versus the variable referencing - or value holder - of an object, string, boolean, number, null and undefined respectively.

  • @allobjects.. Ok, i think I get it. How do I access sensorObject to gather the values when using code:

    var sensorObject = { "restempvalue" : 0, "phvalue" : 0, "ecvalue" : 0 };
    

    Nevermind... I read over your posts again.. I was just a bit confused, before. Thanks for the informative response.

  • Properties - value (state) and function (behavior) properties - of a JavaScript object ("object" === typeof objVariable) can be accessed in two ways: static and dynamic.

    Example to access a value property - read and 'write' (assign):

    1. static get and set access:

      
      2. dynamic get and set access:
      
      

      var phPropName = "phValue"; ph = sensorObject[phPropName]; sensorObject[phPropName] = 7.0;```

    Same static (hard coded) and dynamic (by variable for property name) access goes for functions (behavior). The example below uses the SensorReading prototype based object-oriented approach to have multiple (maxNumOfLastReadings) sensorReading s (objects / instances of SensorReading prototype / class) stored in sensorReadings singleton object (singleton - only one instance exists). Two sensorObjects read values, create a reading with the read values and store it in the readings in different intervals.

    var sensorModule = require("sensorModuleName");
    
    // constructor for a reading accepting a sensor id and read restTemp, ph and ec
    var SensorReading = function(id, restTemp, ph, ec) {
      this.ts = getTime(); // set current system time stamp in seconds
      this.id = sensorId; // set sensor id
      this.restTemp = restTemp; // set restTemp
      this.ph = ph; // set ph
      this.ec = ec; // set ec
    }
    
    // constructor for a sensor (prototype - or class - definition)
    var SensorObject = function(sensorId, sensorModule, connectInfo1,... ) {
      this.sensorId = sensorId;
      this.sensor = sensorModule.connect(connectInfo1, conectInfo2,...);
    }
    // method (function / behavior) to take a reading and store it in readingsObject
    SensorObject.prototype.readAndStoreIn = function(readingsObject) {
      var restTemp = this.sensor.read("temp"); // ...code detail depends on the module
      var ph = this.sensor.read("ph"); // ...code detail depends on the module
      var ec = this.sensor.read("ec"); // ...code detail depends on the module
      var reading = new SensorReading(this.sensorId, restTemp, ph, ec); // create a reading
      readingsObject.store(reading);
    };
      
    // literal 'constructor' for sensorReadings singleton object
    var sensorReadings =
    { maxNumOfLastReadings: 10 // keep max 10 readings
    , readings: [] // most recent reading is in readings[0]
    , store: function(reading) {
        if (this.readings.length >= this.maxNumOfLastReadings) { // chop oldest off
          this.readings = this.slice(0, this.maxNumOfLastReadings - 1); }
        this.readings.splice(o, 0, reading);
      }
    , clearReadings: function() { this.readings = []; }
    , getReadings: function() { return [].concat(this.readings); }
    , getReadingByIndex: function(x) { // return null if x 'outside' of readings
        return (((x>=0)&&(x<this.readings.length)) ? this.readings[x] : null); }
    , getLastReading: function() { // return most recent reading
        return this.getReadingByIndex(0); }
    , getReadingsAsJSON: function() { return JSON.stringify(this.readings); }
    , getReadingsAsHtmlTableString() {
        h  = '<table><tr><th>#</th>';
        h += '<th>ts</th><th>id</th>';
        h += '<th>restTemp</th><th>ph</th><th>ec</th>­</tr>';
        this.readings.forEach(function(r, x) { 
          h += '<tr><td>'+x+'</td>';
          h += '<td>'+r.ts+'</td><td>'+r.id+'</td>';
          h += '<td>'+r.restTemp+'</td><td>'+r.ph+'</td­><td>'+r.ec+'</td></tr>';
        }); h += '</table>';
        return h;
      }
    , getReadingsAsHtmlTableString2() { // dynamic access in loop
        var ps = ["ts","id","restTemp","ph","ec"];
        h  = '<table><tr><th>#</th>';
        ps.forEach(function(p){ h += '<th>'+p+'</th>'; });
        h += '</tr>';
        this.readings.forEach(function(r, x) { 
          h += '<tr><td>'+x+'</td>';
          ps.forEach(function(p){ h += '<th>'+r[p]+'</th>'; });
          h += '</tr>';
        }); h += '</table>';
        return h;
      }
    }
    
    var sensorAtLocation1 = new SensorObject("LocInt",<connect parmsInt>,...); 
    var sensorAtLocation2 = new SensorObject("LocOut",<connect parmsOut>,...); 
    setTimeout(function(){ 
        sensorAtLocation1.readAndStoreIn(sensorR­eadings);
      }, 15000); // every 15 seconds
    setTimeout(function()setTimeout(function­(){
        sensorAtLocation2.readAndStoreIn(sensorR­eadings);
      }, 45000); }, 5000); first time after 50 seconds, after that every 45 seconds
    

    Interval settings produce readings at 15, 30, 45, 50, 60, 75, 90, 95, 105, 120, 135, 140,... seconds (italics are LOC1 readings, bold are LOC2 readings).

    Complete above code with sensor specifics, upload / run it, and then ask readingsObjects for different things - (first) one line after the other:

    readingsObject.getLastReading();
    readingsObject.getReadings();
    readingsObject.getReadingByIndex(4);
    readingsObject.getReadingsAsJSON();
    readingsObject.getReadingsAsHtmlTableStr­ing2();
    ...
    readingsObject["getLastReading"]();
    var getReadingsMethodName = "getReadings";
    readingsObject[getReadingsMethodName]();­
    var rx = readingsObject["getReadingByIndex"](4);
    var ps = ["ts","sensorId","restTemp","ph"];
    if (rx) ps.forEach(function(p){ console.log(p+"="+rx[p]); });
    readingsObject["getReadingsAsJSON"]();
    ...
    

    PS:

    • code not fully tested (yet)
    • in 1v88, '<td>${r[p]}</td>' can replace '<th>'+r[p]+'</th>' in line 59
  • @allObjects Thanks for that. I get it :-) I really like the flow of your code example/explaination.

  • For what it's worth here is my code that I hacked together before you responded a second time to my my original post. Its rather messy and I could probably do this a different way especially change the nested setTimeouts. The reason why I used setTimouts is to wait before I gather the other sensor data. On average, it takes 300ms to process a "read command" then I can issue a a "readFrom". On some occasions, depending on the sensor, it can take 1300ms to process a "read command" before I can issue an I2C "readFrom". The nested timeouts allows me to:

    First, send a read command to the temp sensor then take the temp
    value and update the ph and ec temperature compensation json data.
    Also, save the temp value to a json object.

    Second, send a temp compensation update command to both the ph and
    ec sensor. It takes 300ms for each of the ph and ec sensors to process
    that command.

    Third, send a ph read command and wait 300ms to execute an i2c
    readFrom the ph sensor to gather the data. Also, save the ph value
    to a json object.

    Fourth, send an ec read command and wait 1300ms to execute an i2c
    readFrom the ec sensor to gather the data. Also, save the ec value
    to a json object.

    After all that I send the JSON object via serial to another device.

    Another reason for my nested timeouts was, well atleast I thought, If I send a read command to both the ph and ec sensors at the the same time(after their temp compensations was updated), thought the i2c would crash or timeout...

    I'll have to read through your post and attempt to implement the information into my code.

    I2C1.setup({scl:B8, sda:B9});
    
    Serial2.setup(9600/*baud*/);
    
    var resTempVal = 0;
    var phVal = "";
    var ecVal = "";
    
    var ready = true;
    var alwaysTempCompensate = false;
    
    var sensorReadings = { restempvalue : 0, phvalue : 0, ecvalue : 0 };
    
    //console.log(sensorReadings.phvalue);
    
    function Sensor(id, type, address, buffer) {
        this.sensorName = id;
        this.sensorType = type;
        this.sensorAddress = address;
        this.sensorResultBuff = buffer;
        //this.sensorReading = "";
        this.sensorRequireTempComp = false;
    
        this.sensorResTempValue = 0;
    
        this.resTempCal = {
          "clear" : { "cmd" : "Cal,Clear", "wait" : 300  },
          "one"   : { "cmd" : "Cal,n",     "wait" : 1300 },
          "query" : { "cmd" : "Cal,?",     "wait" : 300  }
        };
    
        this.phCal = {
          "clear" : { "cmd" : "Cal,Clear",      "wait" : 300  },
          "mid"   : { "cmd" : "Cal,mid,7.00",   "wait" : 1300 },
          "low"   : { "cmd" : "Cal,low,4.00",   "wait" : 1300 },
          "high"  : { "cmd" : "Cal,high,10.00", "wait" : 1300 },
          "query" : { "cmd" : "Cal,?",          "wait" : 300  }
        };
    
        this.ecCal = {
          "clear" : { "cmd" : "Cal,Clear",  "wait" : 300  },
          "dry"   : { "cmd" : "Cal,dry",    "wait" : 2000 },
          "one"   : { "cmd" : "Cal,one,n",  "wait" : 1300 },
          "low"   : { "cmd" : "Cal,low,n",  "wait" : 1300 },
          "high"  : { "cmd" : "Cal,high,n", "wait" : 1300 },
          "query" : { "cmd" : "Cal,?",      "wait" : 300  }
        };
    
        this.cmdTable = {
          "Calibrate" : {  //List of Calibration commands and timeout value.
          },
          "Information" : {  //Device Information
          },
          "LED" : {  //Enable / Disable or Query the LEDs
            "L0" : { "cmd" : "L,0", "wait" : 300 },
            "L1" : { "cmd" : "L,1", "wait" : 300 },
            "L?" : { "cmd" : "L,?", "wait" : 300 }
          },
          "Reading" : {  //Takes a single reading
            "R" : { "cmd" : "r", "wait" : 1000 } //Takes a single temperature compensated reading
          },
          "Serial"      : {  //Switch back to UART mode
          },
          "Sleep"       : {  //Enter low power sleep mode
          },
          "Status"      : {  //Retrieve status information
          },
          "Temperature" : {  //Set or Query the temperature compensation
            "T"  : { "cmd" : "T,19.5", "wait" : 300 },  //Where the temperature is any value; floating point, or int, in ASCII form
            "T?" : { "cmd" : "T,?",   "wait" : 300 }  //Query the set temerature
          },
          "Factory"     : {  //Factory reset
          },
        };
    
        if (type == "temp") {
          this.cmdTable.Calibrate = this.resTempCal;
          var celsiusVal = 0;
        }
    
        if (type == "ph") {
          this.cmdTable.Calibrate = this.phCal;
          this.sensorRequireTempComp = true;
        }
    
        if (type == "ec") {
          this.cmdTable.Calibrate = this.ecCal;
          this.sensorRequireTempComp = true;
        }
    }
    
    Sensor.prototype.updateTempCompJSON = function (temp) {
      //console.log("Temp Comp: " + temp);
      this.cmdTable.Temperature.T.cmd = "T," + temp;
    };
    
    Sensor.prototype.updateTempComp = function () {
      var a = this.sensorAddress;
      var cmd = this.cmdTable.Temperature.T.cmd;
    
      I2C1.writeTo(a, cmd);
    };
    
    Sensor.prototype.takeReading = function(callback) {
      var a = this.sensorAddress;
      var c = this.cmdTable.Reading.R.cmd;
      var w = this.cmdTable.Reading.R.wait;
      var b = this.sensorResultBuff;
    
      var d = "";
      var sensorStatus = "";
      var sensorValue = "";
    
      var captureSensorData = false;
    
      I2C1.writeTo(a, c);
    
      setTimeout(function () {
        d = I2C1.readFrom(a, b);
    
        if (d.length > 0) {
          var dContent = d[0];
    
          switch (dContent) {
            case 1:
              sensorStatus = 1;
              captureSensorData = true;
              break;
            case 255:
              sensorStatus = 255;
              break;
            case 254:
              sensorStatus = 254;
              break;
            case 2:
              sensorStatus = 2;
              break;
          }
    
          if (captureSensorData === true) {
            for (i = 1; i < d.length; i++) {
              sensorValue += String.fromCharCode(d[i]);
            }
          }
        }
    
        captureSensorData = false;
    
        callback(sensorValue);
      }, w);
    };
    
    var restemp = new Sensor("RES TEMP", "temp", 77, 9);
    var ph = new Sensor("PH", "ph", 78, 9);
    var ec = new Sensor("EC", "ec", 99, 14);
    
    function getAllSensorData() {
      setTimeout(function () {
        restemp.takeReading(function(val) {
          var f = parseFloat(val).toFixed(1);
          var celsiusVal = (5/9) * (f-32);
    
          //console.log(celsiusVal);
    
          sensorReadings.restempvalue = f;
    
          setTimeout(function () {
            ph.updateTempCompJSON(celsiusVal.toFixed­(1));
            ph.updateTempComp();
          }, 1000);
    
          setTimeout(function () {
            ec.updateTempCompJSON(celsiusVal.toFixed­(1));
    
            ec.updateTempComp();
          }, 2000);
          console.log("RES TEMP READING: " + sensorReadings.restempvalue);
        });
      }, 100);
    
      setTimeout(function () {
        ph.takeReading(function(val) {
          var f = parseFloat(val).toFixed(1);
          sensorReadings.phvalue = f;
          console.log("PH READING: " + sensorReadings.phvalue);
        });
      }, 4000);
    
      setTimeout(function () {
        ec.takeReading(function(val) {
          var f = val;
          sensorReadings.ecvalue = f;
          console.log("EC READING: " + sensorReadings.ecvalue);
        });
      }, 5300);
    }
    
    setInterval(function () {
      //console.log("Taking Reading: ");
      getAllSensorData();
    }, 10000);
    
  • Great description and coding. To recap - in a nutshell:

    1. There are sensor nodes / agents
    2. A sensor node includes / agent handles different types of sensors: temp, ph, ec,...
    3. On (interval) request, the sensor node / agent gets a value from each sensor, sticks them in a readings object and transmits this readings object as JSON over a serial connection to a collector.

    Now, there are some pertinent specifics, such as that:

    • calibrations need to happen before readings can be taken
    • some sensors need other sensor (reading (s) for calibration / calibration which requires a particular sequence by which (some) sensors are operated
    • each type of sensor has a different set or profile of commands to perform its duty.
    • the icing on the cake is that the sensors need some time to process a command -such as for calibration - before a next command can be issued - such get a reading.

    So far so good. - sounds abstract... but most likely not to you sick you touched on all these items in the code (if I'm close).

    2 cents object-oriented analysis and design...

    For each 'thing' (object) in the description we will distill an interface (class, protocol) out of the description. A good techniques is the CRC (index) card analysis, design and validation 'game' to get every thing ready to punch out robust, requirements covering implementation / code (some of that you already have in one form or the other).

    CRC card means to have a card for each 'thing' that names the Class it belongs (the type it is), identifies what all it has to know about itself and what all it has to be able to do - Responsibilities - and whit whom - Collaborators - it needs to work with in order to be able to to fullfill the responsibilities:

    CRC = Class - Responsabilites - Colaborators

    For example, for a 'thing' like me and you, which we call aPerson, because we are an instant (or member) of the Class Person, (basic) Responsibilities are to know first an last name, date of birth (dob), to provide full name - which is first with last name, formal name - which is first's initial and last name, and last but not least, for example, provide / calculate the age in years. For the first four (5) responsibilities we need no other 'thing' - *** Collaborators*** to work with, for the last one though, we need another 'thing', namely aDate thing that knows today's date / year, and that 'thing' is an instance of the Class Date. Noteworthy is that he 'things' we have to know about are already as well instances of Classes, such as aString of String for the name business, and aDate of Date for dob, which both come with responsibilities that come in handy.

    The above text does 'just fine'... but is quite exhausting and not easy to work with on the green table with the business user... therefore the structured CRC index cards, of which we have at least two: one for Class Person and one for Class Date, and for the sake of the game we can also have on for the String class. Luckily, only for the Person class we have to implement, because JavaScript comes with the other two built in - and many more.

    CRC - Class, Responsibilities, Collaborators - cards:
    
    .---------------------------------------­------.
    | Person Class                                |
    |=======================================­======|
    | Responsibilities:          | Collaborators: |
    | - new Person() (create)    |                |
    | - firstname (String)       |                |
    | - lastname (String)        |                |
    | - dateOfBirth (Date)       |                |
    | - getFullName()            |                |
    | - getFormalName()          |                |
    | - getAgeInYears()          | - Date         |
    '---------------------------------------­------'
    
    .---------------------------------------­------.
    | Date Class                                  |
    |=======================================­======|
    | Responsibilities:          | Collaborators: |
    | - new Date() (create today)| (system time)  |
    | - getFullYear()            |                |
    | - ...                      |                |
    '---------------------------------------­------'
    
    .---------------------------------------­------.
    | String Class                                |
    |=======================================­======|
    | Responsibilities:          | Collaborators: |
    | - length                   |                |
    | - charAt(position)         |                |
    | - substr(fromPos, count)   |                |
    | - ...                      |                |
    '---------------------------------------­------'
    

    In an analysis session you sit around the table with empty cards or stand around a flip chart stickies and build together these CRC cards. To verify, run the business use cases / scenarios and each participant holding the card makes sure that responsibilities a fulfillable and collaborators are complete.

    CRC is not really new - actually in IT terms - stony old, but therefore and rock solid AND actual again: agile and CRC. Above cards include already some IT formal notions, such as data types / classes for the state and function parenthesis for behavioral responsibilities, but this can all be done with plain text...

    The Person Class code looks like this:

    //
    // Person class definitions with constructor and methods
    //
    function Person(firstname, lastname, dob) {
      this.firstname = firstname;
      this.lastname = lastname;
      this.dob = dob;
    }
    Person.prototype.getFullName() {
      return this.firstname + " " + this.lastname;
    };
    Person.prototype.getFormalName() {
      return this.firstname + " " + this.lastname;
    };
    Person.prototype.getAgeInYears() {
      return this.dob.getFullYear() - new Date().getFullYear();
    };
    

    Notice the upercase beginning of class names vs. state (variables) and behavior (function) and instances (variable) are - almost - always lower case... This casing makes it very clear from what's a class from which you can make instances with new. To indicate a singletons / 'constant' object / a constant, often all uppercase with underscores is used.

    When you add just one more line to the code above

    exports.Person = Person;
    

    then it is already a complete module - that holds a class definition - and
    you can store it in the modules folder in the sandbox (settings in Web IDE),]
    and you pull it as a module in your application code like this:

    var Person = require("Person");
    

    To save memory and speed up execution, got to google's closure and minifier compiler Web site, paste the code in the left pane, click Compile button, copy-paste the compiled/compressed code from the right pane into a new file which you store in the modules folder in the sandbox named Person.min.js

    function Person(a,b,c){this.firstname=a;this.last­name=b;this.dob=c}Person.prototype.getFu­llName=function(){return this.firstname+" "+this.lastname};Person.prototype.getFor­malName=function(){return this.firstname+" "+this.lastname};Person.prototype.getAge­InYears=function(){return this.dob.getFullYear()-(new Date).getFullYear()};exports.Person=Pers­on;
    

    With his simple piece of code, source code memory saving is about 21%... for complex things the savings are more significant...

    For now, lets just paste the first code block into the source editor / right hand pane of the Espruino Web IDE, add the following lines to crate 'aPerson', upload it and then have some fun with it in the console / left hand pane of the Espruino Web IDE.

    // All Object was born 3/26/1985... wishful thinking... NOTE month is 0..11:
    var person = new Person("All", "Objects", new Date(1985,2,26,));
    

    Work/validate expected behavior of the person in the console with:

    person.lastname;
    person.getFormalName();
    person.getAgeInYears();
    person.firstname = "Just";
    person.getFullName();
    

    This is very simple example, where the 'nestedness' of the objects is minimal. For your application it will be much higher, and individual static / value responsibilities of an complex object can be another complex object, such as your commands are already now (but a bit more memory saving and reuse oriented).

    ...this was a lot beating around the bush for 2 cents... next post goes to sensor work.

  • @allObjects thanks again for your write up. I especially like the explanation you gave about CRC. Your explanations gave me some insights about what I can and should do differently.

  • @d0773d, I got side tracked / delayed in talking about some of the things had in mind when elaborating on the CRC analysis and design technique. I had already some lines, when I tried to better understand what you want to achieve.

    Of course, I could just badmouth your code to everyones frustration... including mine... so I did not want to say anything before better understanding what the challenges actually are across all the layers of your application, hardware and software.

    Therefore I'd ask you to provide a bit more detail about the whole system. Even without that detail, I conclude, there is something in place - such as a central control / monitor node - that will receive the serialized object as string in JSON and then do something with it. Of course, the first part is the easy part: making an object out of JSON with ```var readings = JSON.parse(receivedByteArrayOrString);''­'.

    To give some hints how to decouple the systems, I would let the sensor node (or agent) to its job and read from some place (queue) your high level commands sent by the central control/monitor node, such as: '''takeAllReadings()'''. Since this takes a while, you best end up with an asynchronous coupling of your sensor and central nodes. Since readings are the most important things to you - I assume - you could let that happen with an initiating command that sets up an interval on the particular sensor node. When a reading - including time and all the other things - is complete, you just store it in a variable replacing the previous reading. When the central node is interested in a reading from a particular sensor node, it just asks and gets the most recent. Many of the sensor chips actually work this way, because having to sequence and wait for things all the way up through all layers of an application is a nightmare... indicated already in the amount of code you had to write for your sensor class. Split the it up into more classes / objects covering just a reasonable amount of functionality on a particular level and let lover levels be handled by collaborators... that's why I brought up the CRC stuff in the first place.

    Looking at the challenges here are timing, sequencing, which are purely technically and have 'basically' nothing to with your overall application architecture. This simplistic statement is though not completely truthful and does not good justice to the cause. Underlaying details - and in particular restrictions - have an impact on the UEberlaying (sorry for the word creation) overall architecture. For example: do not design something in an upper layer that expects a real time value within 100 ms... it will just not go-n-a happen... EXCEPT you say: is getting values within 100ms that are may be 5 seconds old? I guess you see where I'm heading for.

    Therefore, in your agent has a property mostRecentReadings and this is updated on an decent, reasonable, and implementable interval - actually a particular event, which is triggered by the completion of updates of the property readingsInProcess. Readings of the individual sensors go update latter property (in the way you do update readings right now, but on setting the last 'outstanding' reading, you just set mostRecentReadings to (now completed) readingsInProcess, and set readingsInProcess a new object { resTemp: null, ph: null, ec: null}. With a timeout you start taking all readings to not keep your agent not too busy burning power... (in case that matters).

    Furthermore, your sensor object looks like a 3 in one (or even more) object. If you can split these out, it could be of help. On the other hand, it looks that only the command sequences and their timings are different... Dividing up the knowledge into separate things that know just about sequencing micro commands singleton as a helper to a more generic sensor object, and similar sequencing of the agent to sequence the the sensors with macro commands. The separation simplifies the understanding and increases grokking by everyone else - including you - especially after you have left the working code alone for some time and need not to go in and make some changes, enhancements, extensions,... or just some simple version maintenance triggered by component version changes.

    In particular, the sonsor object does a lot of repeated things - including eating up memory - even though after being created as a particular sensor, it is never even need. For example you command sequences you can keep outside of your generic sensor code as singleton and pass them in as reference on construction. Furthermore, you can add a reference to a result processing function to get rid of the case-logic in lines 118..138 (I may though be mistaken, since I do not know with what hardware you are dealing...).

    Since these sensors could be looked at as just being a bunch of static declarations - such as your commands already are, you could go even further and make a complete declaration for each individual sensor that also includes the type.

    As dynamic part of the application you write a visitor that takes some parameter and then works itself through the declarations with the collaboration of some helpers, such as a queue, a time sequencer, etc... The problem that you already solved - the solution - of managing timing is not go-n-away, you just express (specify) it in a little bit different way and the so implement the processing - visiting - code. Lookup the visitor pattern... Combined with some timing - which you already do - you will get to the point where you comfortably collect data and stick it into complete / consistent readings that can be picked up anytime - with no delay - by a command from the central.

  • @d0773d, fyi: ...https://www.linkedin.com/groups/73311/73­311-6196619773833506817 just 'sailed' through my linkedin (marketing/advertisment) entangling... (iternet of things)-group... somehow sounded familiar with terms in this thread. - No offense to Python, but I like the JS version more - especially with the frugal resources - no-resources - Espruino can live on.

    PS: have to peek a bit into IEEE 802.15.4 protocol...

  • @allObjects I read through your posts(a few times). I'm definitely needing to split my code up more. In case you are interested... I posted links to the sensors. I would of liked to upload the datasheets, but every time I tried to upload a file I keep getting: "Internal Server Error".

    Temp Sensor
    http://www.atlas-scientific.com/_files/_­datasheets/_circuit/EZO_RTD_Datasheet.pd­f

    PH Sensor
    http://www.atlas-scientific.com/_files/_­datasheets/_circuit/pH_EZO_datasheet.pdf­

    EC Sensor
    http://www.atlas-scientific.com/_files/_­datasheets/_circuit/EC_EZO_Datasheet.pdf­?

  • ...lot of datasheet...

    The main thing having skimmed through the datasheets is what you already tackled: timed commands. Each command has its deferred, particularly to interpret response. Common to the responses is that they are of variable length and their end is detected by receiving a null character.

    These protocols can be implemented as technical functions/objects working on descriptive data to implement them in a generic way and can be easily reused on a higher level of implementation, for example on the sensor. The dynamic nature of JavaScript comes handy to achieve that.

    You can start with each command and related response interpretations as own object. After a few implementations you then factor out repeated/shared function oriented things into service object(s) and specific data/control oriented things into descriptor/specification objects. Factoring out specifics is though not limited to data/control oriented things, a function can be part of it as well, passed as reference to the shared function objects and invoked there. When you 'feel' that things get too complicated or too obscure, you may revert some of the steps and adjust course, or stop all together. Your design should after all reflect the things that you encounter in real and thought world.

    Abstraction is not always simplifying complexity, but can make it manageable by creating manageable, robust code. Sounds plausible... but a lot of aspects fold into the term 'manageable'. Each aspects and its weight is derived from the requirements found top down, bottom up, outside in, inside out,...

  • @allobjects again thank you. Most of what you wrote went WAY over my head; however, It didn't go to waste. You have given me valuable information that I can google to learn more.

  • Give this code a try... I used emulation code to emulate the i2c / sensor and embedded it all into an html. You can run it right off the link of the uploaded/download file. If opened in Chrome and inspected, you should get about the the same view as attached when clicking the buttons init(), logStatus(), and readTemp().

    The AtlasSensor code is based on your initial code that extracts the commands into command specifications. I used longer variable names than usual for documentation purposes and added also some comments.
    Methods starting with underscore (_) hint privacy and are not to be called 'from outside'.

    var p; // prototype var
    
    var STATE =
    { SUCCESS: 1
    , FAILURE: 2
    , INVALID_CMD: 201 // other than what already taken
    , PENDING: 254 // command in execution / pending request
    , NO_DATA: 255 // no command sent / no pending request
    };
    
    var tempDescr = 
      // temp sensor descriptor, most used first
      // pack everything in an anonymous function
      // to allow reusable functions without
      // polluting global name
    (function() {
    
    // reusable functions to be used in command descriptor / cmds
    // - none yet so far
    
    // setup commandSpecifications for tempDescriptor
    // one by one to make one by one executable
    var cmdSpecs = []; 
      // [0] in cmdSpec is command string
      // [1] properties the cmd result is setting
      // [2] function to interpret/handle read raw data
      // [3] time after cmd / before result (rawData) can be read
    cmdSpecs[0] =
        [ "R"     ,   ["temp", "tempTs"]
        , function(rawData) {
            this.temp = rawData * 1;
            this.tempTs = getTime();
          }, 300
        ]
        ;
    cmdSpecs[1] =  
        [ "STATUS",   ["restartCode", "volts", "statusTs"]
        , function(rawData, cmdDescr) {
            var dates = rawData.split(",");
            this.restartCode = dates[1];
            this.volts = dates[2] * 1;
            this.statusTs = getTime();
          }, 300
        ]
        ;
    
    // return descriptor
    return (
    
    { id: "AtlasTempSensor"
    , cmdSpecs: cmdSpecs
    }
    
    ); // /return expression 
    })();
    
    var AtlasSensor = function(id, i2c, addr, descr) {
      this.cmdEvtQueue = [];
      currCmdEvt = null; 
      lastCmdStr = "";
      this.id = id;
      this.i2c = i2c;
      this.addr = addr;
      this.descr = descr;
      lastCmdTs = getTime();
      lastCmdState = -1;
      lastRawData = "";
      descr.cmdSpecs.forEach(function(cmdSpec)­{ // for each command...
          cmdSpec[1].forEach(function(prop){ // ...setup obj with...
              this.prop = null; // ...sensor and cmd specific...
            }, this); // ...properties
        }, this);
    };
    
    AtlasSensor.prototype.cmd = function(cmd, argOrArgsOrCallback, callback) {
      this.cmdEvtQueue.push([cmd, argOrArgsOrCallback, callback]);
      if (!this.currCmdEvt) {
        this.currCmdEvt = this.cmdEvtQueue.splice(0,1)[0];
        this._cmd.apply(this, this.currCmdEvt);
      } 
    };
    
    AtlasSensor.prototype.getLastReading = function(cmd) {
      var cmdSpec = this._getCmdSpec(cmd);
      return ((cmdSpec) 
        ? this._getData(cmdSpec, STATE.SUCCESS)
        : { state: STATE.INVALID_CMD }
        );
    };
    
    AtlasSensor.prototype._cmd = function(cmd, argOrArgsOrCallback, callback) {
      var cmdSpec = this._getCmdSpec(cmd), arg, cb, args = [], cmdStr;
      if ("function" === typeof (arg = argOrArgsOrCallback)) {
        cb = arg;
      } else if ("array" === typeof arg) {
        args = arg;
      } else if ((arg != undefined) && (arg !== null)) {
        args = [arg];
      };
      cb = (cb) ? cb : callback;
      if (cmdSpec) {
        cmdStr = cmdSpec[0];
        args.forEach(function(arg){ cmdStr += "," + arg; });
        this.i2c.writeTo(this.addr, this.lastCmdStr = cmdStr);
        setTimeout(
            this._read.bind(this) // deferred function
          , cmdSpec[3] // defer time
          , cmdSpec, cb // params
          );
      } else { // invalid command
        this.lastCmdTs = getTime();
        this.lastCmdState = STATE.INVALID_CMD;
        this.lastRawData = null;
        if (cb) {
          setTimeout(
              cb
            , 1
            , { state: this.lastCmdState }
            );
        }
        this._nextCmd();
      }
    };
    
    AtlasSensor.prototype._read = function(cmdSpec, callback) {
      var reading = this._readRaw();
      if (reading.state === STATE.SUCCESS) {
        cmdSpec[2].bind(this)(reading.data);
        reading = this._getData(cmdSpec, STATE.SUCCESS);
      }
      this._nextCmd();
      if (callback) { callback(reading); }
    };
    
    AtlasSensor.prototype._nullProps = function(cmdSpec) {
      cmdSpec[1].forEach(function(prop){ this[prop] = null; }, this);
    };
    
    AtlasSensor.prototype._getData = function(cmdSpec, state) {
      var data = { state: state };
      if (cmdSpec) {
        cmdSpec[1].forEach(function(prop){
            data[prop] = this[prop];
          }, this);
      }
      return data;
    };
    
    AtlasSensor.prototype._getCmdSpec = function(cmd) { // get cmdSpec by cmd
      var cmdSpec, cmdSpecs = this.descr.cmdSpecs, 
        idx = 0, idxMax = cmdSpecs.length - 1;
      while (idx <= idxMax) {
        if ((cmdSpec = cmdSpecs[idx])[0] === cmd) { return cmdSpec; }
        idx++;
      }
      return null;
    };
    
    AtlasSensor.prototype._nextCmd = function() {
      if (this.cmdEvtQueue.length > 0) {
        this.currCmdEvt = this.cmdEvtQueue.splice(0,1)[0];
        this._cmd.apply(this, this.currCmdEvt);
      } else {
        this.currCmdEvt = null;
      } 
    }
    
    AtlasSensor.prototype._readRaw = function() {
      var state = -1; // init read status
      var data = null; // init read data
      var byte; // read buffer
      byte = this.i2c.readFrom(this.addr,1)[0]; // read first byte (state)
      if ((state = byte) === STATE.SUCCESS) { // success of command
        data = "";
        byte = this.i2c.readFrom(this.addr,1)[0];
        while (byte !== 0) { // data, not null / ending yet
          data += String.fromCharCode(byte);
          byte = this.i2c.readFrom(this.addr,1)[0];
        }
      } else { // anything else then success
        this.i2c.readFrom(this.addr,1); // consume ending null
        // just let the state pass through
      }
      this.lastCmdState = state;
      this.lastRawData = data;
      return { state: state, data: data };
    };
    
    // *** Test Code
    var tempSensor;
    function init() {
      tempSensor = new AtlasSensor("temp1", tempI2CEmulator, 1, tempDescr);
    }
    function logStatus() {
      tempSensor.cmd("STATUS",function(reading­){
          console.log(reading);
        });
    }
    function readTemp() {
      tempSensor.cmd("R",function(reading){
          console.log(reading);
        });
    }
    

    Executed as part of the test code in Espruino html emulation, the delay of 0.300 seconds in the response is clearly noticeable.

    The emulation code (embedded into html to run in browser):

    <html><head><title>atlas temp sensor emulation / test</title></head>
    <body>
    <script>
    
    // ============ Atlas Sensor code and test code goes here ========
    
    // *** Espruino Emulation / Test Code
    function getTime() { return new Date().getTime() / 1000; }
    
    // *** Atlas Temp Sensor Emulation Code
    var tempI2CEmulator =
    { state: 1 // success
    , cmd: null
    , cmdTs: 0
    , NO_DATA: String.fromCharCode(STATE.NO_DATA) + String.fromCharCode(0)
    , PENDING: String.fromCharCode(STATE.PENDING) + String.fromCharCode(0)
    , readTs: 0
    , readIdx: -1
    , readProperty: null
    , R: String.fromCharCode(1) + "70.25" + String.fromCharCode(0), R_time: 0.300 
    , STATUS: String.fromCharCode(1) + "?STATUS,P,5.038" + String.fromCharCode(0), STATUS_time: 0.300
    , writeTo: function(addr, cmdStr) {
        console.log(cmdStr);
        this.cmdTs = getTime();
        var cmdElts =  cmdStr.split(",");
        this.cmd = cmdElts[0];
        this.readTs = this.cmdTs + this[this.cmd + "_time"];
        this.readIdx = -1;
        this.readProperty = this[this.cmd];
      }
    , readFrom(addr, byteCount) {
        if (this.readProperty === null) {
          this.readProperty = this.NO_DATA;
        } else {
          if (    (this.readIdx === -1) 
               && (getTime() < this.readTs  ) ) {
            readProperty = this.PENDING;
          }
        }
        this.readIdx++;
        var byte = this.readProperty.charCodeAt(this.readId­x);
        if (byte === 0) {
          this.cmd = null;
          this.cmdTs = 0;
          this.readTs = 0;
          this.readIdx = -1;
          this.readProperty = null;
        }
        return [byte];
      }
    };
    // *** /Atlas Temp Sensor Emulation Code
    
    </script>
    <h3>atlas temp sensor emulation / test</h3>
    <button onclick='init()'>init()</button>
    <button onclick='logStatus()'>logStatus()</butto­n>
    <button onclick='readTemp()'>readTemp()</button>­
    </body>
    </html>
    

    Now it is your turn to add the calibration command implementation...

    You may have noticed that all requests go into a queue to make sure the writes and reads are nicely paired. This queuing allows to place multiple requests in a row without having to worry about timings.

    For cascading and combination with other sensors that need values from other sensors, callback implementation gets cumbersome; a promise implementation would look much cleaner.


    2 Attachments

  • @allobjects I haven't forgotten the help you have given me so far. My day time job is consuming most of my time ATM - it's that time for the holiday season :-/ I'm hoping in the next few days I will give your example code a try on the Espruino. I will for sure respond back.

  • @d0773d, np, the sample code was just to fly a bit lower than WAY over the head. Enjoy your day time job (a year ago we worked in shifts to make sure that the 'colored days' went well... and they did: scaling almost 30-fold without a hick up - with same hardware but different delivering and caching strategies - all switchable at runtime. Begin started in the East with a wave all the way thru the West and beyond. It was fascinating to see the architectural work bearing fruit... - Anyway, looking forward to your experience with the code. Take you time - forum will not go away... nor does Espruino. ;<)>

  • @allObjects howdy. It has been awhile since I updated my status on this project/your code. Turns out, the PH sensor that I am using from atlas-scientific is a bit older and buggy which in turn doesn't give me proper readings so I resorted to checking my water ph using the old fashioned method i.e. using the General Hydroponics Test Kit solution. I guess doing things the "old fashioned" way gets the job done :-)

    Now, I'm looking for another project for the espruino.

  • @d0773d , nice to hear back from you. Sad for all the efforts you did put in and the hope to get something done a great way... Sometimes we get things sold that do not exactly what they should and support / updates are just not available. What I can ensure you though is, that Espruino is a very bright light in the darkness of all the (s)crap sold on the internet and otherwise. May be a different, newer version or even a different product for ph sensing could have helped over the hump and over the finish line. I'm glad you go for another project with Espruino. I'm curious what it is all about... if not in public forum post, then in a personal forum message.

  • @allObjects greetings, I sent you a pm..

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

How to access a JSON object?

Posted by Avatar for d0773d @d0773d

Actions