19  Prototyping Languages

19.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Compare IoT Programming Languages: Evaluate C/C++, Python, JavaScript, and Rust for embedded development
  • Select Languages by Constraint: Choose appropriate languages based on memory, real-time, and team factors
  • Write Cross-Platform Code: Understand language portability considerations
  • Evaluate Trade-offs: Balance development speed against runtime performance

Prototyping is building rough, working versions of your IoT device to test ideas quickly and cheaply. Think of it like building a model airplane before constructing the real thing – a prototype reveals problems when they are still easy and inexpensive to fix. Modern prototyping tools make it possible to go from idea to working device in days rather than months.

“Choosing a programming language for IoT is like choosing between different spoken languages,” said Max the Microcontroller. “C and C++ are the native languages of microcontrollers – fast, efficient, and close to the hardware. Most production firmware is written in C because it gives you precise control over every byte of memory and every microsecond of timing.”

Sammy the Sensor mentioned Python. “MicroPython is wonderful for prototyping! You can type commands interactively and see results instantly. Reading my temperature in Python is just one line: sensor.temperature. In C, it takes five lines of register manipulation.” Max agreed but added a caveat. “Python is 10 to 100 times slower than C and uses much more memory. Great for prototyping, but production devices with tight constraints usually need C.”

Lila the LED asked about JavaScript. “Node.js on devices like Espruino is fun for web developers who already know JavaScript. And Rust is the new kid – it prevents memory bugs at compile time, which is amazing for safety-critical IoT devices.” Bella the Battery summed it up. “Use Python for quick experiments, C for production firmware, and Rust when safety matters most. The best language depends on your project, your skills, and your constraints!”

Key Concepts

  • Firmware: Low-level software stored in a device’s non-volatile flash memory that directly controls hardware peripherals.
  • SDK (Software Development Kit): Collection of libraries, tools, and documentation provided by a platform vendor to accelerate application development.
  • RTOS (Real-Time Operating System): Lightweight OS providing task scheduling and timing guarantees for embedded systems with concurrent requirements.
  • Over-the-Air (OTA) Update: Mechanism for delivering new firmware to deployed devices without physical access or a cable connection.
  • Unit Test: Automated test verifying a single function or module in isolation, catching bugs before hardware integration.
  • CI/CD Pipeline: Automated build, test, and deployment workflow that validates firmware quality on every code change.
  • Hardware Abstraction Layer (HAL): Software interface decoupling application code from specific hardware, enabling portability across MCU variants.

19.2 Prerequisites

Before diving into this chapter, you should be familiar with:


19.3 C/C++ for Microcontrollers

Why C/C++:

  • Direct hardware control
  • Minimal overhead and high efficiency
  • Real-time determinism
  • Industry standard for embedded systems

Characteristics:

  • Manual memory management
  • Low-level hardware access
  • Compiled to machine code
  • Small binary sizes

Arduino C++ Example:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Adafruit_BME280 bme;

void setup() {
  Serial.begin(115200);
  if (!bme.begin(0x76)) {
    Serial.println("BME280 not found!");
    while (1);
  }
}

void loop() {
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;
  float humidity = bme.readHumidity();

  Serial.print("Temp: ");
  Serial.print(temp);
  Serial.print(" C, Pressure: ");
  Serial.print(pressure);
  Serial.print("hPa, Humidity: ");
  Serial.print(humidity);
  Serial.println("%");

  delay(2000);
}

Best For:

  • Resource-constrained microcontrollers
  • Real-time applications
  • Power-critical battery devices
  • Performance-sensitive applications

19.4 Python for IoT

Why Python:

  • Rapid development
  • Extensive libraries
  • Readable, maintainable code
  • Strong ecosystem for data analysis and ML

MicroPython: Subset of Python 3 running on microcontrollers (ESP32, ESP8266, Raspberry Pi Pico).

Example:

from machine import Pin, I2C
import bme280
import time

i2c = I2C(scl=Pin(22), sda=Pin(21))
bme = bme280.BME280(i2c=i2c)

while True:
    temp, pressure, humidity = bme.read_compensated_data()
    print(f"Temp: {temp/100:.2f} C, Pressure: {pressure/25600:.2f}hPa, Humidity: {humidity/1024:.2f}%")
    time.sleep(2)

CPython (Raspberry Pi): Full Python implementation on Linux-based systems.

Example:

import board
import adafruit_bme280

i2c = board.I2C()
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

while True:
    print(f"Temp: {bme280.temperature:.2f} C")
    print(f"Humidity: {bme280.humidity:.2f}%")
    print(f"Pressure: {bme280.pressure:.2f}hPa")
    time.sleep(2)

Best For:

  • Raspberry Pi and capable microprocessors
  • Rapid prototyping and experimentation
  • Data processing and analysis
  • Integration with ML frameworks

19.5 JavaScript/Node.js

Why JavaScript:

  • Familiar to web developers
  • Asynchronous I/O model
  • Rich ecosystem (npm packages)
  • Full-stack IoT (device to cloud to web app)

Johnny-Five Framework: JavaScript robotics and IoT platform.

Example:

const five = require("johnny-five");
const board = new five.Board();

board.on("ready", function() {
  const temperature = new five.Thermometer({
    controller: "BME280"
  });

  temperature.on("change", function() {
    console.log(`${this.celsius} C`);
  });
});

Best For:

  • Web developer-friendly IoT
  • Real-time applications (WebSockets)
  • Prototyping with rapid iteration
  • Projects integrating with web services

19.6 Rust for Embedded

Why Rust:

  • Memory safety without garbage collection
  • Zero-cost abstractions
  • Fearless concurrency
  • Growing embedded ecosystem

Example:

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{prelude::*, pac};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let gpioa = dp.GPIOA.split();
    let mut led = gpioa.pa5.into_push_pull_output();

    loop {
        led.toggle();
        cortex_m::asm::delay(8_000_000);
    }
}

Best For:

  • Safety-critical applications
  • Long-lived deployed systems
  • Projects requiring both performance and safety
  • Advanced embedded developers

19.7 Language Selection Criteria

19.7.1 By Resource Constraints

RAM Available Recommended Language
Severe (<16KB) C only
Limited (16-64KB) C/C++
Moderate (64KB-1MB) C/C++, MicroPython
Abundant (>1MB) Python, JavaScript, any

19.7.2 By Development Speed

Priority Best Choice
Rapid prototyping Python, JavaScript
Production optimization C/C++, Rust
Team familiarity Match to existing skills

19.7.3 By Team Skills

Team Background Recommended Path
Embedded engineers C/C++
Web developers JavaScript, then C
Data scientists Python
Systems programmers Rust

19.7.4 By Real-Time Requirements

Requirement Language Choice
Hard real-time (<1ms) C/C++, Rust
Soft real-time (1-100ms) Any with proper design
No real-time Python, JavaScript

19.8 Language Comparison Table

Factor C/C++ Python JavaScript Rust
Memory Footprint Tiny (2-20KB) Large (100-500KB) Medium (50-200KB) Small (10-50KB)
Development Speed Slow Fast Fast Medium
Safety Manual Automatic GC Automatic GC Compile-time
Learning Curve Medium Easy Easy Hard
Library Ecosystem Excellent Excellent Good Growing
Real-time Support Excellent Poor Poor Excellent

19.9 Worked Example: Choosing a Language for a Precision Agriculture Sensor Node

Scenario: An agritech startup is building a solar-powered soil monitoring node that measures moisture, temperature, pH, and electrical conductivity every 15 minutes, then transmits data via LoRa to a gateway 2 km away. The target is 5-year battery life with a 500 mAh solar-recharged LiPo. The team has 3 developers: one embedded C expert, one Python data scientist, and one full-stack JavaScript developer. Evaluate language choices for this project.

Step 1: Hardware Constraints

The chosen MCU is an ESP32-S3 with: - 512 KB SRAM, 8 MB flash - Target active current budget: 15 mA average (solar constraint) - Deep sleep current: 10 uA - Wake-measure-transmit cycle must complete in under 2 seconds (LoRa window)

Step 2: Language Benchmark on Target Hardware

Testing identical sensor-read-transmit cycle on the ESP32-S3:

Metric C (ESP-IDF) MicroPython Espruino (JS) Rust (no_std)
Boot to first measurement 85 ms 1,200 ms 890 ms 92 ms
Sensor read cycle (4 sensors) 12 ms 145 ms 98 ms 14 ms
LoRa packet assembly + send 340 ms 480 ms 410 ms 345 ms
Total wake cycle 437 ms 1,825 ms 1,398 ms 451 ms
RAM usage (runtime) 42 KB 187 KB 112 KB 48 KB
Flash usage (firmware) 380 KB 1,100 KB 680 KB 410 KB
Average current during cycle 68 mA 72 mA 70 mA 68 mA

Language Performance Impact on Battery Life: For the ESP32-S3 soil sensor with 500mAh battery:

Daily energy consumption (96 wake cycles at 15-min intervals):

C/Rust: Wake energy: \(96 \times 0.437s \times 68mA = 2,851 \text{ mAs} = 2.85 \text{ mAh}\) Sleep energy: \((86,400s - 96 \times 0.437s) \times 0.01mA = 86,358s \times 0.01mA = 864 \text{ mAs} = 0.86 \text{ mAh}\) \[E_{\text{daily}} = 2.85mAh + 0.86mAh = 3.71mAh\]

MicroPython: Wake energy: \(96 \times 1.825s \times 72mA = 12,614 \text{ mAs} = 12.6 \text{ mAh}\) Sleep energy: \((86,400s - 96 \times 1.825s) \times 0.01mA = 86,225s \times 0.01mA = 862 \text{ mAs} = 0.86 \text{ mAh}\) \[E_{\text{daily}} = 12.6mAh + 0.86mAh = 13.5mAh\]

Battery life with 500mAh and 80% depth-of-discharge:

  • C/Rust: \(\frac{400mAh}{3.71mAh} = 108\) days
  • MicroPython: \(\frac{400mAh}{13.5mAh} = 30\) days

MicroPython’s 4x longer wake time reduces battery life by 72%. For 5-year solar operation, this may prevent charging during winter months.

Step 3: Energy Impact Calculation

Energy per wake cycle = average current x cycle duration:

Language Cycle Duration Energy per Cycle Cycles per Day (every 15 min) Daily Wake Energy
C 437 ms 29.7 mAs (0.0082 mAh) 96 2,851 mAs (0.79 mAh)
MicroPython 1,825 ms 131.4 mAs (0.0365 mAh) 96 12,614 mAs (3.50 mAh)
Espruino 1,398 ms 97.9 mAs (0.0272 mAh) 96 9,398 mAs (2.61 mAh)
Rust 451 ms 30.7 mAs (0.0085 mAh) 96 2,947 mAs (0.82 mAh)

Deep sleep energy per day at 10 µA: - C: \((86,400s - 96 \times 0.437s) \times 0.01mA = 864 \text{ mAs} = 0.24 \text{ mAh}\) - MicroPython: \((86,400s - 96 \times 1.825s) \times 0.01mA = 862 \text{ mAs} = 0.24 \text{ mAh}\) - Espruino: \((86,400s - 96 \times 1.398s) \times 0.01mA = 863 \text{ mAs} = 0.24 \text{ mAh}\) - Rust: \((86,400s - 96 \times 0.451s) \times 0.01mA = 864 \text{ mAs} = 0.24 \text{ mAh}\)

Language Total Daily Energy 500 mAh Battery Days (no solar, 80% DoD) Meets 5-Year Target?
C 1.03 mAh 388 days Yes (with solar)
MicroPython 3.74 mAh 107 days Marginal (needs larger panel)
Espruino 2.85 mAh 140 days Marginal
Rust 1.06 mAh 377 days Yes (with solar)

MicroPython consumes 3.6x more energy per day than C. During cloudy winter weeks with minimal solar charging, this difference determines whether the node survives or dies.

Step 4: Implementation Examples

To illustrate the development experience differences, here are simplified sensor-read-transmit implementations in each language:

C (ESP-IDF):

#include <esp_wifi.h>
#include "bme280.h"
#include "lora.h"

void app_main() {
    bme280_init();
    float temp = bme280_read_temperature();
    float moisture = adc_read_soil_moisture();

    lora_init();
    uint8_t payload[8];
    memcpy(payload, &temp, 4);
    memcpy(payload + 4, &moisture, 4);
    lora_send(payload, 8);

    esp_deep_sleep(15 * 60 * 1000000ULL);  // 15 min
}

MicroPython:

from machine import Pin, I2C, ADC, deepsleep
import bme280
from sx127x import SX127x

i2c = I2C(scl=Pin(22), sda=Pin(21))
bme = bme280.BME280(i2c=i2c)
moisture = ADC(Pin(34))

lora = SX127x(spi_bus=1, pins={'dio0': 26, 'ss': 18})
temp = bme.temperature
payload = struct.pack('ff', temp, moisture.read() / 4095.0)
lora.send(payload)

deepsleep(15 * 60 * 1000)  # 15 min in ms

Development Time Comparison

Task C (ESP-IDF) MicroPython Rust
Initial sensor driver integration 3 days 0.5 days 2 days
LoRa communication stack 2 days 1 day 2 days
Power management (deep sleep) 2 days 1 day 2 days
OTA update mechanism 3 days 2 days 4 days
Error handling + watchdog 1 day 0.5 days 0.5 days (compiler enforced)
Total development time 11 days 5 days 10.5 days
Debugging time (estimate) 4 days 2 days 1 day (fewer runtime bugs)

Step 5: Recommended Strategy

Phase 1 (Weeks 1-3): Prototype in MicroPython. The data scientist leads rapid prototyping: validate sensor readings, calibrate pH probe, test LoRa range. MicroPython’s REPL allows interactive tuning of calibration coefficients. Cost: 5 dev-days.

Phase 2 (Weeks 4-7): Production firmware in C (ESP-IDF). The embedded engineer ports the validated algorithm to C, optimizing the wake cycle to 437 ms. The MicroPython prototype serves as a living specification. Cost: 15 dev-days.

Result: The team gets the best of both worlds – MicroPython’s development speed for validation, C’s efficiency for production. Total development: 20 dev-days vs. 15 days for C-only (but with higher confidence in sensor calibration) or 7 days for Python-only (but with 3.6x higher energy consumption that risks field failures).

Interactive: Language Battery Life Calculator

Explore how language choice affects battery life for your IoT project:

Tradeoff: Python vs C for Microcontrollers

Python (MicroPython):

  • 10x faster development
  • 10x larger memory footprint
  • 10x slower execution
  • Interactive REPL for debugging

C/C++:

  • Slower development
  • Minimal memory overhead
  • Maximum performance
  • Compile-test-debug cycle

Recommendation: Start with MicroPython for proof-of-concept, migrate to C when performance or memory becomes critical.

19.10 Knowledge Check

Use this decision matrix to select the appropriate language for your IoT project:

Constraint If… Choose…
RAM <32 KB C only
RAM 32-128 KB C or C++
RAM 128 KB - 512 KB C, C++, or MicroPython (tight)
RAM >512 KB Any language
Flash <128 KB C only
Flash 128-512 KB C or C++
Flash >512 KB Any language
Battery Life Years C or Rust (minimize wake time)
Battery Life Months C, C++, or Rust
Battery Life Days-weeks MicroPython acceptable
Real-time <1 ms hard deadline C or Rust with RTOS
Real-time 1-100 ms soft deadline Any language with proper design
Development Speed Proof-of-concept MicroPython or JavaScript
Development Speed Production firmware C, C++, or Rust
Team Skills Embedded engineers C or C++
Team Skills Web developers JavaScript (Node.js) or MicroPython
Team Skills Systems programmers Rust
Safety Critical Medical, automotive, industrial Rust (memory safety) or C with extensive testing
Deployment Scale 1-100 prototypes Any language
Deployment Scale 1,000+ production units C, C++, or Rust (optimize energy/cost)

Example application of framework:

Project: Wearable fitness tracker - RAM: 256 KB available - Battery: 200 mAh, target 7-day life - Team: 2 embedded engineers, 1 Python developer - Scale: 10,000 units/year

Decision path:

  1. RAM (256 KB) → MicroPython possible but tight
  2. Battery (7 days) → Prefer C/C++ for energy efficiency
  3. Team skills → C/C++ primary, MicroPython for rapid prototyping
  4. Scale (10K units) → Production optimization matters

Recommendation: Prototype in MicroPython (2 weeks), production in C++ (saves $2/unit in battery cost × 10K = $20K/year savings).

Common Mistake: Using Python for Production Battery-Powered Devices

The Problem: A team prototypes a soil moisture sensor in MicroPython on an ESP32. It works beautifully in the lab. They deploy 200 units to farmers. Within 2 weeks, 80% of batteries are dead, and customers are furious.

Why it failed:

Lab testing (USB-powered, infinite energy): - Boot: 3 seconds - Read sensor + transmit: 2 seconds - Total: 5 seconds every 15 minutes - Power source: USB (doesn’t matter)

Field reality (battery-powered, 2000 mAh):

MicroPython performance on ESP32: - Boot: 3.5 seconds @ 80 mA = 280 mAs = 0.078 mAh - Sensor read: 1.2 seconds @ 75 mA = 90 mAs = 0.025 mAh - LoRa transmit: 0.8 seconds @ 120 mA = 96 mAs = 0.027 mAh - Total per cycle: 466 mAs = 0.13 mAh

Deep sleep: 10 µA × 894 seconds = 8.9 mAs = 0.0025 mAh

Per 15-minute cycle: 0.13 + 0.0025 = 0.1325 mAh

Cycles per day: 96 (every 15 minutes)

Daily energy: 96 × 0.1325 = 12.72 mAh/day

Battery life: 2000 / 12.72 = 157 days (not great, but acceptable)

What actually happened:

The team didn’t realize MicroPython’s garbage collector runs periodically, causing unpredictable delays and power spikes. During transmission, GC triggered, extending wake time from 5 seconds to 15-20 seconds:

  • Extended wake time: 15 s @ 80 mA = 1200 mAs = 0.33 mAh
  • Daily energy: 96 × 0.33 = 31.7 mAh/day
  • Battery life: 63 days (vs promised 157 days)

In cold weather (5°C), LiPo capacity drops 30%: - Effective capacity: 2000 × 0.7 = 1400 mAh - Battery life: 44 days

In freezing weather (-5°C), LiPo capacity drops 50% and batteries stop charging: - Effective capacity: 2000 × 0.5 = 1000 mAh - Battery life: 31 days → then dead forever

The fix (rewrite in C):

C firmware with tight deep sleep control: - Boot: 0.8 s @ 70 mA = 56 mAs = 0.016 mAh - Sensor read: 0.3 s @ 65 mA = 19.5 mAs = 0.0054 mAh - LoRa transmit: 0.6 s @ 120 mA = 72 mAs = 0.02 mAh - Total per cycle: 0.041 mAh (3.2x better than MicroPython)

In 60 Seconds

This chapter covers prototyping languages, explaining the core concepts, practical design decisions, and common pitfalls that IoT practitioners need to build effective, reliable connected systems.

New battery life: 2000 / (96 × 0.041) = 508 days = 1.4 years

Even in cold weather (-5°C): 1000 / (96 × 0.041) = 254 days (acceptable)

Lesson: MicroPython is wonderful for prototyping (2-day development vs 2-week in C). But for battery-powered production devices, the 3-5x energy overhead is unacceptable. Prototype in Python, deploy in C.

Common Pitfalls

Adding too many features before validating core user needs wastes weeks of effort on a direction that user testing reveals is wrong. IoT projects frequently discover that users want simpler interactions than engineers assumed. Define and test a minimum viable version first, then add complexity only in response to validated user requirements.

Treating security as a phase-2 concern results in architectures (hardcoded credentials, unencrypted channels, no firmware signing) that are expensive to remediate after deployment. Include security requirements in the initial design review, even for prototypes, because prototype patterns become production patterns.

Designing only for the happy path leaves a system that cannot recover gracefully from sensor failures, connectivity outages, or cloud unavailability. Explicitly design and test the behaviour for each failure mode and ensure devices fall back to a safe, locally functional state during outages.

19.11 What’s Next

If you want to… Read this
Explore application domains for this technology Application Domains Overview
Learn about UX design for connected devices UX Design for IoT
Start prototyping with the concepts covered Prototyping Essentials