Project tutorial
Remote System Monitor

Remote System Monitor © CC BY-SA

Monitor a remote system (battery, power usage and sensors) with this Arduino MKR FOX 1200-based, Sigfox-connected solution.

  • 8,716 views
  • 1 comment
  • 24 respects

Components and supplies

Abx00014 featured xjsqphgdlv
Arduino MKR Fox 1200
×1
Lead acid battery (e.g. car battery)
×1
Latching Relay (16A)
×2
BD6211F
×2
7805 style buck converter
×1
Capacitor 10uF 10V
×2
Terminal block - 2 pin
×3
LED - 1206 case - reverse mount
Optional
×1
USB A Socket
Optional
×1
INA219
Current monitor
×1
Resistor 0R1 1210 case
Current sense resistor. If not using the PCB select a batter case style to better power handing (or a smaller value resistor)
×1
IRF7413
×3
MBRA140
×3
1117
3v3 regulator
×1
Capacitor 0.1uF 0805 Case
×3
Capacitor 1.0 uF 0805 Case
×5
Resistor 10k 0805
×3
Resistor 10R 0805 Case
×3
Resistor 100k 0805 Case
×3

Necessary tools and machines

3drag
3D Printer (generic)
Used for the case only

Apps and online services

About this project

My garage has no mains electricity or WiFi and so I use an old car battery for power and LED lighting, I'd like to know when the battery is getting flat, or if the door is open without having to go out to the garage.

I'm sure many remote/agricultural buildings have similar problems where it's not feasible to run a mains supply and so they rely on battery power, maybe for powering the electric fences, small low power pumps to keep a leak from flooding or simple occasional lighting.

This project provides a way to monitor the battery level and power usage, along with sensor inputs and provide switchable outputs as well as a 5v USB power socket.

Construction:

  • The battery (12 or 24V lead acid) is connected through the lower 2 pin terminal block, leading to a 0R1 ohm resistor connected to a INA219 to monitor voltage and current flow (in and out of the battery) and then to the relays and FETs above.
  • Two latching relays are used for a power out and/or charge (solar) input on the right and above that 3 optional N-Channel FET controlled outputs.
  • An Arduino MKR FOX 1200 at the heart allows for low power Sigfox wireless communications and monitoring of the system.
  • On the left a series of analog or digital inputs can be used with things like motion sensors, push button switches, door contact switches, water level sensors etc.
  • Analog 1-4 connectors have both 3v3 and GND as well as the signal pin (A1-A4 on the Arduino.
  • Analog 5 (4 pin connector) has 5V power and the inputs are via potential dividers to allow for a 5V input - this can be useful for hall effect flow sensors that need a bit more juice than 3v3 and connects to A5 and A6.
  • Bottom left is a 5V USB power connector (Allows for charging of my phone or other equipment when in the garage).
  • An on-board 5V and 3v3 regulator convert the battery voltage to 5V for USB power and 3v3 for the Arduino. Note that most of the system can run powered from the Arduinos 3v3 if this is preferred. The Arduino could also drop back to its own battery input if needed whilst still measuring the main battery/current and other sensors.
  • Top right is a small prototyping area to allow a little customisation.
  • All the pins of the Arduino are brought out to a second row of connectors for easy prototyping.
  • The various power rails have diodes and optional jumpers to allow for powering from the Arduino or battery (or both).

Battery Monitoring

The chart below shows the battery voltage over the last 28 days, you can clearly see when the charger was connected. A peak at the tables below the chart also shows current consumption ( I disabled it on the chart to keep it clearer).

The Arduino monitors the battery voltage and current flow, this is reported back via the Sigfox cloud to Tinamous where it can be monitored and notifications send on low voltage. Sigfox allows for a maximum of 140 messages a day so the measurements are reported every 15 minutes.

At present the charge input is switched off with every Sigfox uplink, this allows the system to determine how much power is being used by the consumer (otherwise charger current may be drawn as well) and the battery voltage without the charge voltage applied. However this means operation of the relay which will lead to increased battery discharge over time.

The Adafruit library for the INA219 is used for the Arduino to monitor the INA219 and the resistor.

Power Control and Sensor

Latching relays are used for output/charge control as they draw no power in either state once set. To set the relay in either state the coil polarity is changed, this is achieved using a H-Bridge driver (BD6211F) and two outputs from the Arduino. A double high or double low output has no effect on the relay, and a high & low or low & high output puts the relay into the desired state. Then both outputs are set to low to remove power from the relay.

The BD6211F H-Bridge driver is intended to drive motors, hence the inputs are for forward and reverse (RIN, FIN) which may be a little confusing for a relay!

Unfortunately I used D6 for one of the relay pins which is also used by the Sigfox chip as an indicator when in debug mode, this results in the output relay playing some interesting tunes when transmitting Sigfox data.

Whilst the relays are marked Charge and Output both could be used for outputs allowing one to be switched off as the battery gets low to ensure a more critical circuit remains powered.

The N-Channel FETs can be driven with a simple high/low digital pin, however these do not latch so once the Arduino is placed in low power state they will revert to off.

Connectivity

This project uses Sigfox for connectivity, this is ideal for this solution as it gives us low power and long range and is handily build into the Arduino MKR FOX 1200.

Data sent to the Sigfox cloud is limited to 12 bytes, which is plenty of space as the device only needs to transmits status, voltage, current and temperature. The data structure is:

typedef struct __attribute__ ((packed)) sigfox_message { 
 uint8_t status;     // status::uint:8 -> Split to 8 bits 
 uint8_t version;    // version::uint:8 
 int16_t temperature; // temperature::int:16:little-endian 
 int16_t current;    // current::int:16:little-endian (e.g. 1159 mA -> 0487 but stored as 8704) 
 uint16_t voltage;   // voltage::uint:16:little-endian 
 int16_t currentOffCharge;   // currentOffCharge::int:16:little-endian 
 uint16_t voltageOffCharge;   // voltageOffCharge::uint:16:little-endian 
} SigfoxMessage;  

At the Sigfox cloud we define a custom callback with a custom payload configuration to convert this to json properties to send to Tinamous.

The configuration used is:

status::uint:8 version::uint:8 temperature::int:8 current::int:16:little-endian voltage::uint:16:little-endian humidity::int:16:little-endian light::uint:8 light2::uint:8 lastStatus::uint:8

So for example the first byte parsed is read as a unsigned integer of 8 bits and is put into the customData#status property. Next version is read, then temperature and following that current, this is a little more interesting in that it reads 16 bits (2 bytes) and converts it using the little-endian format (least significant digit is read first, then most), so a packet with 0x0A 0x1B bytes would result in a current property of 0x1B0A (6922), voltage, humidity, light and lastStatus are then read in similar style.

Current is represented in milliamps inside the signed 16 bit integer which allows for +/-32A at 1mA resolution, more than ample given I selected the wrong resistor package which limits it to 2A!

Voltage is held as a float (e.g. 12.34) but is converted via a x100 factor into a 16 bit unsigned integer (i.e. 1234) when sent to Sigfox, this is more than enough precision for battery monitoring and keeps the data structure small.

The first byte (status) is composed of a series of bit flags:

// B7: Battery Flat 
// B6: Charge Relay state 
// B5: Charging 
// B4: Reserved 
// B3: Output relay state 
// B2: Fet 1 state 
// B1: Fet 2 state 
// B0: Fet 3 state 

This allows a single byte to convey the state of the outputs of the system.

Sigfox Cloud:

On first use the Arduino needs to be activated at the Sigfox cloud, this is very easy but you need to ID and PAC from the Arduino. These are printed out via the serial port when the system is first powered up.

With ID and PAC in hand head on over to https://backend.sigfox.com/activate to activate the device following the wizard.

Once activated the device and device type are available through the Sigfox backend, we then set-up a custom callback to push the data to Tinamous.

It's important to note that the Arduino uses little-endian format for multi-byte words but Sigfox default to big-endian this needs to be specified in the call back configuration.

Tinamous Internet of Things Platform:

I added a Sigfox Bot to my Tinamous account, this enables the callback to push data into my Tinamous account. Each Sigfox device gets it's own Tinamous device to represent it.

For more information on connecting Sigfox to Tinamous see my other "Get Your Sigfox On" project for instructions on how to do this.

Tinamous will automatically add fields to a device when measurements are push in based on the fields specified in the callback. However, these tend to have unfriendly field names, no units and Tinamous has no way to know that voltage is actually 1/100 of the value that is pushed in.

This is easily fixed by editing the device and updating the field definitions.

For voltage we need to go one step further into the Advanced options and apply a calibration, I also set a working range on this as well which means I can set-up notifications if the battery voltage is out of range.

I applied a calibration of y = 0.01x + 0 (where x is the reported value and y is the actual resultant voltage), so a value of 1345 translates to a voltage of 13.45V.

Next I created a dashboard so I could get a quick overview of the battery state, current consumption, temperature and the Sigfox signal strength. This is currently in sitting my house, sadly it's certainly not 17°C in the garage!.

Conclusion:

This project hasn't made it out to the garage just yet (pending some wood for the wall and time to fit it), however it's been happily running from a car battery for the last month and has performed well (and not totally flattered the battery yet!).

Low power mode is not implemented at this time as it messes up the serial port and I wanted to iron out a few bugs. Current consumption is about 13mA in its present state, some of this is down to the 5v regulator. At this rate the 70AH car battery would last about 220 days powering only the monitor which isn't ideal, but if a system had solar charging and needed continuous sensor monitoring and control this would be fine.

Downlink from Sigfox isn't yet configured, this could be used to control the relays and/or FETs but it's not needed at present for my garage.

Yes, I spelt Sigfox wrong on the PCB silk screen!

The current test set-up ready to be moved to the garage:

Code

RemoteSystemMonitor.inoArduino
// Arduino Low Power - Version: Latest 
#include <ArduinoLowPower.h>

// Arduino SigFox for MKRFox1200 - Version: Latest 
#include <SigFox.h>

#include <Wire.h>
#include <Adafruit_INA219.h>

Adafruit_INA219 ina219;

bool fet1State = false;
bool fet2State = false;
bool fet3State = false;
bool outputRelay = false;
bool chargeRelay = false;
bool oshLed = false;

int loopCounter  =0;
bool debug = true;

// First run request downlink data
// from the Sigfox backend.
bool requestDownlinkData = true;

// status::uint:8 version::uint:8 temperature::int:8 current::int:16 voltage::uint:16 humidity::int:16 light::uint:8 light2::uint:8 lastStatus::uint:8
typedef struct __attribute__ ((packed)) sigfox_message {
  uint8_t status;     // status::uint:8 -> Split to 8 bits
  uint8_t version;    // version::uint:8
  int16_t temperature; // temperature::int:16:little-endian
  int16_t current;    // current::int:16:little-endian (e.g. 1159 mA -> 0487 but stored as 8704)
  uint16_t voltage;   // voltage::uint:16:little-endian
  int16_t currentOffCharge;   // currentOffCharge::int:16:little-endian
  uint16_t voltageOffCharge;   // voltageOffCharge::uint:16:little-endian
} SigfoxMessage;

// stub for message which will be sent
SigfoxMessage msg;

void setup() {
  pinMode(0, OUTPUT); // led
  pinMode(1, OUTPUT); // Fet1
  pinMode(2, OUTPUT); // Fet2
  pinMode(3, OUTPUT); // Fet3
  pinMode(4, OUTPUT); // charge relay FIN
  pinMode(5, OUTPUT); // charge relay RIN
  pinMode(6, OUTPUT); // output relay FIN
  pinMode(7, OUTPUT); // output relay RIN
  
  setFetOutput(1, fet1State);
  setFetOutput(2, fet2State);
  setFetOutput(3, fet3State);
  // On to indicate in setup
  setOshLed(true);
  
  // Intialize the relays to off
  // whilst in set-up. Force state so we know
  // the state their in and can then track it.
  setChargeRelay(false, true);
  setOutputRelay(false, true);
  
  //Initialize serial and wait for port to open:
  if (debug) {
    Serial.begin(9600);
  }
     
  // Initialize the INA219.
  // By default to (32V, 2A).  
  ina219.begin();
 
  setupSigfox();  

  if (debug) {
    Serial.println("System Monitor Setup Complete... V0.0.4");
  }
  
  // turn off the OSH Led now set-up is complete.
  setOshLed(false);
}

void setupSigfox() {
  if (!SigFox.begin()) {
    Serial.println("Shield error or not present!");
    return;
  }
  
  // Enable debug led and disable automatic deep sleep
  // Comment this line when shipping your project :)
  if (debug) {
    SigFox.debug();
  }

  // Output the ID and PAC needed to register the 
  // device at the Sigfox backend.
  String version = SigFox.SigVersion();
  String ID = SigFox.ID();
  String PAC = SigFox.PAC();

  // Display module informations
  Serial.println("MKRFox1200 Sigfox first configuration");
  Serial.println("SigFox FW version " + version);
  Serial.println("ID  = " + ID);
  Serial.println("PAC = " + PAC);

  Serial.println("");

  Serial.print("Module temperature: ");
  Serial.println(SigFox.internalTemperature());

  Serial.println("Register your board on https://backend.sigfox.com/activate with provided ID and PAC");

  delay(100);

  // Send the module to the deepest sleep
  SigFox.end();
}

void loop(void) 
{ 
  float shuntvoltage = 0;
  float busvoltage = 0;
  float current_mA = 0;
  float loadvoltage = 0;
  
  // V and I when not charging
  // to give an idea of the state of the battery
  float current_off_charge_mA = 0;
  float busvoltage_off_charge = 0;
 
  // Ensure output relay and charge relay are on by default
  // now that we're monitoring.
  setOutputRelay(true, false);
  setChargeRelay(true, false);
     
  // Flip the LED to show we're doing stuff.
  setOshLed(true);

  // Meaure normal running voltage/current.
  shuntvoltage = ina219.getShuntVoltage_mV();
  busvoltage = ina219.getBusVoltage_V();
  current_mA = ina219.getCurrent_mA();
  loadvoltage = busvoltage + (shuntvoltage / 1000);
    
  if (debug) {    
    Serial.println("-------------------------------------------------------");
    Serial.println("Charger connected: ");    
    Serial.print("Bus Voltage:   "); 
    Serial.print(busvoltage); 
    Serial.println(" V");
    
    Serial.print("Current:       "); 
    Serial.print(current_mA); 
    Serial.println(" mA");
    Serial.println("");
    
    Serial.print("Loop counter:  "); 
    Serial.print(loopCounter); 
    Serial.println("");
    
    Serial.println("");
  }
          
  // Flip the LED to show we're doing stuff.
  setOshLed(false);
       
  // 6 (loops) * 10s (delay) gives approx 1 minute 
  // so send an update to Sigfox rather than (when debugging)
  // having to wait the full 15 minutes for a transmission.
  if (loopCounter == 12) {

    // TODO: Don't disable charge relay when voltage is low
    // as we might not be able to re-enable it.
    //
    // Once every Sigfox transmit cycle, disconnect the charger
    // to measure the raw battery voltage and actual load current
    // (otherwise load current excludes current from the charger).
    setChargeRelay(false, false);
    // Little delay to let the battery chemistry settle a little.
    delay(2000);
    current_off_charge_mA = ina219.getCurrent_mA();
    busvoltage_off_charge = ina219.getBusVoltage_V();
    setChargeRelay(true, false);  

    if (debug) {
      Serial.println("Charger disconnected: ");
      Serial.print("Charge Off Voltage:  "); 
      Serial.print(busvoltage_off_charge); 
      Serial.println(" V");
      
      Serial.print("Charge Off Current: "); 
      Serial.print(current_off_charge_mA); 
      Serial.println(" mA");
      
      Serial.println("");
    }
  
    // remove once we have the device details
    //setupSigfox();
    sendToSigfox(busvoltage, current_mA, busvoltage_off_charge, current_off_charge_mA);
  }

  // Reset the loop counter after (hopefully) 15 minutes.
  if (loopCounter > 6 * 15) {
    loopCounter = 0;
  }

  // Monitor every 10s. (2s delay in relay clickyness).
  delay(8000);
  //LowPower.sleep(8 * 1000);
  loopCounter++;
}

void setOshLed(bool state) {
  //Serial.println("Setting OSH LED");
  digitalWrite(0, state);
  oshLed = state;
}

void setOutputRelay(bool state, bool force) {
  
  // Unless forced, ignore instruction if relay already in correct state
  // to save power.
  if (!force) {
    if (outputRelay == state) {
      return;
    }
  }

  if (debug) {    
    Serial.print("Setting output relay: ");
    Serial.println(state);
  }
  
  digitalWrite(6, state);
  digitalWrite(7, !state);
  outputRelay = state;
  
  delay(100);
  digitalWrite(6, LOW);
  digitalWrite(7, LOW);
}

void setChargeRelay(bool state, bool force) {
  // Unless forced, ignore instruction if relay already in correct state
  // to save power.
  if (!force) {
    if (chargeRelay == state) {
      return;
    }
  }

  if (debug) {    
    Serial.print("Setting charge relay: ");
    Serial.println(state);
  }
  
  // FIN/RIN order swapped on charge compared to output.
  digitalWrite(5, state);
  digitalWrite(4, !state);
  chargeRelay = state;
  
  delay(100);
  digitalWrite(4, LOW);
  digitalWrite(5, LOW);
}

void setFetOutput(int fet, bool state) {
  if (debug) {    
    Serial.print("Setting FET: ");
    Serial.print(fet);
    Serial.print(" State: ");
    Serial.println(state);
  }
  
  switch (fet) {
    case 1:
      fet1State = state;
    case 2:
      fet2State = state;
    case 3:
      fet3State = state;
    default:
      // Invalid fet!
      return;
  }
  
  digitalWrite(fet, state);
}

// B7: Battery Flat
// B6: Charge Relay state
// B5: Charging
// B4: Reserved
// B3: Output relay state
// B2: Fet 1 state
// B1: Fet 2 state
// B0: Fet 3 state
byte getStatusFlags(float voltage, float current_mA, float current_charger_off_mA) {
  byte status = 0;
  
  // Upper Nibble (Charging/Battery)
  // Battery flat
  if (voltage < 10.5) {
    status |= 0x80; // 1000 0000
  }

  // Charger relay status
  if (chargeRelay) {
    status |= 0x40; // 0100 0000
  }

  // +ve current flow to the battery means it's charging.
  if (current_mA > 0) {
    status |= 0x20; // 0010 0000
  }

  // Not sure what this means...
  if (current_charger_off_mA > 0) {
    status |= 0x10; // 0001 0000
  }

  // Lower Nibble (Outputs)
  // Output relay enabled.
  if (outputRelay) {
    status |= 0x08; // 0000 1000
  }

  if (fet1State) {
    status |= 0x04; // 0000 0100
  }

  if (fet2State) {
    status |= 0x02; // 0000 0010
  }

  if (fet3State) {
    status |= 0x01; // 0000 0001
  }

  return status;
}

void sendToSigfox(float busvoltage, float current_mA, float busvoltage_off_charge, float current_charger_off_mA) {
  if (debug) {    
    if (requestDownlinkData) { 
      Serial.println("**** Sending to Sigfox with downlink *****");
    } else {
      Serial.println("**** Sending to Sigfox *****");
    }
  }
  SigFox.begin();

  // Wait at least 30mS after first configuration (100mS before)
  delay(100);
  // Clears all pending interrupts
  SigFox.status();
  delay(1);

  // Bytes 0 and 1
  msg.status = getStatusFlags(busvoltage_off_charge, current_mA, current_charger_off_mA);
  msg.version = 3;
  
  msg.temperature = (int16_t)(SigFox.internalTemperature() * 10);
  
  // Charger connected
  // 2000mA is max for PCB (32768 max for signed int. i.e. +/-32 Amps)
  msg.current = (int16_t)(current_mA); 
  // 12.13 -> 1213 (65535 max i.e. 655 Volts)  
  msg.voltage = (uint16_t)(busvoltage * 100); 
  
  // Charger Off (Isolated).
  msg.currentOffCharge = (int16_t)(current_charger_off_mA); 
  msg.voltageOffCharge = (uint16_t)(busvoltage_off_charge * 100);

  Serial.println("Sending packet...");
  SigFox.beginPacket();
  SigFox.write((uint8_t*)&msg, 12);
  // endPacket actually sends the data.
  uint8_t lastMessageStatus = SigFox.endPacket(requestDownlinkData);
  
  if (requestDownlinkData) {
    parseDownlinkData(lastMessageStatus);
  }
  
  SigFox.end();
  
  // Ensure downliad link is cleared after sending to Sigfox.
  // TODO: Re-enable this every 6 hours (Max 4x per 24 hours).
  requestDownlinkData = false;
  
  if (debug) {    
    Serial.println("Status: " + String(lastMessageStatus));
  
    if (lastMessageStatus > 0) {
      Serial.println("No transmission!");
    }  
  }
}

void parseDownlinkData(uint8_t lastMessageStatus) {
  if (lastMessageStatus > 0) {
    Serial.println("No transmission. Status: " + String(lastMessageStatus));
    return;
  }

  // Max response size is 8 bytes
  // set-up a empty buffer to store this. (0x00 == no action for us.)
  // each byte has meaning for control.
  uint8_t response[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

  // Response packet needs parsePacket to be called
  // before being read.
  if (SigFox.parsePacket()) {
    
    Serial.println("Response from server:");
    int i = 0;
    while (SigFox.available()) {
      Serial.print("0x");
      int readValue = SigFox.read();
      Serial.println(readValue, HEX);
      response[i] = (uint8_t)readValue;
      i++;
    }

    // byte 0 - relay control.
    parseRelayControl(response[0]);
    // byte 1 - FETs control
    parseFETControl(response[1]);
    // byte 2 - GPIO output?
    
  } else {
    Serial.println("No response from server");
  }
  Serial.println();
}

void parseRelayControl(uint8_t value) {
  // TODO: implement.
  // 00 : Ignore
  // 01 : Off
  // 10 : on
  // 11 : invalid
  // Bits 7 & 6 = > Output relay
  // Bits 5 & 4 = > Charge relay
  // Bits 3 & 2 = > Ignored
  // Bits 1 & 0 = > Ignored
  Serial.println("Setting relays from:");
  Serial.println(value, HEX);
  Serial.println("");
}

void parseFETControl(uint8_t value) {
  // TODO: Implement.
  // 00 : Ignore
  // 01 : Off
  // 10 : on
  // 11 : invalid
  // Bits 7 & 6 = > Fet 1
  // Bits 5 & 4 = > Fet 2
  // Bits 3 & 2 = > Fet 3
  // Bits 1 & 0 = > Ignored
  Serial.println("Setting FET outputs from:");
  Serial.println(value, HEX);
  Serial.println("");
}

Custom parts and enclosures

Case - OpenScad file
batterymonitorcase_kaATs1K2RW.scad
Case - stl

Schematics

Schematic
Schematic sg9mlnkpie

Comments

Similar projects you might like

IoT System To Monitor Soil Moisture With Arduino

Project tutorial by Francesco Azzola

  • 2,904 views
  • 0 comments
  • 6 respects

Monitor and control your irrigation system with a mobile app

Project tutorial by Officine Innesto

  • 3,763 views
  • 0 comments
  • 9 respects

WaterPi: Houseplant Remote Watering and Monitoring System

Project tutorial by Demirhan Aydin

  • 29,545 views
  • 14 comments
  • 128 respects

Carfox: A Device to Find Them All

Project tutorial by Luis Roda Sánchez

  • 7,428 views
  • 2 comments
  • 28 respects
Add projectSign up / Login