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¶
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¶
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¶
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¶
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
portto 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.
Note that the Arduino part uses two different methods to write to serial
printandprintln. Describe, in your own words, what would happen if you useprintinstead ofprintlnat the end of a response.