Hand on IoT Assignment

This Notebook is a wrapper on the IoT assignnment developmeent and testing tasks related with communication between the Python part and the Arduino part of your IoT application. The next sections provide some basic development tasks and integration tests aimed to kick off your IoT assignment using the templates. You can perform these basic tests iteratively during the development of your project, just to make sure that the basic functionalities are still in place whenever you introduce new functionality. It also provides some questions to make sure you understand the key concepts behind the templates.

Device set-up test

  1. Take a look at the Arduino set-up used to guide this lecture (available here. You can use this as a scaffold for your project. Let us take a look at the code of the example:

// ─────────────────────────────────────────────────────────
// Arduino: Minimal serial template (no external hardware)
// Commands:
// 0: Handshake
// 1: Uptime (millis)
// 2: Analog read A0 (floating pin ok)
// 3: Pseudo-random 0..1023
// 4: LED state (0/1)
// 5: LED toggle
// 6: LED on
// 7: LED off
// ─────────────────────────────────────────────────────────

const int PIN_LED = LED_BUILTIN;
char option = '\0';

// Read the supply voltage (Vcc) in millivolts using the internal 1.1V band-gap.
// Works on ATmega328P-based boards (UNO/Nano). No external wiring needed.
long readVcc() {
  // Select 1.1V band-gap as ADC input, Vcc as reference.
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  delay(2);                        // let the reference settle
  ADCSRA |= _BV(ADSC);             // start conversion
  while (ADCSRA & _BV(ADSC)) {}    // wait until done

  uint8_t low  = ADCL;
  uint8_t high = ADCH;
  uint16_t adc = (high << 8) | low;

  // Vcc (mV) ≈ 1.1V * 1023 * 1000 / adc
  return 1125300L / (long)adc;     // 1125300 ≈ 1.1 * 1023 * 1000
}


void setup() {
  Serial.begin(9600);
  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, LOW);
  // Optional: small startup banner
  Serial.println(F("READY;VER=1;BAUD=9600"));
}

void loop() {
  if (Serial.available() > 0) {
    option = Serial.read();

    switch (option) {
      case '0': { // Handshake
        Serial.print(F("ACK;BOARD=ARDUINO;BAUD=9600;UPTIME_MS="));
        Serial.println(millis());
        break;
      }
      case '1': { // Uptime
        Serial.print(F("UPTIME_MS:"));
        Serial.println(millis());
        break;
      }
      case '2': { // Built-in reading: Vcc (mV) via 1.1V band-gap
        long mv = readVcc();
        if (mv > 0) {
            Serial.print(F("VCC_mV:"));
            Serial.println(mv);
        } else {
            Serial.println(F("VCC:UNSUPPORTED"));
        }
        break;
      }
      case '3': { // LED state
        int state = digitalRead(PIN_LED);
        Serial.print(F("LED:"));
        Serial.println(state ? 1 : 0);
        break;
      }
      case '4': { // LED toggle
        int state = !digitalRead(PIN_LED);
        digitalWrite(PIN_LED, state);
        Serial.print(F("LED:"));
        Serial.println(state ? 1 : 0);
        break;
      }
      case '5': { // LED ON
        digitalWrite(PIN_LED, HIGH);
        Serial.println(F("LED:1"));
        break;
      }
      case '6': { // LED OFF
        digitalWrite(PIN_LED, LOW);
        Serial.println(F("LED:0"));
        break;
      }
      default: {
        Serial.println(F("ERR Unknown"));
        break;
      }
    }
  }
}

This is a minimal template that does not use any external hardware, just the built-in LED and the internal voltage sensor of the Arduino board, so you can use it as a reference or to test serial communication even if you do not have additional hardware.

Let us take a look at some key sections of the code:

Questions and Code cards

Handshake

What is the purpose of the handshake command?

Uptime

Why do you need it is useful to have an uptime command in your IoT application?

Card D1 -

what does this code do?

int state = !digitalRead(PIN_LED);
digitalWrite(PIN_LED, state);
Serial.print(F("LED:"));
Serial.println(state ? 1 : 0);

Python part template

Take a look at the Python part template (available here). This template provides a basic command line user interface to interact with the Arduino part through serial communication. It also includes a simulation mode that allows you to test the user interface without connecting to the actual hardware.

The cell below contains the code of the Python part template for your reference. If you are in Colab, you can run it directly using the simulation mode.

[ ]:
# ─────────────────────────────────────────────────────────
# Python: Serial CLI + Continuous CSV Logger
#  - Simple command-line interface to an Arduino over serial.
#  - Commands:
#    '0' = Handshake
#    '1' = Read uptime (ms)
#    '2' = Read Vcc (mV)
#    '3' = Read LED state (0/1)
#    '4' = Toggle LED
#    '5' = LED ON
#    '6' = LED OFF
#    'c' = continuous log to CSV (Ctrl+C to stop)
#    's' = set simulation mode
#    'q' = quit
#       - Logs: timestamp, uptime_ms, vcc_mv, rand, led
# Dependencies: pyserial
# ─────────────────────────────────────────────────────────
import serial
import time
import os
import csv
from datetime import datetime

# Set your serial port here (examples: 'COM7', '/dev/ttyACM0', '/dev/ttyUSB0')
PORT = None  # e.g., 'COM7' or '/dev/ttyACM0'
BAUD = 9600
TIMEOUT = 1.5  # seconds

def open_serial():
    if PORT is None:
        print("⚠️  Set PORT to your Arduino serial port (e.g., 'COM7' or '/dev/ttyACM0').")
        return None

    ser = serial.Serial(PORT, BAUD, timeout=TIMEOUT)
    time.sleep(1.8)             # allow auto-reset + banner
    ser.reset_input_buffer()     # drain startup text
    return ser


def send_cmd(ser, cmd_char):
    """Send single-char command; return one decoded line ('' on timeout)."""
    ser.write(cmd_char.encode("utf-8"))
    ser.flush()
    line = ser.readline()
    return line.decode("utf-8", errors="replace").strip()

def parse_metric(line, key):
    """Return value after 'KEY:' in 'KEY:VALUE' lines; else None."""
    prefix = key + ":"
    if line.startswith(prefix):
        return line[len(prefix):].strip() # Remove prefix and whitespaces
    return None

def read_metrics_once(ser, sim_mode=False):
    """
    Query Arduino for a set of built-in readings:
      '1' -> UPTIME_MS:<int>
      '2' -> VCC_mV:<int>
      '3' -> RAND:<int>
      '4' -> LED:<0|1>
    Returns a string to be written as a CSV row.
    """
    ts = datetime.now().isoformat(timespec="seconds")
    row = {"timestamp": ts, "uptime_ms": "", "vcc_mv": "", "led": ""}

    if sim_mode:
        # Simulated data
        uptime_ms = int(time.time() * 1000) % 1000000
        row["uptime_ms"] = uptime_ms
        vcc_mv = 5000 + (uptime_ms % 1000)  # Vcc varies slightly
        row["vcc_mv"] = vcc_mv
        led = uptime_ms // 1000 % 2          # LED toggles every second
        row["led"] = led
    else:
        # Uptime
        r = send_cmd(ser, '1')  # expect UPTIME_MS:<int>
        v = parse_metric(r, "UPTIME_MS")
        row["uptime_ms"] = v if v is not None else ""

        # Vcc
        r = send_cmd(ser, '2')  # expect VCC_mV:<int>
        v = parse_metric(r, "VCC_mV")
        row["vcc_mv"] = v if v is not None else ""


        # LED state
        r = send_cmd(ser, '4')  # expect LED:<0|1>
        v = parse_metric(r, "LED")
        row["led"] = v if v is not None else ""
    return row


def log_continuous(ser, out_path="arduino_log.csv", period=1.0, sim_mode=False):
    """
    Poll metrics every 'period' seconds and append to CSV.
    Stop with Ctrl+C.
    """
    fields = ["timestamp", "uptime_ms", "vcc_mv", "led"]
    # Ensure output directory exists
    if not os.path.exists(os.path.dirname(out_path)):
        with open(out_path, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fields)
            writer.writeheader()
    print(f"📝 Logging to {out_path} every {period:.2f}s — press Ctrl+C to stop.")
    try:
        with open(out_path, "a+", encoding="utf-8") as f:
            while True:
                t0 = time.time()
                writer = csv.DictWriter(f, fieldnames=fields)
                row = read_metrics_once(ser, sim_mode=sim_mode)
                writer.writerow(row)
                f.flush()
                # Console status line
                print(row)

                # pacing delay:
                dt = time.time() - t0
                sleep_left = period - dt
                if sleep_left > 0:
                    time.sleep(sleep_left)
    except KeyboardInterrupt:
        print("\n⏹️  Logging stopped.")

def main():
    sim_mode = False  # By default, simulation mode is off
    arduino = open_serial()
    if arduino is None:
        sim_mode_on = input("Arduino not available, press Enter to exit or 's' to enable simulation mode: ").strip().lower() == 's'
        if sim_mode_on:
            sim_mode = True
            print("⚠️  Simulation mode enabled. No Arduino connected.")
        else:
            return
    else:
        print(f"✅ Connected to Arduino on {PORT} at {BAUD} baud.")

        # Optional: do a handshake
        hello = send_cmd(arduino, '0')   # expect ACK;...;UPTIME_MS=...
        print("Handshake:", hello or "(no reply)")

    print(r"""
 _      __    __
| | /| / /__ / /______  __ _  ___
| |/ |/ / -_) / __/ _ \/  ' \/ -_)
|__/|__/\__/_/\__/\___/_/_/_/\__/
Welcome to the Arduino control panel

Commands:
  0  Handshake
  1  Read uptime (ms)
  2  Read Vcc (mV)
  3  Read LED state (0/1)
  4  Toggle LED
  5  LED ON
  6  LED OFF
  c  Continuous log to CSV
  q  Quit
""")

    try:
        while True:
            cmd = input("Enter command [0-6/c/q]: ").strip().lower()

            if cmd not in ["0", "1", "2", "3", "4", "5", "6", "c", "q"]:
                print("Invalid command")
                continue

            if cmd == 'q':
                print("Bye!")
                break

            if cmd == 'c':
                # Ask filename + period (optional)
                path = input("CSV path [arduino_log.csv]: ").strip() or "arduino_log.csv"
                try:
                    period = float(input("Sampling period seconds [1.0]: ").strip() or "1.0")
                    period = max(0.1, period)  # clamp to safe lower bound
                except ValueError:
                    period = 1.0
                log_continuous(arduino, out_path=path, period=period, sim_mode=sim_mode)
                continue

            if sim_mode:
                print("→ (simulation mode: no reply)")
            else:
                reply = send_cmd(arduino, cmd)
                print("→", reply or "(no reply / timeout)")

    except KeyboardInterrupt:
        print("\nInterrupted. Bye!")
    finally:
        try:
            arduino.close()
        except Exception:
            pass

if __name__ == "__main__":
    main()

After taking a look, let us try to answer the following questions:

Questions and Code cards

Question P1

What is the purpose of the simulation mode in the Python part template? Why do you think it is useful when developing your IoT application?

Code card P2 - parse_metric

Explain in your own words how slicing is used in the function below. Why is the strip() method used at the end?

def parse_metric(line, key):
    """Return value after 'KEY:' in 'KEY:VALUE' lines; else None."""
    prefix = key + ":"
    if line.startswith(prefix):
        return line[len(prefix):].strip()
    return None

Code card P3 - flush and decode

Why is the ser.flush() method used after writing a command to the serial port in the function below? The method decode is used to convert bytes to a string, why do we use it? what do you think the parameter errors="replace" does?

def send_cmd(ser, cmd_char):
    """Send single-char command; return one decoded line ('' on timeout)."""
    ser.write(cmd_char.encode("utf-8"))
    ser.flush()
    line = ser.readline()
    return line.decode("utf-8", errors="replace").strip()

User interface design

  1. Based on the example, take a moment to rethink and decide which user commands you will add to your project. You can reason with an AI assistant to choose. When prompting your assistant, provide as much content as possible. Be critical and analyse carefully the proposed options, make sure you can implement them with the tools you have at hand.

User interface tests

  1. Now start implementing a preliminary version of your user interface, focusing first on the simulation mode. You can use an AI assistant to generate the code, but make sure you pass the script template and ask specifically to focus on the simulation mode first and to comply with the provided template. Next, run the Python part in your IDE and test it, and verify that it works.

Basic integration test

  1. Go back to your Arduino IDE and note the name of the port used to connect to the Arduino board through the USB connector (for instance, in Windows, it will be something like ‘COM05’). Update your Python script to set the variable port to the actual name of the port. Next, select one direct command to perform a basic integration tests. Choose for instance an option that provides a sensor value, as this is a very straightforward command we can use to perform this basic integration test. Implement the direct command in Arduino and test it running the Python part in your Python IDE.

Analysis questions

Take a look again at the template description in the Serial communication tutorial again before you try to answer the following analysis questions.

  1. Note that the Arduino part uses two different methods to write to serial print and println. Describe, in your own words, what would happen if you use print instead of println at the end of a response.