Electric Imp Developer Wiki
NB Server persisted data storage has not yet been implemented. This example demonstrates various techniques but cannot be used verbatim until persisted storage is available.
This example will demonstrate use of a UART serial port and persistent memory. It also explores object oriented techniques such as context binding for callback handlers.
We'll create code for an imaginary Impee:
The purpose of the impee is to identify cats by their pet RFID chip (ISO11784) and allow them to enter only if their ID has been previously registered. This will keep out strays and allow for the tracking of authorised pets.
When a cat enters scanning range an IR beam is broken, causing a GPIO input pin to change state (high = beam broken).
The RFID reader is powered up via a GPIO output pin - while powered up it continuously scans for RFID chips, returning the ID via serial when discovered. The protocol is:
Successful scan: 0x02 XX XX XX XX XX XX 0x0A 0x0D (XX = ASCII hex representation of code, length may vary) Leaving range: 0x1B
The RFID reader is powered down when not in use to save power. The project would most likely be battery powered, so is power sensitive.
A solenoid latch releases the flap when energised, allowing the cat to pass through the flap. The solenoid circuit is connected to a GPIO output pin.
Finally a programming button may be pressed by the user to indicate that the next chip scanned is an authorised pet. This button is connected to a GPIO input pin and requires a pull up. It drives low when pressed. The authorised ID will be saved to non volatile memory.
The cat flap will send the index of the authorised cat to a node port each time the flap is released. We'll also write this information to the server log. In this way the owner may monitor cat activity or, perhaps, send an SMS message when a particular pet enters the house.
The flap will accept an override code on an input port to allow the flap to be force locked or unlocked remotely, for example from a web page or phone app.
An extension to the project might allow for controlling and tracking egress as well as ingress, so the owner could program the flap to trap a particular cat inside or outside the house, and review its current whereabouts remotely. This would require a different arrangement of sensors.
// Electric Imp code example - RFID Cat Flap
We'll derive from InputPort for our input class. This will save lock mode commands to its parent's member data. A reference to the parent is passed to the constructor.
// Input class for accepting lock override commands class inputFlap extends InputPort { parentClass = null; // Our constructor will save its parent class reference constructor(parent) { base.constructor("Control", "number"); parentClass = parent; } // Override set function to receive lock mode messages function set(mode) { // Save the mode if valid if(mode >= 0 && mode <= 2) parentClass.lockMode = mode; } }
We'll make a class to represent the RFID reader. This will be constructed with references to the serial port and power enable pin to use, and to a callback function to which we'll pass received RFID codes.
// RFID reader device class class rfidDevice { eventHandler = null; // Callback function to handle ID read events serPort = null; // Serial port to which RFID reader is attached powerEn = null; // Pin which controls RFID power enabled = false; // Reader enabled flag rxData = ""; // Received data // Constructor takes hardware assignments and callback handler reference constructor(port, power, handler) { eventHandler = handler; serPort = port; powerEn = power; // RFID reader requires 9600bps, 8 bits, no parity, 1 stop bit, no flow control serPort.configure(9600, 8, PARITY_NONE, 1, NO_CTSRTS); // Power enable line driven push-pull (high = enabled) powerEn.configure(DIGITAL_OUT); // Start offline disable(); } // Enable RFID reader and start polling function enable() { rxData = ""; powerEn.write(1); enabled = true; poll(); } // Disable RFID reader function disable() { enabled = false; powerEn.write(0); } // Poll for data from RFID reader function poll() { if(enabled) {
Here we call imp.wakeup to schedule the next poll for serial data, however we cannot pass a simple reference to the method. While this would work fine for a free (top level) function, to pass this member function we must bind its environment (this object) to ensure that it has the correct context when the callback occurs. Squirrel provides a method bindenv for this purpose, which we will use whenever passing member functions when a free function parameter is expected (such as most callback parameters).
// Schedule the next poll in 100ms imp.wakeup(0.1, poll.bindenv(this)); // Read a byte local byte = serPort.read(); // Continue reading while data is available while(byte != -1) { switch(byte) { case 0x02 : // Start of data case 0x1B : // Ignore out of range rxData = ""; break; case 0x0A: // Data complete eventHandler(rxData); rxData = ""; break; case 0x0D: // Ignore CR break; default: // ID byte rxData += byte; break; } // Get the next byte byte = serPort.read(); } } } }
We'll make a class to represent the catflap product, bringing together all of its functionality.
// Cat flap class represents our example product class catFlap { output = null; // Output for reporting feline ingress activity input = null; // Input for accepting lock override commands rfid = null; // RFID device detect = null; // Cat proximity detect input pin, 1 = beam broken prog = null; // Program button input pin, 0 = pressed power = null; // RFID power control output pin, 1 = power on latch = null; // Solenoid latch control output pin, 1 = latch released lockMode = 0; // Lock mode: 0 = Normal, 1 = Force Unlocked, 2 = Force Locked programMode = false; // Program mode flag state = 0; // Current state: 0 = Idle, 1 = Scanning // Constructor takes device assignments constructor(rfidDev, detectDev, progDev, powerDev, latchDev) { // Save device assignments detect = detectDev; prog = progDev; latch = latchDev; // Create RFID device rfid = rfidDevice(rfidDev, powerDev, rfidEvent.bindenv(this)) // Configure GPIOs detect.configure(DIGITAL_IN_PULLUP, detectEvent.bindenv(this)); prog.configure(DIGITAL_IN_PULLUP, progEvent.bindenv(this)); latch.configure(DIGITAL_OUT); // Create output and input objects output = OutputPort("Activity", "number"); input = inputFlap(this); // Register with the server imp.configure("Cat Flap", [input], [output]); } // Handle RFID code received event function rfidEvent(id) { // Disable the RFID scanner rfid.disable(); // If programming, add this ID to the authorised table if(programMode) { nv[id] <- ++nv["kittyIdx"]; programMode = false; server.log("Authorised ID " + id + " as cat " + nv[id] + "."); } // If ID is authorised, consider opening the latch if(id in nv) { if(lockMode == 2) { // Forced locked, log but do not open the latch server.log("Locked against cat " + nv[id] + "."); } else { // Open the latch latch.write(1); imp.wakeup(2.0, latchEvent.bindenv(this)); server.log("Opened for cat " + nv[id] + "."); } } } // Handle latch period elapsed event function latchEvent() { // Close the latch latch.write(0); // New state: Idle state = 0; } // Handle program button event function progEvent() { // Update the program mode flag (set when pin is low) if(prog.read() == 0) programMode = true; } // Handle cat proximity event function detectEvent() { // Get beam state local beam = detect.read(); if(beam == 1) { // Beam broken, animal is present if(lockMode == 1) { // Forced open, no need to read chip latch.write(1); imp.wakeup(2.0, latchEvent.bindenv(this)); } else { // Check for a new beam break if(state == 0) { // New state: Scanning state = 1; // Enable the RFID scanner rfid.enable(); } } } else if(beam == 0 && state == 1) { // Cat has departed, disable the RFID scanner rfid.disable(); // New state: Idle state = 0; } } }
We'll record authorised cat IDs in the nv table. This is a special global table which persists across power cycles (it is flushed to the cloud when the script sleeps, and retrieved at each start up). Every Imp has its own unique nv table per script executed upon it.
NB Persistence across power cycles is a planned feature which has not been implemented at time of writing. Details of the implementation may change.
The first time the script runs on an imp it is necessary to create its nv table. A short sleep is included to force sync the table to the cloud.
// Check whether non-volatile table has been created if(!("nv" in this)) { nv <- { kittyIdx = 0 }; server.sleepfor(1); }
We'll create one slot in the table “kittyIdx” as an index counter of authorised cats. We'll check for this at start up to determine whether the nv table has already been initialised.
// Create index slot if not present if(!("kittyIdx" in nv)) { nv["kittyIdx"] <- 0; }
Each authorised cat will have one slot in the table identified by its chip ID and containing its cat index. We'll log this at start up as a convenience for advanced users.
// Log the authorised ID list at start up server.log("Authorised ID list:"); foreach(k,v in nv) { if(k != "kittyIdx") server.log(v + " : " + k); }
Finally we instantiate our catflap object. Instead of using a local we'll create our object as a table. This is necessary because there are no other strong references to the object; it would otherwise go out of scope when execution reached the end of the script. You might think that the callback parameters passed to the pin.configure and imp.wakeup methods would qualify, however the bindenv method stores only a weak reference to the environment object, in this case our class instantiation. Instantiating to a table adds the object to the root environment, keeping it in scope for the whole run time.
// Create a cat flap instance thisCatFlap <- catFlap(hardware.uart1289, hardware.pin1, hardware.pin2, hardware.pin8, hardware.pin9); // End of code.
For the full code listing click here.