IoT & Robotics crash course

Laboratory track

Fourteen labs, one for each line of the syllabus. Every lab runs in simulation: in Python on the code you download with the unit, or in the Wokwi simulator for the two firmware labs. Each lab ends with an optional box for a real ESP32 kit.

LAB 1

Python and a sense-think-act simulation

Goal. Simulate a thermostat as a loop of sensing, deciding and acting, and see what delay and noise do.

Needs. Unit 1. Files. Unit 1 code zip (sense_think_act)

  1. python codes/foundations/sense_think_act/example_basic.py; then read sense_think_act.simulate.
  2. Run simulate(BangBang(21, 0.5), delay=d) for d = 0, 2, 5 minutes and compare metrics(...)['max_abs_err'].
  3. Add noise (sigma=0.3) and count the heater switches per hour.
Check yourself
Why does the room swing wider than the hysteresis band when the reading is late?

On a real ESP32 kit (optional). Optional kit: read a real thermistor on an ESP32 ADC pin and print it once a second.

LAB 2

Circuits and power budget

Goal. Size a series resistor and a divider, quantise a signal with an ADC, and compute a node's battery life.

Needs. Unit 2. Files. Unit 2 code zip (circuits_np)

  1. circuits_np.led_resistor, divider (with and without r_load), lsb, adc_rms_error.
  2. power_budget(states) for the wake, connect, publish, shutdown and sleep states; then battery_life_days.
  3. Find the awake time that reaches 30 days with max_awake_seconds.
Check yourself
Which state uses most of the charge, the long sleep or the short awake time?

On a real ESP32 kit (optional). Optional kit: measure the real current of a board in deep sleep with a multimeter in series.

LAB 3

Sensor calibration and filtering

Goal. Calibrate a noisy soil sensor by least squares and choose a filter.

Needs. Unit 3. Files. Unit 3 code zip (sensors_np)

  1. sensors_np.fit_line on calibration points; compare with two_point.
  2. moving_average and ema: match their smoothing with alpha_matching_ma and compare noise_gain_* and mean_delay_*.
  3. alias_frequency(70, 100), then sample a 70 Hz sine at 100 Hz and look at the result.
Check yourself
Why does a filter that removes more noise also react later?

On a real ESP32 kit (optional). Optional kit: log a capacitive soil sensor in dry and wet soil and fit your own line.

LAB 4

ESP32 GPIO and finite-state machine (Wokwi)

Goal. Debounce a button and run a three-mode LED state machine; the logic is tested on the host.

Needs. Unit 4. Files. Unit 4 code zip (fsm_sim, firmware/)

  1. python -m pytest codes/embedded/fsm_sim/tests -q (the debouncer and the node state machine, with a fake hardware object).
  2. Open firmware/wokwi/diagram.json in the Wokwi simulator, paste firmware/arduino/button_fsm.ino or the MicroPython files, and press the button.
  3. fsm_sim.bouncy_press and count_events: how many events do you count with and without a 20 ms stable time?
Check yourself
What goes wrong with a mechanical button if you count every edge?

On a real ESP32 kit (optional). Optional kit: wire a real button and LED to an ESP32 (pins in the firmware README); the Wokwi diagram was not opened in Wokwi by the author.

LAB 5

I2C sensor read

Goal. Read a register over I2C, decode it, and compute how long the transfer takes.

Needs. Unit 4. Files. Unit 4 code zip (fsm_sim: i2c_sensor.py, firmware/micropython/i2c_read.py)

  1. i2c_sensor.decode_temp(bytes([0x19, 0x00])) and encode_temp; read_temp works with any object that has readfrom_mem.
  2. fsm_sim.i2c_read_clocks(2) and i2c_transfer_time_us(2, 100e3); repeat at 400 kHz.
  3. i2c_rise_time_ns and i2c_max_pullup: pick a pull-up resistor for a 100 pF bus.
Check yourself
Why must the pull-up resistor be small enough for a fast bus but large enough not to waste current?

On a real ESP32 kit (optional). Optional kit: connect a TMP102-style sensor to GPIO 21 and 22 and run `i2c_read.py`. The register format is modelled from memory and the file was not run on a board or in Wokwi.

LAB 6

MQTT publish/subscribe

Goal. Run a small broker and client and compare them with the real paho-mqtt client.

Needs. Unit 5. Files. Unit 5 code zip (mqtt_mini)

  1. python codes/net/mqtt_mini/example_basic.py (broker, publisher and subscriber in one process).
  2. Publish at QoS 0 and 1 over a lossy link; count packets with netcalc.qos_simulate(0.1, 1).
  3. Optional, in a venv with paho-mqtt: python codes/net/mqtt_mini/conformance.py (the real client against our broker).
Check yourself
With 10% loss, what fraction of QoS 0 messages arrive, and what does QoS 1 cost in packets?

On a real ESP32 kit (optional). Optional kit: point a real ESP32 or phone MQTT app at the broker and watch the retained messages.

LAB 7

Dashboard and alerts

Goal. Ingest readings into a store, raise alerts and serve a dashboard with Flask.

Needs. Unit 6. Files. Unit 6 code zip (telemetry_np, dashboard.py)

  1. telemetry_np.TimeSeries, ThresholdDetector, ZScoreDetector, EwmaDetector, CusumDetector: feed each a series with a step.
  2. python codes/cloud/telemetry_np/dashboard.py then open the local page; POST a reading to /ingest.
  3. Compare payload sizes with encode_json, cbor_encode, pack_reading.
Check yourself
Which detector would you choose for a slow heater fault, and why?

On a real ESP32 kit (optional). Optional kit: send real readings from the ESP32 node of Lab 6 to the dashboard.

LAB 8

HMAC-secured commands

Goal. Forge a command, then block the forgery with an HMAC envelope and a counter.

Needs. Unit 7. Files. Unit 7 code zip (iot_sec)

  1. iot_sec.sign_command(key, counter, b'water 0,2') then verify_command.
  2. Replay the same envelope and flip one tag byte: read the reason returned.
  3. forgery_probability(8) against forgery_probability(1); try 5000 random guesses against a 1-byte tag.
Check yourself
Why must the counter be part of the authenticated bytes?

On a real ESP32 kit (optional). Optional kit: run the verifier in MicroPython (`hmac` availability differs between ports; check before relying on it).

LAB 9

Arm kinematics

Goal. Move a 2-link arm to targets with forward and inverse kinematics and see singularities.

Needs. Unit 8. Files. Unit 8 code zip (kinematics_np)

  1. kinematics_np.fk_2link, ik_2link (both elbows), jacobian_2link, reachable_2link.
  2. Check fk_2link(ik_2link(x, y, ...)) == (x, y) over a grid of targets.
  3. Approach the workspace boundary and watch joint_rates grow.
Check yourself
Why do the joint rates blow up near full extension?

On a real ESP32 kit (optional). Optional kit: a two-servo pen plotter (Unit 3 servo pulse widths).

LAB 10

Differential-drive odometry

Goal. Integrate wheel counts into a pose and measure the drift caused by a wheel-radius error and by slip.

Needs. Unit 8. Files. Unit 8 code zip (kinematics_np)

  1. kinematics_np.Rover(eps_r=0.005); drive(0.5, 0.0, 40) and read position_error().
  2. Compare with radius_mismatch_arc and random_slip_lateral_std.
  3. Calibrate: run 5 m, measure with a tape, correct the radius, rerun.
Check yourself
Why does a systematic radius error grow as distance squared but random slip only as distance to the 3/2?

On a real ESP32 kit (optional). Optional kit: two wheel encoders and a tape measure on a real chassis.

LAB 11

PID motor control

Goal. Tune a wheel-speed loop and see windup, delay and saturation.

Needs. Unit 9. Files. Unit 9 code zip (control_np)

  1. control_np.lambda_pi(K, tau, 0.03); simulate_speed_loop(PID(...), WheelMotor(), 3.0); step_metrics.
  2. ultimate_gain(motor, 0.01, delay_samples=1) then zn_pid: compare with your lambda design.
  3. Block the wheel and release it with windup='none', 'clamp' and 'backcalc'.
Check yourself
Why is a derivative term a problem with a quantised speed measurement?

On a real ESP32 kit (optional). Optional kit: a geared motor with an encoder on an H-bridge; step the setpoint and log the speed.

LAB 12

Kalman localisation

Goal. Fuse encoders, a gyroscope and wall ranges and see what the filter can and cannot correct.

Needs. Unit 10. Files. Unit 10 code zip (localize_np)

  1. localize_np.patrol('odom'), 'gyro', 'ekf': compare err and crashed.
  2. Kalman1D on a drifting level against ema; steady_state_gain(q, r) against the iterated gain.
  3. Check the filter's own uncertainty against the actual error (consistency): where is it overconfident?
Check yourself
Why can wall ranges fix the sideways error but not the along-the-wall error?

On a real ESP32 kit (optional). Optional kit: an IMU (gyro) and an ultrasonic sensor on a chassis driven along a wall.

LAB 13

A* navigation

Goal. Plan on an inflated occupancy grid and follow the path with pure pursuit.

Needs. Unit 11. Files. Unit 11 code zip (planning_np)

  1. planning_np.inflate, astar, dijkstra, bfs: compare expanded on the same grid.
  2. shortcut then follow_path(xy, lookahead=0.2): read max_xte; repeat with 0.1 and 0.8.
  3. max_ping_period(0.5, 0.05, 0.015) and watchdog_stop_distance(0.5, 0.2).
Check yourself
Why inflate by more than the robot's radius on a grid of square cells?

On a real ESP32 kit (optional). Optional: a ROS 2 install is NOT needed for this lab; the miniature `Bus` and `TFTree` imitate the concepts.

LAB 14

Capstone: the Smart Greenhouse Rover

Goal. Run the whole system for a day, compare it with a timer baseline, and write the report.

Needs. Unit 12. Files. Unit 12 code zip (greenhouse_rover and its dependencies)

  1. greenhouse_rover.simulate_day('event', 3, plan_legs(), detect_lo=0.33) and the same with 'timer'.
  2. Change one thing (sampling period, packet loss, detector threshold, pose source) and report the effect with its interval.
  3. Write the report with the skeleton of Unit 12; pick one of the ten alternative briefs if you prefer.
Check yourself
Which assumption of the simulation would you test first on real hardware?

On a real ESP32 kit (optional). Optional kit: any subset of the earlier kit boxes, wired into one node and one vehicle.

What the labs do and don’t show