Digital ChallengeWorkshop · Arduino × Processing
Workbook

28 exercises.
Zero busywork.

Predict, calculate, build, code, debug and design. Every card has a hint and a full solution. Progress is saved in this browser.

Basic · 6Intermediate · 11Advanced · 6Challenge · 5≈ 6 lessons of practice
0/28 marked done
L01

Climate Station — DHT11

open lab →
L01-E1BasicPredict 5 minL01 · Climate

What does the line look like?

Arduino prints Serial.print(temperature); Serial.print(","); Serial.println(humidity); with temperature = 22.4 and humidity = 48. Write the exact characters that travel over USB, including the invisible ones.

println adds a newline (\n). There is no space after the comma.
22.40,48.00 followed by \n. (Arduino may print extra zeros — the important part is number,comma,number,newline.)
L01-E2BasicExplain 8 minL01 · Climate

Why a pull-up?

A classmate uses a bare 3-pin DHT11 with only three wires: 5V, GND, DATA. Readings fail at random. Explain, in 4–6 sentences, what a pull-up resistor does on the DATA line and why the module version often works without an extra resistor.

The DATA pin is open-drain. When nobody is driving it, it would float.
The DHT11 DATA pin is open-drain: it can pull the line LOW but cannot push it HIGH. A 10 kΩ resistor to 5 V holds the line HIGH between pulses. Without it the pin floats and the library sees garbage. A DHT11 module already has that resistor soldered on the PCB, so three wires are enough.
L01-E3IntermediateCalculate 8 minL01 · Climate

Map a bar

Processing window is 640 px wide. You draw a bar starting at x = 40, and you want 50 °C to fill 520 px. What is map(28, 0, 50, 0, 520)? How wide is the bar at 28 °C?

map(value, fromLow, fromHigh, toLow, toHigh) = toLow + (value−fromLow)/(fromHigh−fromLow) × (toHigh−toLow).
28/50 × 520 = 291.2 px. The bar is 291 px wide (Processing will draw it as a float).
L01-E4IntermediateDebug 10 minL01 · Climate

The NaN trap

This sketch sometimes prints nan,nan and Processing’s bars vanish. Find two bugs and write the corrected loop().

buggy.inoArduino · C++
void loop() {
  float h = dht.readHumidity();
  float t = dht.readTemperature();
  Serial.print(t);
  Serial.print(",");
  Serial.println(h);
  delay(200);
}
DHT11 needs ≥ 1 s between reads. Failed reads return NaN.
Bug 1: no isnan() guard. Bug 2: 200 ms is too fast. Guard and wait 2 s.
loop()Arduino · C++
void loop() {
  float h = dht.readHumidity();
  float t = dht.readTemperature();
  if (isnan(h) || isnan(t)) {
    Serial.println("Failed to read from DHT sensor!");
    delay(2000);
    return;
  }
  Serial.print(t);
  Serial.print(",");
  Serial.println(h);
  delay(2000);
}
L01-E5AdvancedCode 15 minL01 · Climate

Log to CSV

Extend the dashboard so every accepted reading is appended to climate.csv as millis,temp,hum. Create the file in setup() with a header row.

In Processing: PrintWriter log = createWriter("climate.csv"); then log.println(...) and log.flush().
Create the writer in setup(), write a header, then in serialEvent after a successful parse write millis()+","+temperature+","+humidity and flush().
logging fragmentProcessing · Java
PrintWriter log;

void setup() {
  // ... serial setup ...
  log = createWriter("climate.csv");
  log.println("millis,temp,hum");
}

void serialEvent(Serial p) {
  String data = trim(p.readStringUntil('\\n'));
  String[] v = split(data, ",");
  if (v.length != 2) return;
  temperature = float(v[0]);
  humidity = float(v[1]);
  log.println(millis() + "," + temperature + "," + humidity);
  log.flush();
}
L01-E6ChallengeDesign 20 minL01 · Climate

A chart, not a bar

Design (on paper or in code) a 2-minute scrolling chart of temperature. List: the data structure you store, how many points, how you map time to x and temperature to y, and what happens when the array is full.

A fixed-size array used as a ring buffer, or append() + subset() to drop the oldest point.
Store ~60 floats (one every 2 s for 2 min). Each frame, x = map(i, 0, n-1, 40, width-40), y = map(values[i], 10, 40, height-40, 40). When full, shift left (for i in 0..n-2: a[i]=a[i+1]) and write the new sample at the end. Draw with beginShape() / vertex() / endShape().
L02

Light Meter — LDR

open lab →
L02-E1BasicPredict 5 minL02 · Light

Cover the sensor

Wiring: LDR to 5 V, 10 kΩ to GND, tap to A0. You cover the LDR with your palm. Does analogRead go up or down? Explain in two sentences.

More resistance on the top half of the divider means less voltage at the tap.
Down. Covering the LDR raises R_LDR. A larger top resistor drops more of the 5 V, so V_A0 (and the 0–1023 reading) falls.
L02-E2IntermediateCalculate 10 minL02 · Light

Voltage at A0

R_LDR = 4 kΩ, R_fixed = 10 kΩ, V_in = 5.0 V. Compute V_A0 and the expected analogRead (round to nearest integer).

V = 5 × 10 / (4 + 10). raw = round(V / 5 × 1023).
V_A0 = 5 × 10/14 = 3.57 V. raw ≈ 3.57/5 × 1023 = 731.
L02-E3IntermediateCalculate 6 minL02 · Light

ADC step size

Arduino’s ADC is 10-bit, 0–5 V. How many millivolts is one analogRead step? If the reading jumps from 400 to 401, how much did the voltage change?

5 V / 1023 ≈ 4.89 mV per step. A jump of 1 is about 4.9 mV.
L02-E4IntermediateDebug 8 minL02 · Light

The bar never moves

Processing shows “Light intensity: 0” forever. The Serial Monitor (when Processing is closed) shows numbers. Find the bug.

stuck.pdeProcessing · Java
void draw() {
  if (myPort.available() > 0) {
    String val = myPort.readStringUntil('\\n');
    lightLevel = int(val);
  }
}
int(null) and leftover \r will fail. Also, reading in draw can catch half a line.
readStringUntil can return null. Windows often sends \r\n, so int("412\r") is 0. Trim and guard. Prefer serialEvent.
serialEventProcessing · Java
void serialEvent(Serial p) {
  String val = p.readStringUntil('\\n');
  if (val == null) return;
  val = trim(val);
  if (val.matches("\\\\d+")) lightLevel = int(val);
}
L02-E5AdvancedCode 12 minL02 · Light

Night-light firmware

Add an LED on D8 (220 Ω to GND). Turn it on when lightValue < 350 and off otherwise. Keep sending the reading to Processing.

pinMode(8, OUTPUT); in setup. digitalWrite(8, lightValue < 350 ? HIGH : LOW);
LDR_NightLight.inoArduino · C++
const int sensorPin = A0;
const int ledPin = 8;
const int DARK = 350;

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  int lightValue = analogRead(sensorPin);
  digitalWrite(ledPin, lightValue < DARK ? HIGH : LOW);
  Serial.println(lightValue);
  delay(200);
}
L02-E6ChallengeDesign 15 minL02 · Light

Calibrate a classroom scale

You cannot trust 0–1023 as “percent brightness”. Write a 6-step procedure to calibrate your kit: record dark-desk, room, and window-sill readings, then decide how Processing should map those three numbers onto a 0–100 % scale.

1. Cover the LDR, average 20 samples → dark. 2. Point at the ceiling lights → room. 3. Hold at the window → bright. 4. In Processing, pct = constrain(map(lightLevel, dark, bright, 0, 100), 0, 100). 5. Label the bar with those three marks. 6. Recalibrate if you change the 10 kΩ or the room.
L03

Distance, Servo & Radar

open lab →
L03-E1BasicCalculate 6 minL03 · Sonar

How far?

pulseIn returns 2915 µs. Using 343 m/s, how far is the object in centimetres?

d = t × 0.0343 / 2.
2915 × 0.0343 / 2 ≈ 50.0 cm.
L03-E2IntermediateCalculate 10 minL03 · Sonar

Cold classroom

The code assumes 343 m/s. The room is 10 °C (v ≈ 337.4 m/s). A wall is truly 100 cm away. What distance does the sketch print, and what is the % error?

True echo time uses 337.4 m/s; the code multiplies that time by 0.0343.
t = 2 × 1.00 / 337.4 = 5.928 ms = 5928 µs. Code: 5928 × 0.0343 / 2 ≈ 101.7 cm. Error ≈ +1.7 %.
L03-E3IntermediateDebug 10 minL03 · Sonar

STOP is late

A student used the original blocking sweep. Explain in 4 sentences why STOP waits, then write the non-blocking idea in two lines of pseudocode.

for (angle = 0; angle <= 180; angle++) { servo.write(angle); delay(10); } does not call Serial.available() for 1.8 s. The STOP line sits in the UART buffer. Pseudocode: each loop: read serial → update sweeping flag → if sweeping, angle += dir, write once, delay 12 ms.
L03-E4AdvancedCode 12 minL03 · Sonar

Parse a messy line

Arduino prints distance: 42.0 cm. Write serialEvent that extracts 42.0. Do not crash on distance: 0.0 cm or a timeout line.

splitTokens(data, " :cm") or find the first number with a small scan.
serialEventProcessing · Java
void serialEvent(Serial p) {
  String data = trim(p.readStringUntil('\\n'));
  String[] bits = splitTokens(data, " :cm");
  for (int i = 0; i < bits.length; i++) {
    if (bits[i].length() > 0 && Character.isDigit(bits[i].charAt(0))) {
      distance = float(bits[i]);
      break;
    }
  }
}
L03-E5AdvancedCode 12 minL03 · Sonar

Polar plot one point

Window 600×600, origin at (300, 580). Angle 45°, distance 50 cm mapped onto r = 0…250 for 0…100 cm. Compute x and y of the blip.

r = 50/100 × 250 = 125. x = 300 + 125·cos(45°) ≈ 388. y = 580 − 125·sin(45°) ≈ 492. (y is flipped because screens grow downward; we subtract so 90° goes up.)
L03-E6ChallengeDesign 15 minL03 · Sonar

Parking sensor UX

Design a Processing UI for a reversing sensor: three zones (<30, 30–80, >80 cm), colour, optional beep, and a frozen “HOLD” button that stops the servo. Sketch the layout and list the serial messages in both directions.

Arduino → PC: angle,distance or just distance. PC → Arduino: START, STOP, maybe HOLD. UI: big number, coloured bar, zone chips, mute toggle. Beep interval shortens as distance falls (map 80→30 cm onto 800→80 ms). HOLD sends STOP and freezes the last bar.
L04

Ohm Meter

open lab →
L04-E1BasicCalculate 8 minL04 · Ohm

Do the algebra

R_known = 1000 Ω, V_A0 = 2.0 V, V_in = 5.0 V, unknown on the 5 V side. Find R_x.

R_x = 1000 × (5/2 − 1).
5/2 − 1 = 1.5, so R_x = 1500 Ω (1.50 kΩ).
L04-E2IntermediateCalculate 10 minL04 · Ohm

What does the ADC see?

R_x = 2.2 kΩ, R_known = 1.0 kΩ, unknown on 5 V side. Compute V_A0 and analogRead.

V = 5 × 1000/(2200+1000) = 1.563 V. raw ≈ 1.563/5 × 1023 = 320.
L04-E3IntermediateExplain 8 minL04 · Ohm

Why not measure 1 MΩ with 1 kΩ?

In 5–6 sentences, explain why a 1 kΩ reference is a bad choice for a 1 MΩ unknown. Mention V_A0 and one ADC step (≈ 4.9 mV).

V_A0 = 5 × 1k / (1M + 1k) ≈ 5 mV — about one ADC step. A jump of 1 count changes the computed R_x by tens of percent. The meter is “legal” but useless. Match the decades: use 100 kΩ or 1 MΩ as the known resistor.
L04-E4AdvancedDebug 8 minL04 · Ohm

Infinity on the screen

Someone unplugged the unknown resistor. Processing prints Infinity Ω. Point to the line that blew up and write the guard.

unguarded.inoArduino · C++
float Vout = (raw * 5.0) / 1023.0;
float Rx = 1000.0 * (5.0 / Vout - 1.0);
Serial.println(Rx);
If raw ≈ 0 then Vout ≈ 0 and 5.0 / Vout is infinity. Send a sentinel (−1) and let Processing print OPEN.
guardArduino · C++
if (raw < 2) { Serial.println(-1); return; }
float Vout = (raw * 5.0) / 1023.0;
Serial.println(1000.0 * (5.0 / Vout - 1.0));
L04-E5ChallengeCode 18 minL04 · Ohm

Range picker

Processing keys 1, 2, 3 send "K,1000\n", "K,10000\n", "K,100000\n". Arduino updates Rknown when a line starts with K. Keep printing R_x as a number.

On Arduino use Serial.readStringUntil and startsWith("K,"). On Processing keyPressed().
Arduino holds float Rknown = 1000. Each loop, if serial is available, read a line; if it starts with K,, parse the rest as the new known value. Processing’s keyPressed writes the matching command.
L05

Soil Moisture

open lab →
L05-E1BasicPredict 5 minL05 · Soil

Which way does the number go?

You dip a typical classroom probe from dry potting mix into a wet cup. Does analogRead rise or fall? Why did the Processing bar still get longer in the starter sketch?

The raw reading falls. map(moisture, DRY, WET, 0, 100) has DRY > WET, so a smaller input maps to a larger output. The bar follows wetness, not voltage.
L05-E2IntermediateCalculate 8 minL05 · Soil

Percent wet

Your calibration: DRY = 880, WET = 340. A reading is 610. What % wet is constrain(map(610, 880, 340, 0, 100), 0, 100)?

(880−610) / (880−340) × 100.
270 / 540 × 100 = 50 %.
L05-E3IntermediateBuild 15 minL05 · Soil

Three-cup lab sheet

Fill in a table: medium (dry soil, damp soil, wet soil, cup of water, open air) × 10 readings × average. Then choose DRY and WET for your Processing sketch. Photograph the table or type it into the handbook.

  1. 01Wipe the probe between cups.
  2. 02Wait 8 seconds after dipping before you record.
  3. 03Average 10 samples — do not pick the prettiest one.
  4. 04Open air is not the same as dry soil. Use dry soil as DRY if the plant will live in soil.
There is no universal answer. A good sheet shows a clear gap between the three soil averages and a note of which two numbers you put in the sketch.
L05-E4AdvancedCode 10 minL05 · Soil

Smooth the jitter

Average the last 8 analog readings on Arduino before printing. Keep the serial contract as one integer per line.

A small array + an index, or a running sum.
average 8Arduino · C++
const int N = 8;
int buf[N];
int i = 0;

void loop() {
  buf[i] = analogRead(A0);
  i = (i + 1) % N;
  long sum = 0;
  for (int k = 0; k < N; k++) sum += buf[k];
  Serial.println(sum / N);
  delay(50);
}
L05-E5ChallengeDesign 15 minL05 · Soil

Don’t chatter the pump

A single threshold if (reading > 700) pumpOn(); else pumpOff(); will flip the relay every few hundred milliseconds as the reading hovers. Design hysteresis: pick two numbers and write the if logic. Explain why plants (and relays) prefer this.

Example: turn on only if reading > 750 (drier than dry), turn off only if reading < 550 (wet enough). In between, keep the last state. Relays live longer; roots are not pulsed with 5 Hz floods. This is the same idea as a thermostat.