Note in class:
Nginx just provides a single gateway, a single contact so it reduces the risk of having so many ports like in NodeJS.
Nginx responds to only static file requests.
Nginx and Node communicate with each other via common protocol HTTP.
For testing, we're using Arduino Nano 33 IoT, GPS, and Waveshare LCD 1.69" / Adafruit LCD. Though for production,
Description
A mini device that returns images from nearest traffic camera to user's current location and allow to capture and save the image.
API
webcams.tmcnyc.org
- GET all cameras: /api/cameras
- GET the image from the camera ID: /api/cameras/[id]/image
Usage
1. Bring your device to outdoor, the GPS will only be able to work when it is outside a building or window.
2. Press power button to turn on, the device GPS will get your location by default and show you the footages of the nearest 5 traffic cameras.
3. Scroll through the cameras by rotating the rotary encoder.
4. Press the rotary encoder to refresh the camera footage.
5. Press the capture button to snap the current camera footage and save it internally.
Learn
Materials
- XIAO ESP32S3 or ESP32C6
- Adafruit GPS Breakout v3
- rotary encoder
- button
Pins
Adafruit_ST7789 TFT LCD or Waveshare
- CS: D3
- DC: D2
- RST: D1
- BL (optional): D0
Encoder / Button
- ENC_CLK: D4
- ENC_DT: D5
- ENC_SW: D6
- CAPTURE_BTN: D9
GPS
- GPS_RX_PIN: D7
- GPS_TX_PIN: D6
Battery Reading
- A_PIN: D0
A lot issues I encounter have to do with wrong wiring. Fried an Arduino because i also plugged the controller 1 row off, since ESP32C6 GND and 3.3V pin is side by side.
Code
1. WiFiNINA -> store latest connection result in lastHttpCode
2. TFT Screen (Width: 280, Height: 240) -> Adafruit ST7789
3. GPS to define USER_LAT & USER_LON
-> Use haversine formula to calculate the distance between user lat & lon to the nearest camera lat & lon:
calculate the great-circle distance (the shortest distance over the surface of a sphere) between two points given their latitude and longitud
a = math.sin(delta_phi / 2.0) ** 2 + math.cos(phi_1) * math.cos(phi_2) * math.sin(delta_lambda / 2.0) ** 2
c = 2 * Radius of earth * math.atan2(math.sqrt(a), math.sqrt(1 - a))
4. Check with hardcoded camera list at cameras.h to find nearest cameras
5. Requesting the NYCTMC HTTP API for nearest cameras:
netFile.client.print("GET " + path + " HTTP/1.1\r\n");
netFile.client.println("Host: webcams.nyctmc.org");
The complete path is:
path = webcam.tmcnyc.org/api/cameras/[cam.id]/image
Content will be stored in netFile.contentLength.
6. Processing the image
- jpegOpen: start HTTP Request
- jpegRead: reads byte and buffer the file
- jpegClose: close the connection and clear data
Flow
jpegOpen → return &netFile
↓
JPEGDEC stores it in f->fHandle
↓
jpegRead gets f
↓you cast back to NetJPEGFile*
↓
read from WiFi → write into buf
7. Rotary encoder to scroll through camera list, and press to refresh. Rotating the rotary encoder and pressing it will both redraw the image, hence refreshing the image shown on display. The fn drawImage will draw the image that is currently being the selectedNearest.
8. Capture button
Getting user’s network to connect individually to the camera
Thanks to Sky’s documentation!
Think of what happened when device lost connection or do not have connection?
Keep searching?
Note
Upload SSL certificate to Arduino and make sure Serial Monitor is closed everytime doing so.
Tools > Upload SSL Root Certificates
SSL Certificate is the public key that is used to decrypt the content from the HTTPS API
Issues
1. Storing allCameras in a JSON file cameras.h internally may cause issue as it takes a lot of storage. Thinking to just fetch it from API.
2. GPS data is not being read even after being brought outdoors.
GPS
We learnt from the example file (wired) to test the GPS is using the UART & Serial. It is a two-way data bridge:
PC <--USB (Serial, 115200)--> XIAO ESP32-C6 <--UART (Serial1, 9600)--> GPS
- Serial: Computer -> GPS (sends command to GPS)
- GPSSerial (UART): GPS -> Computer (forwards raw NMEA)
On ESP32-C6, we may need to explicit the pin definition since it is not auto-assigned like other boards.
GPSSerial.begin(9600, SERIAL_8N1, D7, D6);
1. Power button
2. Sound module & haptics for feedback
3. Battery indicator

What's inside?

Printed instruction sheet

Ideally want to make a packaging box too as if it is a real product, but for another time.
Elizabeth Kezia Widjaja © 2026 🙂