Remote industrial monitoring presents a critical challenge: achieving reliable data collection over extended periods without frequent maintenance interventions. The Norvi EC-M12-BC-C6-C-A addresses this challenge through intelligent power management, achieving 11.4 years of battery life while maintaining real-time cloud connectivity.
This project guide demonstrates a complete industrial deployment for diesel fuel tank monitoring, covering hardware setup, firmware configuration, cloud integration, and real-world optimisation strategies.
Application OverviewThis implementation monitors diesel fuel levels in remote storage tanks using:
- 4-20mA analog sensor interface for industry-standard instrumentation
- NB-IoT/LTE-M cellular connectivity for wide-area coverage
- MQTT over 4G for efficient cloud communication
- ThingsBoard dashboard for real-time visualization and alerts
Key Achievement: Through optimised duty cycling and ultra-low-power design, the system operates for over a decade on primary lithium batteries—eliminating costly maintenance visits to remote sites.
Why This MattersTraditional IoT solutions struggle with battery life in remote deployments:
- Conventional cellular devices: 6-12 months battery life
- Frequent site visits for battery replacement: $150+ per visit
- System downtime during maintenance
- Safety concerns accessing tanks in hazardous locations
The EC-M12-BC-C6-C-A's architecture solves these issues through:
- STM32L0 ultra-low-power microcontroller (1.05µA sleep current)
- Intelligent peripheral power management
- Optimized cellular communication protocols
- Robust fault detection and diagnostics
[Fuel Sensor] → [ADS1115 ADC] → [STM32L0 MCU] → [SIM7070 Modem] → [4G Network] → [ThingsBoard]
↓ ↓ ↓
4-20mA 38, 000mAh Battery Web Dashboard
Operating Cycle
The EC-M12-BC-C6-C-A operates in a power-optimized duty cycle:
1. Wake Up Phase (60 seconds)
- RTC alarm triggers system wake from shutdown mode
- STM32L0 initializes peripherals
- SIM7070 modem powers up and registers with cellular network
- GPRS/LTE-M data connection established
2. Data Acquisition (2.5 seconds)
- Enable 12V booster for sensor power
- ADS1115 ADC reads 4-20mA sensor current
- Convert current to fuel level percentage and height
- Read battery voltage for health monitoring
- Perform sensor fault detection (open/short circuit)
3. Cloud Transmission (5 seconds)
- Establish MQTT connection with ThingsBoard broker
- Publish telemetry data (fuel level, battery status, faults)
- Wait for broker acknowledgement.
- Disconnect MQTT session
4. Sleep Phase (15-60 minutes)
- Disable all peripherals (ADC, I2C, SPI, UART)
- Power down SIM7070 modem
- Disable 12V booster
- Enter STM32L0 shutdown mode (only RTC active)
This duty-cycled approach reduces average current from 17mA (active) to 1.29mA (15-min cycle) or 380µA (1-hour cycle).
Power Consumption ProfileDetailed measurements using precision current probes:
| Stage | Duration | Avg Current | Peak Current | Energy per Cycle |
|--------------------|---------|------------|-------------|----------------|
| Modem Initialization | 60 sec | 17.71 mA | 840 mA | 3.93 J |
| Data Acquisition | 2.5 sec | 11.24 mA | 120 mA | 0.14 J |
| Data Transmission | 5 sec | 11.24 mA | 443 mA | 0.17 J |
| Sleep Mode | 15 min | 1.05 µA | 2.85 mA | 0.0035 J |
Complete 15-minute cycle: 1.29mA average, 4.60 J total energy
Battery Life ProjectionsWith 38, 000mAh total capacity (2× 19, 000mAh ER34615H cells):
| Transmission Interval | Average Current | Battery Life | Practical Deployment |
|----------------------|----------------|--------------|----------------------------|
| 5 minutes | 3.2 mA | 1.2 years | Frequent monitoring needed |
| 15 minutes | 1.29 mA | 3.36 years | Standard industrial use |
| 30 minutes | 0.65 mA | 6.5 years | Low-change applications |
| 1 hour | 380 µA | 11.4 years | Stable tank monitoring |
Key Insight: Doubling the sleep interval from 15 to 30 minutes doubles battery life. The relationship is nearly linear because sleep current (1.05µA) is negligible compared to active current (17mA).
SchematicsPin ConfigurationEC-M12-BC-C6-C-A Key Pins:STM32L0 Microcontroller: PA8 - Modem power key PA15 - 12V booster enable PB6 - I2C SCL (ADC) PB7 - I2C SDA (ADC) PA2 - UART TX (modem) PA3 - UART RX (modem)8-Pin Terminal Block: Pin 1 - 4-20mA analog input Pin 2 - Digital input 1 Pin 3 - Digital input 2 Pin 4 - GND Pin 5 - RS485 A Pin 6 - RS485 B Pin 7 - 12V output (sensor power) Pin 8 - GND
Hardware Connections
4-20mA Fuel Level Sensor:
Sensor Wire → EC-M12 Terminal
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Red (+) → Pin 7 (12V out)
Black (-) → Pin 8 (GND)
White (sig) → Pin 1 (4-20mA in)
ST-Link V2 Programming Interface:
ST-Link Pin → EC-M12 Header
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SWCLK → CLK
SWDIO → DIO
GND → GROUND
3.3V → (leave disconnected)
RST → RESET
Power Supply Architecture
The EC-M12-BC-C6-C-A incorporates sophisticated power management:
Battery Configuration:
- 2× ER34615H lithium thionyl chloride cells
- Parallel connection: 3.6V @ 38, 000mAh
- Pulse current capability: 600mA (2× 300mA per cell)
- Operating temperature: -60°C to +85°C
1. 1000µF Tantalum Capacitor
- Located near SIM7070 modem
- Buffers 840mA current spikes during network registration
- Prevents voltage sag below modem's 3.0V minimum
2. Boost Converter (12V Output)
- Generates 12V for 4-20mA sensor power
- Software-controlled enable (PA15)
- Disabled during sleep to eliminate quiescent current
- Max output current: 150mA
3. Low-Dropout Regulators
- 3.3V LDO for STM32L0 and peripherals
- Ultra-low quiescent current (<5µA) for sleep mode
- Thermal shutdown protection
Wake → Enable LDO → Init STM32 → Power modem → Enable booster →
Read sensor → Transmit data → Disable booster → Power down modem → Sleep
Jumper ConfigurationJP1 - Power Source Selection:
- Position 1: Battery power (field deployment)
- Position 2: USB power (programming and testing)
Important: Always use USB power (Position 2) during initial programming to preserve battery charge
CodeComplete Project RepositoryGitHub Repository: NORVI EC-M12-BC-C6-C-A 4-20mA Sensor Application
Contains:
- Complete Arduino sketch
- Hardware configuration files
- ThingsBoard dashboard templates
- Battery life calculation spreadsheet
- Power consumption analysis tools
System Initialization
#include <STM32LowPower.h>
#include <SIM7070.h>
#include <Adafruit_ADS1X15.h>
#include <PubSubClient.h>
// Hardware pin definitions
#define MODEM_PWRKEY PA8
#define BOOSTER_ENABLE PA15
#define LED_STATUS PC13
#define ADC_SDA PB7
#define ADC_SCL PB6
// Network configuration
#define APN "iot.1nce.net" // Adjust for your carrier
#define MQTT_SERVER "thingsboard.cloud"
#define MQTT_PORT 1883
#define DEVICE_TOKEN "YOUR_DEVICE_TOKEN"
// Timing configuration
#define SLEEP_MINUTES 15 // Adjust for desired battery life
// Global objects
SIM7070 modem;
Adafruit_ADS1115 ads;
WiFiClient netClient;
PubSubClient mqtt(netClient);
void setup() {
// Initialize serial for debugging
Serial.begin(115200);
// Configure GPIO pins
pinMode(MODEM_PWRKEY, OUTPUT);
pinMode(BOOSTER_ENABLE, OUTPUT);
pinMode(LED_STATUS, OUTPUT);
// Ensure peripherals start disabled
digitalWrite(BOOSTER_ENABLE, LOW);
digitalWrite(LED_STATUS, LOW);
// Initialize low-power framework
LowPower.begin();
// Initialize I2C ADC
Wire.begin();
if (!ads.begin(0x48)) {
Serial.println("ADS1115 not found!");
while(1);
}
// Configure ADC for 4-20mA measurement
// Gain: ±4.096V, Resolution: 0.125mV
ads.setGain(GAIN_ONE);
ads.setDataRate(RATE_ADS1115_128SPS);
// Initialize cellular modem
modem.begin();
// Connect to cellular network
connectNetwork();
// Setup MQTT connection
mqtt.setServer(MQTT_SERVER, MQTT_PORT);
mqtt.setCallback(mqttCallback);
}Cellular Network Connection
bool connectNetwork() {
Serial.println("Initializing modem...");
// Power on modem
digitalWrite(MODEM_PWRKEY, HIGH);
delay(500);
digitalWrite(MODEM_PWRKEY, LOW);
delay(3000); // Wait for modem boot
// Wait for network registration
int attempts = 0;
while (!modem.isNetworkRegistered() && attempts < 60) {
delay(1000);
attempts++;
Serial.print(".");
}
if (!modem.isNetworkRegistered()) {
Serial.println("Network registration failed!");
return false;
}
Serial.println("\nNetwork registered");
// Activate GPRS/LTE data
if (!modem.activateDataConnection(APN)) {
Serial.println("Data activation failed!");
return false;
}
Serial.println("Data connection active");
return true;
}4-20mA Sensor Reading
struct SensorReading {
float current; // mA
float fuelPercent; // 0-100%
float fuelHeight; // meters
bool fault; // true if sensor error
String faultType; // "open", "short", or "none"
};
SensorReading readFuelSensor() {
SensorReading reading;
// Enable 12V booster for sensor power
digitalWrite(BOOSTER_ENABLE, HIGH);
delay(500); // Stabilization time
// Read ADC (differential input for current sensing)
int16_t adcValue = ads.readADC_Differential_0_1();
// Convert ADC reading to voltage
// ADS1115: 16-bit, ±4.096V range = 0.125mV/bit
float voltage = adcValue * 0.000125; // Volts
// Convert voltage to current using sense resistor
// 249Ω sense resistor (1% tolerance)
reading.current = (voltage / 249.0) * 1000.0; // mA
// Detect sensor faults
if (reading.current < 3.8) {
reading.fault = true;
reading.faultType = "open";
reading.fuelPercent = -1;
reading.fuelHeight = -1;
}
else if (reading.current > 20.5) {
reading.fault = true;
reading.faultType = "short";
reading.fuelPercent = -1;
reading.fuelHeight = -1;
}
else {
reading.fault = false;
reading.faultType = "none";
// Convert 4-20mA to 0-100% fuel level
// 4mA = 0%, 20mA = 100%
reading.fuelPercent = ((reading.current - 4.0) / 16.0) * 100.0;
// Calculate fuel height (assuming 2-meter tank)
reading.fuelHeight = (reading.fuelPercent / 100.0) * 2.0;
// Clamp values to valid range
reading.fuelPercent = constrain(reading.fuelPercent, 0, 100);
reading.fuelHeight = constrain(reading.fuelHeight, 0, 2.0);
}
// Disable booster to save power
digitalWrite(BOOSTER_ENABLE, LOW);
// Debug output
Serial.print("Current: "); Serial.print(reading.current); Serial.println(" mA");
Serial.print("Fuel: "); Serial.print(reading.fuelPercent); Serial.println(" %");
Serial.print("Height: "); Serial.print(reading.fuelHeight); Serial.println(" m");
return reading;
}Battery Voltage Monitoring
float readBatteryVoltage() {
// Battery voltage is connected to ADC channel 3 via 2:1 divider
int16_t adcValue = ads.readADC_SingleEnded(3);
// Convert to actual voltage
float voltage = (adcValue * 0.000125) * 2.0; // Account for divider
Serial.print("Battery: "); Serial.print(voltage); Serial.println(" V");
return voltage;
}MQTT Data Publishing
bool publishTelemetry() {
// Ensure MQTT connected
if (!mqtt.connected()) {
Serial.println("Connecting to MQTT...");
if (!mqtt.connect("EC-M12-Device", DEVICE_TOKEN, NULL)) {
Serial.println("MQTT connection failed!");
return false;
}
}
// Read sensors
SensorReading fuel = readFuelSensor();
float battery = readBatteryVoltage();
// Construct JSON payload
String payload = "{";
payload += "\"fuel_level\":" + String(fuel.fuelPercent, 1) + ",";
payload += "\"fuel_height\":" + String(fuel.fuelHeight, 2) + ",";
payload += "\"sensor_current\":" + String(fuel.current, 2) + ",";
payload += "\"battery_voltage\":" + String(battery, 2) + ",";
payload += "\"sensor_fault\":" + String(fuel.fault ? "true" : "false") + ",";
payload += "\"fault_type\":\"" + fuel.faultType + "\"";
payload += "}";
Serial.println("Publishing: " + payload);
// Publish to ThingsBoard
bool success = mqtt.publish("v1/devices/me/telemetry", payload.c_str());
if (success) {
Serial.println("Data published successfully");
digitalWrite(LED_STATUS, HIGH);
delay(100);
digitalWrite(LED_STATUS, LOW);
} else {
Serial.println("Publish failed!");
}
// Allow time for acknowledgment
mqtt.loop();
delay(1000);
return success;
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
// Handle incoming MQTT messages (if needed for remote control)
Serial.print("Message received [");
Serial.print(topic);
Serial.print("]: ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}Ultra-Low-Power Sleep Mode
void enterDeepSleep(uint32_t minutes) {
Serial.print("Entering sleep for ");
Serial.print(minutes);
Serial.println(" minutes...");
delay(100); // Flush serial
// Disconnect MQTT
mqtt.disconnect();
// Power down modem
modem.powerDown();
delay(1000);
// Disable all peripherals
digitalWrite(BOOSTER_ENABLE, LOW);
digitalWrite(LED_STATUS, LOW);
// Close I2C, SPI, UART
Wire.end();
SPI.end();
Serial.end();
// Configure RTC wakeup timer
uint32_t sleepMillis = minutes * 60 * 1000;
// Enter STM32L0 shutdown mode
// Current consumption: ~1µA
// Only RTC remains active
LowPower.shutdown(sleepMillis);
// After wake, execution restarts from setup()
}Main Loop
void loop() {
// Ensure network connectivity
if (!modem.isNetworkRegistered()) {
Serial.println("Network lost, reconnecting...");
connectNetwork();
}
// Publish sensor data to cloud
publishTelemetry();
// Enter deep sleep to conserve battery
enterDeepSleep(SLEEP_MINUTES);
// Note: After wake, system restarts from setup()
// This loop will not execute again in same session
}Build InstructionsStep 1: Initial Hardware Setup
1.1 Power Configuration
- Locate jumper JP1 on EC-M12-BC-C6-C-A board
- Set to Position 2 (USB power) for programming
- Do not insert batteries yet
1.2 ST-Link Connection
- Connect the ST-Link V2 programmer to EC-M12 programming header:
ST-Link → EC-M12━━━━━━━━━━━━━━━━━SWCLK → CLKSWDIO → DIOGND → GROUNDRST → RESET
- Connect ST-Link to computer via USB
- Connect EC-M12 to computer via USB (for serial monitoring)
1.3 Sensor Wiring
- Connect 4-20mA fuel sensor to 8-pin terminal block:
Sensor → Terminal━━━━━━━━━━━━━━━━━Red → Pin 7 (12V)Black → Pin 8 (GND)White → Pin 1 (4-20mA input)
Step 2: Arduino IDE Configuration
2.1 Install STM32 Board Support
- Open Arduino IDE
- Go to File → Preferences
- Add to "Additional Boards Manager URLs"
- Open Tools → Board → Boards Manager
- Search "STM32" and install "STM32 MCU-based boards" by STMicroelectronics
2.2 Install Required Libraries
Via Library Manager (Sketch → Include Library → Manage Libraries):
- STM32LowPower (by STMicroelectronics)
- Adafruit ADS1X15 (by Adafruit)
- PubSubClient (by Nick O'Leary)
Manual Installation for SIM7070 Library:
- Download from NORVI GitHub repository
- Extract to Arduino/libraries folder
- Restart Arduino IDE
2.3 Board Configuration
- Tools → Board → STM32 Boards → Generic STM32L0 Series
- Tools → Board Part Number → STM32L072RBTx
- Tools → Upload Method → STM32CubeProgrammer (SWD)
- Tools → USB Support → "CDC (generic 'Serial' supersede U(S)ART)"
- Tools → U(S)ART Support → "Enabled (generic 'Serial')"
- Tools → Optimize → "Smallest (-Os default)"
Step 3: ThingsBoard Cloud Setup
3.1 Create ThingsBoard Account
- Navigate to https://thingsboard.cloud
- Sign up for free tier account
- Verify email and log in
3.2 Add Device
- Go to Devices → Add Device (+)
- Name: "Fuel Tank Monitor 01"
- Device Profile: "default"
- Click "Add"
- Copy the Device Access Token (you'll need this)
3.3 Configure Dashboard
Create a new dashboard with widgets:
Fuel Level Gauge:
- Widget type: Analog Gauges → Radial Gauge
- Data key: fuel_level
- Min: 0, Max: 100
- Units: %
- Color ranges: 0-20 (red), 20-50 (yellow), 50-100 (green)
Fuel Height Display:
- Widget type: Cards → Simple Card
- Data key: fuel_height
- Units: meters
- Decimals: 2
Battery Voltage Chart:
- Widget type: Charts → Timeseries Line Chart
- Data key: battery_voltage
- Time window: Last 7 days
- Y-axis: 2.5 - 4.0V
Sensor Fault Indicator:
- Widget type: Alarm Widgets → Alarms Table
- Filter: sensor_fault = true
- Severity: Critical
Step 4: Firmware Configuration
4.1 Download Project Code
- Download
4.2 Update Configuration
Edit the main sketch file:
// Network Configuration
#define APN "iot.1nce.net" // Replace with your carrier APN
// ThingsBoard Configuration
#define MQTT_SERVER "thingsboard.cloud"
#define DEVICE_TOKEN "YOUR_DEVICE_ACCESS_TOKEN_HERE" // Paste token from Step 3.2
// Sleep Configuration
#define SLEEP_MINUTES 15 // Adjust for desired battery lifeAPN Configuration Guide:
| Carrier | APN |
|---------------|----------------|
| 1NCE (Global IoT) | iot.1nce.net |
| Hologram | hologram |
| AT&T IoT | m2m.com.attz |
| Verizon IoT | vzwinternet |
| Vodafone IoT | iot.vodafone.com |
Step 5: Upload Firmware
5.1 Compile Sketch
- Open sketch in Arduino IDE
- Click Verify (✓) to check for errors
- If compilation successful, click Upload (→)
STM32CubeProgrammer Method:
- Open STM32CubeProgrammer
- Connect via ST-Link (click "Connect")
- Download → Browse for.bin file
- Start Address: 0x08000000
- Click "Start Programming"
Step 6: Initial Testing
6.1 Serial Monitor Verification
- Open Arduino IDE Serial Monitor (Tools → Serial Monitor)
- Set baud rate to 115200
- Press reset button on EC-M12
- You should see:
Initializing modem...Network registeredData connection activeCurrent: 12.35 mAFuel: 51.9 %Height: 1.04 mBattery: 3.58 VPublishing: {...}Data published successfullyEntering sleep for 15 minutes...6.2 ThingsBoard Verification
- Log in to ThingsBoard
- Navigate to Devices → Fuel Tank Monitor 01
- Check "Latest Telemetry" tab
- You should see fresh data within 1-2 minutes
- Open dashboard to view visualizations
Step 7: Field Deployment
7.1 Battery Installation
- Disconnect USB and ST-Link
- Move jumper JP1 to Position 1 (Battery power)
- Insert two ER34615H batteries into holders
- Verify polarity: + terminal to positive contact
7.2 Enclosure Installation
- Mount EC-M12 in weatherproof enclosure (IP65 rated)
- Route sensor cables through cable glands
- Include desiccant pack to prevent condensation
- Seal all openings with thread sealant
7.3 Antenna Placement
- SIM7070 modem requires LTE antenna
- Keep antenna vertical for best signal
- Avoid metal obstacles within 10cm
- For underground tanks, mount antenna above ground
7.4 Final System Check
- Verify data appears on ThingsBoard within 2 minutes
- Monitor battery voltage (should be 3.5-3.6V)
- Check sensor_fault status (should be false)
- Confirm fuel level readings match expected values
Power Consumption Verification
Equipment Required:
- Digital multimeter with mA/µA range
- 10Ω current sense resistor (optional for oscilloscope)
- USB power supply (5V, 2A minimum)
Procedure:
1. Measure Sleep Current
Setup: Battery (+) → Multimeter (µA mode) → EC-M12 (+)
EC-M12 (-) → Battery (-)
Expected: 1-2 µA average
2-3 mA occasional spikes (RTC activity)2. Measure Active Current
Setup: Same as above, but multimeter in mA mode
Expected during data transmission:
- 15-20 mA baseline
- 100-200 mA during MQTT publish
- 400-800 mA during modem network registration3. Capture Power Profile (Optional)
Using oscilloscope and current probe:
Probe: Current probe on battery positive rail
Timebase: 1 second/division
Trigger: Rising edge, 10mA threshold
You should see:
- 60-second modem init phase (17mA avg, 800mA peaks)
- 7-second data transmission phase (11mA avg)
- Drop to near-zero during sleepSensor Calibration
The 4-20mA sensor outputs current proportional to fuel level:
Standard Calibration Points:
Fuel Level | Current | ADC Reading | Expected Output
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0% (Empty) | 4.00 mA | ~12,900 | 0.0%
25% | 8.00 mA | ~25,800 | 25.0%
50% (Half) | 12.00 mA| ~38,700 | 50.0%
75% | 16.00 mA| ~51,600 | 75.0%
100% (Full)| 20.00 mA| ~64,500 | 100.0%Calibration Procedure:
1. Empty Tank Calibration
- Drain tank to known empty state
- Record ADC reading
- Should be approximately 12, 900 (4mA)
- If different, adjust offset in code:
float offsetCurrent = 4.0; // Adjust to match actual empty readingreading.fuelPercent = ((reading.current - offsetCurrent) / 16.0) * 100.0;2. Full Tank Calibration
- Fill tank to known full state
- Record ADC reading
- Should be approximately 64, 500 (20mA)
- If different, adjust span in code:
float spanCurrent = 16.0; // Adjust (full - empty) current rangereading.fuelPercent = ((reading.current - 4.0) / spanCurrent) * 100.0;3. Verification
- Test at multiple intermediate levels (25%, 50%, 75%)
- Compare sensor reading to manual dipstick measurement
- Accuracy should be within ±2% of full scale
| Problem | Possible Cause | Solution |
|------------------|------------------------|----------------------------|
| Reading stuck at 0% | Open circuit | Check sensor wiring |
| Reading stuck at 100% | Short circuit | Check for damaged cable |
| Erratic readings | Poor ground connection | Verify GND continuity |
| Readings drift | Temperature effect | Use sensor with temp compensation |
| Offset error | Sense resistor tolerance | Adjust offset in code |Network Performance TestingSignal Strength Check:
// Add to setup() function for testing
void printNetworkInfo() {
int rssi = modem.getSignalQuality();
String networkType = modem.getNetworkType();
Serial.print("Signal Strength (RSSI): ");
Serial.println(rssi);
Serial.print("Network Type: ");
Serial.println(networkType);
if (rssi < 10) {
Serial.println("WARNING: Weak signal - battery life will be reduced");
}
}Expected Signal Levels:
RSSI Value | Signal Quality | Impact on Battery Life
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0-9 | Poor | +40% power consumption
10-14 | Fair | +20% power consumption
15-19 | Good | Normal operation
20-31 | Excellent | Optimal power efficiencyNetwork Registration Time Test:
- Good signal: 30-45 seconds
- Fair signal: 45-75 seconds
- Poor signal: 75-120 seconds (or failure)
Poor signal areas may require repositioning the antenna or choosing a different carrier.
Optimization StrategiesBattery Life Optimization
1. Adjust Transmission Interval
Match your interval to application requirements:
// High-priority monitoring (fuel deliveries expected)
#define SLEEP_MINUTES 15 // 3.36 years battery life
// Standard monitoring (stable consumption)
#define SLEEP_MINUTES 30 // 6.5 years battery life
// Low-priority monitoring (backup tank)
#define SLEEP_MINUTES 60 // 11.4 years battery life2. Implement Adaptive Intervals
Only transmit when fuel level changes significantly:
float lastFuelLevel = -1;
#define CHANGE_THRESHOLD 5.0 // 5% change triggers transmission
void loop() {
SensorReading fuel = readFuelSensor();
// Only transmit if significant change
if (lastFuelLevel < 0 || abs(fuel.fuelPercent - lastFuelLevel) > CHANGE_THRESHOLD) {
publishTelemetry();
lastFuelLevel = fuel.fuelPercent;
} else {
Serial.println("No significant change, skipping transmission");
}
enterDeepSleep(SLEEP_MINUTES);
}This can extend battery life by 2-3× in stable tank applications.
3. Enable Power-Saving Mode
Configure SIM7070 for maximum efficiency:
void setupModemPowerSaving() {
// Enable PSM (Power Saving Mode)
modem.setPowerSavingMode(true);
// Enable eDRX (Extended Discontinuous Reception)
modem.seteDRX(true, 5); // 5-second cycle
// Reduce TX power if signal is strong
int rssi = modem.getSignalQuality();
if (rssi > 20) {
modem.setTxPower(5); // Minimum power
}
}4. Optimize MQTT Payload
Reduce data size to minimize transmission time:
// Verbose payload: 156 bytes
{"fuel_level_percentage":87.5,"fuel_height_meters":1.75,"battery_voltage":3.58,"sensor_fault_status":false}
// Optimized payload: 45 bytes
{"fl":87.5,"fh":1.75,"bv":3.58,"sf":0}
// Savings: 71% reduction in data size// Verbose payload: 156 bytes
{"fuel_level_percentage":87.5,"fuel_height_meters":1.75,"battery_voltage":3.58,"sensor_fault_status":false}
// Optimized payload: 45 bytes
{"fl":87.5,"fh":1.75,"bv":3.58,"sf":0}Smaller payloads = faster transmission = less power consumption.
Environmental ConsiderationsTemperature Compensation
Lithium batteries lose capacity in extreme cold:
float getTemperatureDerating() {
float temp = readTemperature(); // Add temperature sensor
if (temp > 20) return 1.0; // Full capacity
if (temp > 0) return 0.95; // 5% reduction
if (temp > -10) return 0.85; // 15% reduction
if (temp > -20) return 0.75; // 25% reduction
return 0.60; // 40% reduction below -20°C
}
// Adjust battery life estimate
float effectiveCapacity = 38000 * getTemperatureDerating();Weatherproofing Best Practices1. Enclosure Selection
- Minimum IP65 rating (dust-tight, water jet resistant)
- Polycarbonate or ABS material
- UV-resistant for outdoor installations
- Vented to prevent condensation
2. Cable Entry
- Use PG glands with O-rings
- Apply thread sealant (not Teflon tape)
- Route cables downward to prevent water ingress
- Leave drip loops before entry points
3. Antenna Mounting
- External antenna for metal enclosures
- Vertical orientation for omnidirectional coverage
- Lightning arrestor for exposed installations
- Keep clear of metal structures (>10cm clearance)
Common Issues and Solutions
Problem: Device not connecting to cellular network
Symptoms:
- Modem initialization exceeds 60 seconds
- Serial output shows "Network registration failed"
- No data appears on ThingsBoard
Solutions:
- Verify SIM card is activated and has data plan
- Check APN settings match your carrier
- Test cellular coverage with smartphone at same location
- Try different network mode (NB-IoT vs LTE-M):
modem.setPreferredMode(NBIOT_MODE); // or LTE_M_MODE- Increase connection timeout to 120 seconds
- Check antenna connection (SMA connector tight)
Problem: Current consumption higher than expected
Symptoms:
- Sleep current >10µA
- Battery drains in months instead of years
- Device feels warm during sleep
Diagnostic Steps:
// Add debug code to verify peripheral shutdown
void verifyPowerDown() {
Serial.println("Checking power-down sequence:");
Serial.print("Booster EN: ");
Serial.println(digitalRead(BOOSTER_ENABLE) == LOW ? "OFF" : "ON");
Serial.print("Modem power: ");
Serial.println(modem.isPoweredOn() ? "ON" : "OFF");
// Should all be OFF/false before sleep
}Common Causes:
- Booster not disabled - Verify digitalWrite(BOOSTER_ENABLE, LOW) called
- Modem still powered - Call modem.powerDown() before sleep
- Peripherals still active - Call Wire.end(), Serial.end(), etc.
- Poor solder joint - Check for current leakage on PCB
- Incorrect sleep mode - Use LowPower.shutdown() not LowPower.sleep()
Measurement Procedure:
- Remove EC-M12 from enclosure
- Disconnect sensor (to isolate device current)
- Connect ammeter in series with battery positive
- Wait through complete cycle (wake → transmit → sleep)
- Record minimum current during sleep phase
- Should be <2µA (excluding brief RTC spikes)
Problem: Sensor readings incorrect or erratic
Symptoms:
- Fuel level stuck at 0% or 100%
- Readings fluctuate wildly
- "Sensor fault" error constantly triggered
Diagnosis Table:
| Symptom | Reading | Likely Cause | Solution |
|----------------|---------------|-------------------|------------------------------|
| Stuck at 0% | <4mA | Open circuit | Check wiring continuity |
| Stuck at 100% | >20mA | Short circuit | Inspect cable for damage |
| Erratic ±10% | Varying | Loose connection | Tighten terminal screws |
| Slow drift | Gradual change | Temperature effect | Add temp compensation |
| Noisy readings | ±2% fluctuation| EMI interference | Add ferrite bead to cable |Verification Steps:
- Measure sensor current with multimeter in series
- Should be 4-20mA proportional to level
- Disconnect sensor and measure ADC voltage directly
- Should be 0.996V to 4.98V (for 249Ω sense resistor)
- Check sense resistor value (should be 249Ω ±1%)
Problem: MQTT connection fails
Symptoms:
- Serial output: "MQTT connection failed!"
- Modem connected to network but no data on ThingsBoard
- Publishing returns false
Checklist:
- [ ] ThingsBoard device access token correct in code
- [ ] MQTT server address correct ("thingsboard.cloud" or custom)
- [ ] Port 1883 open (check firewall if using VPN)
- [ ] Device exists in ThingsBoard and is active
- [ ] Network time synchronized (some MQTT servers require accurate time)
Debug MQTT Connection:
void debugMQTT() {
Serial.println("MQTT Debug:");
Serial.print("Server: ");
Serial.println(MQTT_SERVER);
Serial.print("Port: ");
Serial.println(MQTT_PORT);
Serial.print("Client ID: EC-M12-Device");
Serial.print("Token: ");
Serial.println(DEVICE_TOKEN);
int result = mqtt.connect("EC-M12-Device", DEVICE_TOKEN, NULL);
Serial.print("Connection result: ");
Serial.println(result); // 0=success, negative=error code
}MQTT Error Codes:
- 0: Success
- -1: Connection refused - incorrect protocol version
- -2: Connection refused - identifier rejected
- -3: Connection refused - server unavailable
- -4: Connection refused - bad username/password (token)
- -5: Connection refused - not authorized
Problem: Battery voltage dropping faster than expected
Symptoms:
- Battery reads <3.4V after 6 months
- Voltage drops >0.1V per month
- Device resets unexpectedly
Diagnostic:
void batteryHealthCheck() {
float voltage = readBatteryVoltage();
Serial.print("Battery voltage: ");
Serial.print(voltage);
Serial.println(" V");
if (voltage < 3.0) {
Serial.println("CRITICAL: Replace batteries immediately!");
} else if (voltage < 3.2) {
Serial.println("WARNING: Low battery - replace soon");
} else if (voltage < 3.4) {
Serial.println("NOTICE: Battery at 50% capacity");
} else {
Serial.println("Battery healthy");
}
}Causes:
- High transmission frequency - Reduce from 15min to 60min intervals
- Poor cellular signal - Modem uses more power in weak signal areas
- Sensor draws high current - Verify 4-20mA sensor not exceeding 20mA
- Battery manufacturing defect - Replace with fresh ER34615H cells
- Temperature extremes - Cold weather reduces effective capacity
Battery Life Calculation Tool: Use actual measured currents to predict remaining life:
void estimateRemainingLife() {
float batteryVoltage = readBatteryVoltage();
float remainingCapacity = 38000 * ((batteryVoltage - 2.8) / (3.6 - 2.8));
float avgCurrent = 1.29; // mA for 15-min interval
float remainingHours = remainingCapacity / avgCurrent;
float remainingDays = remainingHours / 24;
Serial.print("Estimated remaining: ");
Serial.print(remainingDays);
Serial.println(" days");
}Real-World Deployment Case StudiesCase Study 1: Remote Construction Site Fuel Monitoring
Application: 15 diesel fuel tanks across construction sites in rural areas
Configuration:
- Transmission interval: 30 minutes
- 4-20mA float-style level sensors
- NB-IoT connectivity (better penetration in remote areas)
- Weatherproof NEMA 4X enclosures
Results After 18 Months:
- Zero maintenance visits required
- Battery voltage: 3.42V (95% original capacity)
- Projected total life: 6+ years
- Prevented 3 fuel theft incidents (real-time alerts)
- Saved 45 manual inspection trips ($6, 750 labor cost)
Lessons Learned:
- Solar panel not needed for 6+ year target
- NB-IoT more reliable than LTE-M in rural areas
- 30-minute interval provides good balance of data freshness and battery life
- Antenna placement critical—above-ground mounting required for underground tanks
Case Study 2: Industrial Chemical Storage Compliance
Application: Hazardous chemical level monitoring for regulatory compliance
Configuration:
- Transmission interval: 15 minutes (regulatory requirement)
- Explosion-proof enclosure (Class I, Div 1)
- Redundant 4-20mA pressure transducers
- LTE-M connectivity
- SMS alerts for fault conditions
Results After 12 Months:
- 100% uptime for compliance reporting
- Battery voltage: 3.48V
- Projected total life: 3.5 years
- Avoided 2 overflow incidents ($40, 000+ cleanup costs)
- Passed safety audit with continuous monitoring evidence
Lessons Learned:
- Fault detection critical for safety applications
- Redundant sensors worth the added complexity
- More frequent transmission (15 min) still achieves multi-year life
- Explosion-proof enclosure adds significant cost but required for hazmat
Case Study 3: Agricultural Water Tank Network
Application: 50 livestock water tanks across 5, 000-acre ranch
Configuration:
- Transmission interval: 2 hours (water consumption is gradual)
- Ultrasonic level sensors (non-contact, easier installation)
- Mix of NB-IoT and LTE-M based on coverage
- Solar panel supplement for indefinite operation
Results After 24 Months:
- 48 of 50 units still operational (2 lightning strikes)
- Battery voltage: 3.55V (solar keeps batteries topped off)
- Detected 7 tank leaks before animals were affected
- Ranch hands save 15 hours/week on manual tank checks
Lessons Learned:
- 2-hour interval more than sufficient for slow-changing levels
- Solar panel + battery = indefinite operation for accessible locations
- Lightning protection essential for open-field installations
- LoRaWAN considered but cellular coverage adequate
Over-the-Air (OTA) Firmware Updates
While not implemented in base firmware, OTA updates are possible:
// Conceptual OTA implementation
void checkForUpdates() {
if (mqtt.connected()) {
// Subscribe to firmware update topic
mqtt.subscribe("v1/devices/me/firmware");
// Download new firmware to flash memory
// Verify checksum
// Write to bootloader area
// Set flag to trigger update on next reset
}
}Considerations:
- Requires bootloader support (STM32 built-in bootloader compatible)
- Large downloads consume significant battery (plan for battery life impact)
- Failed updates can brick device (implement rollback mechanism)
- Test thoroughly before deploying to production units
Multi-Sensor Support
EC-M12-BC-C6-C-A has multiple input channels:
// Read multiple tanks from one device
struct MultiTankReading {
float tank1_level;
float tank2_level;
float tank3_level;
float tank4_level;
};
MultiTankReading readAllTanks() {
MultiTankReading tanks;
// ADC has 4 single-ended inputs
tanks.tank1_level = readFuelLevel(0); // ADC channel 0
tanks.tank2_level = readFuelLevel(1); // ADC channel 1
tanks.tank3_level = readFuelLevel(2); // ADC channel 2
tanks.tank4_level = readFuelLevel(3); // ADC channel 3 (or battery)
return tanks;
}Predictive Maintenance
Implement trend analysis for proactive alerts:
// Store last 7 days of readings
float fuelHistory[168]; // 7 days * 24 hours
int historyIndex = 0;
void analyzeTrend() {
// Calculate consumption rate
float dailyChange = (fuelHistory[0] - fuelHistory[24]) / 24.0;
// Predict days until empty
float daysUntilEmpty = fuelHistory[0] / abs(dailyChange);
if (daysUntilEmpty < 7) {
// Send alert for refill
String alert = "{\"alert\":\"Fuel low - refill needed in " +
String(daysUntilEmpty) + " days\"}";
mqtt.publish("v1/devices/me/attributes", alert.c_str());
}
}Integration with Enterprise Systems
Connect to existing infrastructure:
REST API Integration:
// HTTP POST instead of MQTT
void publishViaHTTP() {
HTTPClient http;
http.begin("https://your-erp-system.com/api/fuel-levels");
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer YOUR_TOKEN");
String payload = "{\"tank_id\":\"TANK-001\",\"level\":" + String(fuelLevel) + "}";
int httpCode = http.POST(payload);
http.end();
}Database Direct Insert:
// Some cloud MQTT brokers can auto-insert to PostgreSQL/MySQL
// Configure ThingsBoard rule chain to forward data to your databaseConclusion
The Norvi EC-M12-BC-C6-C-A demonstrates that ultra-low-power industrial IoT is not just theoretically possible—it's practical, economical, and reliable. By achieving 11.4 years of battery life with hourly data transmission, this platform solves the fundamental challenge of remote monitoring: minimizing total cost of ownership.
Key Takeaways:
- Duty cycling is the dominant factor in battery life
- STM32L0's shutdown mode (1µA) enables multi-year operation
- Cellular connectivity is viable for battery-powered applications
- Proper power management requires holistic system design
- Real-world deployments validate theoretical calculations
Whether you're monitoring fuel tanks, water levels, environmental conditions, or industrial processes, the principles demonstrated here apply across countless applications. The combination of efficient hardware, optimized firmware, and intelligent duty cycling creates a deploy-and-forget solution that delivers ROI through eliminated maintenance costs and reliable 24/7 monitoring.
Ready to deploy your own remote monitoring system? The complete source code, documentation, and technical support are available through Norvi's official channels.
License & Credits- Hardware: NORVI EC-M12-BC-C6-C-A Industrial IoT Controller
- Firmware: Open-source under MIT License
- Cloud Platform: ThingsBoard IoT Platform
- Documentation: Copyright © 2025 Norvi (Pvt) Ltd.













Comments