E-Paper meeting room dashboard firmware for the LaskaKit ESPink board with a Good Display GDEY075T7 7.5" 800×480 BW display.
Shows the room's daily schedule (fetched directly from Microsoft 365 Graph API), current time indicator, temperature/humidity (SHT40 sensor), and battery level. Refreshes every 5 minutes using deep sleep.
| Component | Details |
|---|---|
| Board | LaskaKit ESPink v2.7 (ESP32) or v3.5 (ESP32-S3) |
| Display | Good Display GDEY075T7 – 7.5" 800×480, BW, connected via 24-pin FPC |
| Sensor | SHT40 temperature & humidity, connected via μŠup I2C |
| Battery | LiPo single-cell (3.7 V nominal), connected to JST-PH connector on ESPink |
| Function | ESPink v2.7 (ESP32) | ESPink v3.5 (ESP32-S3) |
|---|---|---|
| EPD MOSI | 23 | 11 |
| EPD SCK | 18 | 12 |
| EPD CS | 5 | 10 |
| EPD DC | 17 | 48 |
| EPD RST | 16 | 45 |
| EPD BUSY | 4 | 38 |
| I2C SDA | 21 | 42 |
| I2C SCL | 22 | 2 |
| Power Gate | 2 | 47 |
| VBAT ADC | 34 | 9 |
src/
├── config.h – All configurable parameters (WiFi, Azure, pins, etc.)
├── main.cpp – Setup-only loop: power on → read sensors → fetch calendar → draw → deep sleep
├── ms_graph.h/cpp – OAuth2 client-credentials flow + MS Graph calendarView API
├── display_ui.h/cpp – Full-screen e-Paper rendering (portrait 480×800 layout)
- Power on peripherals (GPIO drives MOSFET gate for display + I2C bus)
- Read battery voltage via ADC + voltage divider
- Read SHT40 temperature & humidity over I2C
- Connect to WiFi
- Sync time via NTP (
pool.ntp.org) - Acquire OAuth2 access token from Azure Entra ID
- Fetch today's calendar events from MS Graph (
/users/{room}/calendarView) - Disconnect WiFi
- Render dashboard on e-Paper display
- Enter deep sleep for 5 minutes
# Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install Python 3 and PlatformIO CLI
brew install python3
pip3 install platformio
# Install USB-to-serial driver (CH340) if not auto-detected
# Most modern macOS versions include the driver. If not:
brew install --cask wch-ch34x-usb-serial-driver- Python 3 — Download and install from python.org. Check "Add Python to PATH" during install.
- PlatformIO CLI:
pip install platformio
- USB driver — Install the CH340 driver (ESPink uses CH340 USB-to-serial chip).
- Plug in the board and note the COM port in Device Manager (e.g.,
COM3).
sudo apt update && sudo apt install python3 python3-pip
pip3 install platformio
# Add your user to the dialout group for serial port access
sudo usermod -a -G dialout $USER
# Log out and back in for the group change to take effect
# The CH340 driver is included in the kernel. Verify with:
ls /dev/ttyUSB*pio --version
# Should print something like: PlatformIO Core, version 6.x.xFollow these steps for each new meeting room display you deploy.
If you already have an app registration, skip to the per-room steps.
- Go to Azure Portal → Entra ID → App registrations
- Click New registration
- Name:
Meeting Room Display - Supported account types: Single tenant
- Redirect URI: leave blank
- Name:
- Go to Certificates & secrets → New client secret
- Copy the Value (this is
AZURE_CLIENT_SECRET). It is only shown once!
- Copy the Value (this is
- Note down from the Overview page:
- Application (client) ID →
AZURE_CLIENT_ID - Directory (tenant) ID →
AZURE_TENANT_ID
- Application (client) ID →
Each meeting room in Microsoft 365 has a room resource mailbox. Ask your Exchange/M365 admin for the room's email address, e.g., meeting-room-lobby@yourcompany.com.
- Get the correct Service Principal Object ID
Connect-MgGraph -Scopes "Application.Read.All"
$sp = Get-MgServicePrincipal -Filter "appId eq 'AZURE_CLIENT_ID'"
$sp.Id- Connect to Exchange Online PowerShell
Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force
Connect-ExchangeOnline- Define variables
$AppId = "xxx"
$SpObjectId = "<PASTE_ENTERPRISE_APP_SERVICE_PRINCIPAL_OBJECT_ID_HERE>"
$AdminUnitId = "xxx"- Create the Exchange “pointer” to the Entra service principal
New-ServicePrincipal -AppId $AppId -ObjectId $SpObjectId -DisplayName "ADD NAME HERE"- Assign the scoped role Application Calendars.Read to the AU
New-ManagementRoleAssignment -Name "ADD NAME HERE" `
-App $SpObjectId `
-Role "Application Calendars.Read" `
-RecipientAdministrativeUnitScope $AdminUnitId- Test Scope
Test-ServicePrincipalAuthorization -Identity $SpObjectId -Resource "meeting-room-half-life@noibit.com"By default, Exchange replaces the meeting subject with the organizer's name for room mailboxes. To show actual meeting titles, an Exchange admin should run:
# Connect to Exchange Online
Connect-ExchangeOnline
# Check current settings
Get-CalendarProcessing -Identity "room-email@yourcompany.com" | FL DeleteSubject, AddOrganizerToSubject
# Fix it
Set-CalendarProcessing -Identity "room-email@yourcompany.com" -DeleteSubject $false -AddOrganizerToSubject $falseNote: The firmware handles the duplicate gracefully — if subject equals organizer name, it shows the name only once. But running the PowerShell fix above will show actual meeting titles.
Edit src/config.h:
// WiFi credentials for the room's network
#define WIFI_SSID "YourWiFiSSID"
#define WIFI_PASSWORD "YourWiFiPassword"
// Azure credentials (from step 1)
#define AZURE_TENANT_ID "your-tenant-id"
#define AZURE_CLIENT_ID "your-client-id"
#define AZURE_CLIENT_SECRET "your-client-secret"
// This room's mailbox
#define ROOM_EMAIL "room-email@yourcompany.com"
#define ROOM_DISPLAY_NAME "Room Name"Other settings you may want to adjust:
| Setting | Default | Description |
|---|---|---|
CALENDAR_START_HOUR |
7 |
First hour shown on the calendar grid |
CALENDAR_END_HOUR |
20 |
Last hour shown on the calendar grid |
SLEEP_MINUTES |
5 |
Minutes between display refreshes |
DISPLAY_ROTATION |
3 |
0=landscape, 1=portrait, 2=landscape flipped, 3=portrait flipped |
TZ_INFO |
CET-1CEST,M3.5.0,M10.5.0/3 |
POSIX timezone string (default: Europe/Prague) |
cd meeting-room-display
# For ESPink v2.7 (ESP32):
pio run -e espink-v2 -t upload
# For ESPink v3.5 (ESP32-S3):
pio run -e espink-v3 -t uploadIf auto-detection fails, specify the port explicitly:
# macOS
pio run -e espink-v2 -t upload --upload-port /dev/cu.usbserial-XXXX
# Linux
pio run -e espink-v2 -t upload --upload-port /dev/ttyUSB0
# Windows
pio run -e espink-v2 -t upload --upload-port COM3pio device monitor -e espink-v2You should see output like:
====== Meeting Room Display ======
[PWR] peripherals ON
[BAT] 4.12 V (93%)
[SHT40] 23.1 °C 45 %RH
[WiFi] connecting to YourSSID . OK IP=10.0.0.42 RSSI=-45 dBm
[NTP] syncing OK
[NTP] 2026-02-27 09:15:22 CET
[Graph] token obtained (1942 chars)
[Graph] fetched 5 calendar events
[WiFi] disconnected
[PWR] peripherals OFF
[SLEEP] deep sleep for 5 min
| Problem | Solution |
|---|---|
| Upload fails with "Wrong chip" error | You selected the wrong environment. Use -e espink-v2 for ESP32 boards, -e espink-v3 for ESP32-S3. |
| "No serial port detected" | Install the CH340 driver. On Linux, ensure your user is in the dialout group. |
| WiFi connection fails | Check SSID/password in config.h. The ESP32 only supports 2.4 GHz WiFi. |
[Graph] token request HTTP 401 |
Verify AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET. The secret may have expired. |
[Graph] token request HTTP 403 |
Admin consent for Calendars.Read has not been granted. Go to Azure Portal → App registrations → API permissions → Grant admin consent. |
| No events showing on display | Verify ROOM_EMAIL matches the room mailbox. Check that the room has events in the calendar window (CALENDAR_START_HOUR–CALENDAR_END_HOUR). |
| Display shows garbled image | Check DISPLAY_ROTATION in config.h. Try values 0–3 to match your mounting orientation. |
| Sensor shows "N/A" | Ensure the SHT40 module is connected via the μŠup I2C connector. Check that the I2C address is 0x44 (default for SHT40). |
| Battery reads 0% while on USB | Normal — the ADC reading can be inaccurate when USB power is connected. Battery percentage is accurate on battery power. |
All dependencies are managed by PlatformIO and downloaded automatically on first build.
| Library | Version | Purpose |
|---|---|---|
| GxEPD2 | ^1.6.2 | E-Paper display driver |
| Adafruit SHT4x | ^1.0.5 | Temperature/humidity sensor |
| Adafruit GFX | ^1.12.4 | Graphics primitives & fonts |
| ArduinoJson | ^7.4.2 | JSON parsing for MS Graph API responses |
The board uses deep sleep between refreshes. Approximate power draw:
| State | Current |
|---|---|
| Active (WiFi + display refresh) | ~120–180 mA for ~5s |
| Deep sleep | ~10 μA |
With a 5000 mAh battery and 5-minute refresh interval, expect several months of battery life.
Server Side Public License