A portable handheld solid-state anemometer built with a M5Stack DIAL, a WindQX solid-state anemometer sensor, a 3D-printed case, and a rechargeable battery.
- Overview
- Bill of Materials
- 3D-Printed Case
- Hardware Assembly
- Programming with UIFlow 2.0
- Usage
- Calibration
- Troubleshooting
- License
This project turns an M5Stack DIAL and a WindQX SA.01P solid-state anemometer into a compact, portable handheld wind-speed meter. Unlike traditional cup-anemometers, the WindQX sensor has no moving parts it measures wind speed and temperature using thermal technology, making it robust and maintenance-free.
The M5Stack DIAL provides:
- A round 1.28″ TFT display (240 × 240 px) for an analogue-gauge-style readout.
- A rotary encoder (EC11) to switch between display units and modes.
- An ESP32-S3 processor with built-in Wi-Fi and Bluetooth.
- A USB-C port and built-in battery (or external battery pack via Grove).
The program is written entirely with visual blocks in the UIFlow 2.0 IDE, making it accessible without prior text-based coding experience.
| # | Component | Qty | Notes | Picture |
|---|---|---|---|---|
| 1 | M5Stack DIAL | 1 | ESP32-S3, 1.28″ round display | ![]() |
| 2 | WindQX SA.01P Solid-State Anemometer | 1 | 5 V, I2C output, UART optional | ![]() |
| 3 | 3D-printed case | 1 | Files in 3d/ |
![]() |
| 4 | Li-Po rechargeable battery | 1 | 3.7v 3400mAh 18650 Li-ion with PH1.25mm 2P | ![]() |
| 5 | Grove cable 4-pin, 10 cm | 1 | HY2.0-4Pin for M5Stack Development Board | ![]() |
| 6 | USB-C cable | 1 | Programming and charging | ![]() |
Tools required: soldering iron, resing 3D printer (or printing service).
The case is designed to hold the M5Stack DIAL in one hand with the WindQX sensor pointing forward. All design files are in the 3d/ directory.
Print settings: recommended 0.2 mm layer height, 20 % infill, PLA or PETG.
- Charge the Li-Po rechargeable battery before starting. You can use the M5Stack DIAL via USB-C as a charger.
- Confirm the firmware version is UIFlow 2.x (hold the encoder button while powering on; the screen shows the current mode and firmware).
- If needed, flash UIFlow 2.x firmware using the M5Burner tool.
| Download the UIFlow firmware | Setup the firmware | Flash the firmaware |
|---|---|---|
![]() |
![]() |
![]() |
The WindQX sensor communicates over I2C at address id 54. Using the i2c0 login interface at 100 KHz. Connect it to the M5Stack DIAL Grove Port A (bottom right connector) using a standard Grove cable:

| Grove Pin | DIAL Pin | WindQX Wire |
|---|---|---|
| 1 — White | SCL (15) | SCL |
| 2 — Yellow | SDA (13) | SDA |
| 3 — Red | 5 V | VCC + |
| 4 — Black | GND | GND - |
⚠️ The WindQX sensor requires 5 V. The M5Stack DIAL Grove port provides 5 V on pin 3. Do not exceed this voltage.
The WindQX sensors come with a connector that is not a Grove connector, so it will need to be replaced. The simplest way is to cut it off and solder the wires so that it looks like this:

| 1 | 2 | 3 |
|---|---|---|
![]() |
![]() |
![]() |
The M5Stack DIAL has an internal JST PH 1.25mm connector for a Li-Po battery and includes a charge regulator. Insert the 3.7 V 500 mAh battery and secure it with the provided adhesive pad if your enclosure requires it.
| 1 | 2 | 3 |
|---|---|---|
![]() |
![]() |
![]() |
The firmware is a UIFlow 2.0 block program located in src/anemometer_dial.m5f2.
- Open UIFlow 2.0 in your browser (or the desktop app).
- Connect the M5Stack DIAL via USB-C or Wi-Fi.
| 3. Create a new project | 4. Click Import project from local file and select src/WindQX_Anemometer.m5f2 |
|---|---|
![]() |
![]() |
| 5. Select run or download | 6. Setup the connection to burn the program |
|---|---|
![]() |
![]() |
7. Click Project files (+) and select src/iconAlarm.png |
Icon |
|---|---|
![]() |
The program is structured in three parts:
[On Start]
├─ Init built-in hardware
├─ Init rotary encoder → built-in
├─ Set alarmHighSpeed → 20
├─ Set SA01_ADDR → 54
├─ Load page0
├─ Configure History chart
│ ├─ Series name → speed
│ ├─ Update mode → shift
│ ├─ Point count → 10
│ ├─ Y-axis min/max → 0 / 60
│ └─ Point size → width 10, height 10
├─ Set last_min_ms → current ticks in milliseconds
├─ Set title text → "WindQX"
├─ Set dialWind current value → 20
├─ Set lblWind text → "20"
├─ Set kmh text → "km/h"
├─ Configure alarm selector
│ ├─ Min → 0
│ ├─ Max → 100
│ └─ Value → alarmHighSpeed
├─ Set AlarmNumber text → alarmHighSpeed
├─ Turn AlarmLED off
├─ Set AlarmLED size → 0 x 0
├─ Set dialTemp current value → 0
├─ Set lblTemp text → "0"
├─ Set Celsius text → "C"
└─ Init I2C bus → SCL 15, SDA 13, 100 kHz
[Loop Forever]
├─ If BtnA is holding
│ └─ Turn off the device
├─ Read 4 bytes by I2C from SA01_ADDR → data
├─ Extract wind bytes
│ ├─ rawWind1 ← data[1]
│ └─ rawWind2 ← data[2]
├─ Calculate wind_kmh
│ └─ wind_kmh = ((rawWind1 × 256) + rawWind2) ÷ 10
├─ Update wind display
│ ├─ Set lblWind text → wind_kmh
│ └─ Set dialWind value → rounded wind_kmh
├─ Extract temperature bytes
│ ├─ rawTemp3 ← data[3]
│ └─ rawTemp4 ← data[4]
├─ Calculate temp_c
│ └─ temp_c = (((rawTemp3 × 256) + rawTemp4) ÷ 100) - 40
├─ Round temp_c to integer
├─ Update temperature display
│ ├─ Set lblTemp text → temp_c
│ └─ Set dialTemp value → temp_c
├─ If rotary encoder has rotated
│ ├─ Change alarmHighSpeed by encoder increment
│ ├─ If alarmHighSpeed > 100
│ │ └─ Keep value unchanged
│ ├─ Set AlarmSelector value → alarmHighSpeed
│ └─ Set AlarmNumber text → alarmHighSpeed
├─ If wind_kmh > alarmHighSpeed
│ ├─ Set AlarmLED size → 10 x 10
│ ├─ Turn AlarmLED on
│ └─ Repeat 3 times
│ ├─ Speaker tone 1000 for 500 ms
│ └─ Speaker tone 15000 for 500 ms
├─ Else
│ ├─ Turn AlarmLED off
│ └─ Set AlarmLED size → 0 x 0
├─ Set now_ms → current ticks in milliseconds
├─ If (now_ms - last_min_ms) > 10000
│ ├─ Set last_min_ms → current ticks in milliseconds
│ └─ Add rounded wind_kmh to History chart
└─ Wait 250 ms
[When AlarmSelector value changed]
├─ Set AlarmNumber text → AlarmSelector value
└─ Set alarmHighSpeed → AlarmSelector value
- Power on — Short-press the encoder button. The DIAL logo appears and the device boots (~3 s).
- Read wind speed — The gauge needle and numeric value update every 250 ms. If the wind speed > 0, the LED color of sensor turns into red.
- Set alarm threshold a. Rotating the encoder clockwise to increase the value or anti-clockwise to reduce the value. b. By dragging the dot on the slider with your finger to the desired alarm value.
- Alarm — If the current wind speed is greater than the selected alert threshold, the device will emit beeps and the speed icon will display a red dot.
- Power off — Long-press the encoder button.
The WindQX sensor is factory-calibrated. For best results:
- Point the sensor directly into the wind (0° incidence).
- Hold the device at arm's length, away from body turbulence.
- For field calibration against a reference anemometer, use the
CALIBRATION_OFFSETconstant at the top of the program (block: Set CALIBRATION_OFFSET to0.0). A positive value adds to every reading; a negative value subtracts.
| Symptom | Possible cause | Fix |
|---|---|---|
Display shows --- |
No I2C data from sensor | Check wiring; confirm sensor is powered (5 V LED) |
| Wind speed always 0.0 | Low battery power | |
| Device won't power on | Battery depleted | Charge via USB-C for 1 hour before retrying |
| UIFlow can't find device | Wi-Fi not connected | Use USB-C connection or check Wi-Fi credentials in M5Burner |
This project is released under the MIT License.
© McOrts — contributions welcome via Pull Request.























