Firmware + browser console for a WIKA F2802 force transducer read through a WIKA B1940 current-loop amplifier, an ADS1115 ADC, and an ESP32, with SD-card backup logging and a Web Serial HTML frontend.
F2802 load cell ── B1940 amp ── 150 Ω shunt ── ADS1115 ── ESP32 ──USB serial── browser UI
2 kN, 2.0015mV/V 4–20 mA I→V drop 16-bit │
exc 6–10 VDC sup 18–30V └── SD card (FAT32 backup)
The B1940 outputs 4–20 mA proportional to force (live zero: 4 mA = 0 N, 20 mA = full scale = 2 kN). The ADS1115 measures voltage, so the loop current is dropped across a precision shunt and the ADS reads the shunt voltage.
| Shunt | 4 mA | 20 mA | Notes |
|---|---|---|---|
| 150 Ω, 0.1 %, ≤25 ppm/°C | 0.60 V | 3.00 V | under 3.3 V, well under the amp's 400 Ω max burden |
ADS1115 gain ±4.096 V (GAIN_ONE) → 125 µV/bit → ≈0.83 µA/bit →
~19 000 counts across the 16 mA span.
- B1940 supply: 18–30 VDC (use 24 V). Its supply ground must be common with the ESP32/ADS1115 ground.
- B1940 4–20 mA output → shunt → common GND; ADS1115 A0 taps across the shunt.
- F2802 → B1940 (factory-matched DMS bridge): Input
Red(+) / Black(−), OutputGreen(+) / White(−). - ADS1115: I²C
SDA=21,SCL=22,VDD=3.3 V, addr0x48(ADDR→GND). - SD card (SPI):
CS=5,SCK=18,MISO=19,MOSI=23,VCC=3.3 V.
Reading below ~3.5 mA ⇒ broken loop wire or unpowered amplifier — flagged as
BROKENand surfaced in POST and telemetry.
Arduino sketch in firmware/loadcell/, split into tabs (no RTOS, no flash use):
| Tab | Responsibility |
|---|---|
loadcell.ino |
globals, setup() (POST → sampler → calib), cooperative loop() |
post.ino |
Power-On Self Test (ADS + loop current + SD write/read/verify) → run mode |
sampler.ino |
ADS1115 continuous read, EMA filter, calibration math, tare/span |
logger.ino |
SD series folders, CSV/meta, wear-aware flush, calibration file, list/get |
comms.ino |
NDJSON command parser + telemetry/status/ack emitters |
config.h |
all tunables — pins, shunt Ω, FNOM_N, rates, paths |
- Adafruit ADS1X15 ≥ 2.4
- ArduinoJson ≥ 7
- ESP32 Arduino core (bundled
SD,Wire,SPI)
loop() is non-blocking: the ADS runs in continuous mode so
getLastConversionResults() returns instantly. Each iteration samples at
SAMPLE_HZ (timestamped with real millis()), services serial, flushes the SD
buffer at most every FLUSH_MS, and emits decimated telemetry at TELEM_HZ.
- NORMAL — SD mounts and passes write/read/verify → full logging to card.
- DEGRADED — SD missing/unreadable, or pulled mid-session → no card writes,
live streaming continues, every frame carries
"sd":false, and the browser records as a fallback while showing a large warning banner. Calibration in this mode comes from the browser (held in RAM only).
FNOM_Nis preset to 2000 N to match the labelledF2802-2KN. It only affects the uncalibrated fallback mapping; a 2-point calibration overrides it.
No ESP32 flash is used. The SD card is the single source of truth; swapping the card cleanly restarts numbering from that card's contents.
/SYS/CALIB.CSV tare_mA,scale_N_per_mA,span_calibrated
/DATA/000123_pulltest_A/
META.JSON id, label, fw, sample_hz, shunt, start_ms, host_epoch?, calib{}
DATA.CSV seq,t_ms,raw,mA,force_N,flags
- Series id = highest existing
/DATA/NNNNNN_*folder + 1 (the filesystem is the counter — no flash, host-independent). - In-series time =
t_mssince series start (millis()), always present. - Wall clock = optional: the UI sends
host_epoch(Unix seconds) at start, stored once inMETA.JSON. Reconstruct offline ashost_epoch + (t_ms − 0)/1000. Logging never depends on it, so a missing/odd host clock can't break a recording.
DATA.CSV example
seq,t_ms,raw,mA,force_N,flags
0,0,17234,12.0430,250.5,0
1,20,17240,12.0470,250.7,0
2,40,17198,12.0200,249.4,4flags is a bitfield: 1=broken wire, 2=saturated, 4=uncalibrated.
- Writes happen only during an active series.
- The log file stays open for the whole series; rows accumulate in the SD
sector cache and are
flush()ed at most once perFLUSH_MS(≈1 s) → large, sequential writes and few FAT/directory updates. - A flush bounds worst-case power-loss to ~1 s; the append-only CSV loses at most a partial final line, which parsers tolerate.
One JSON object per line, both directions, at 115200 baud.
| Command | Effect |
|---|---|
{"cmd":"status"} |
request a status frame |
{"cmd":"tare"} |
set current reading as zero |
{"cmd":"calibrate","known_n":1000} |
set span from a known applied load |
{"cmd":"start","label":"pulltest_A","host_epoch":1750000000} |
begin a series |
{"cmd":"stop"} |
end the series, flush + close file |
{"cmd":"stream","on":false} |
pause/resume telemetry |
{"cmd":"list"} |
list series on the card |
{"cmd":"get","path":"/DATA/000123_x/DATA.CSV"} |
stream a file back |
| Frame | When |
|---|---|
{"post":{ads,loop_mA,loop_ok,sd_mount,sd_write,sd_read,sd_free_mb,mode,fw}} |
once at boot |
{"status":{mode,sd,ads,recording,series,sample_hz,calib{...}}} |
on request / boot |
{"telem":{t,mA,N,raw,series,rec,sd,flags}} |
every 1000/TELEM_HZ ms |
{"ack":"start",series,path,sd} / {"ack":"stop"} / {"ack":"tare|calibrate",...} |
command acks |
{"event":"sd_lost"} |
card pulled mid-session |
{"err":"..."} |
bad json / unknown cmd / need_known_n / span_too_small |
{"list_begin"} · {"series":{id,name,path,bytes}} · {"list_end"} |
list response |
{"file_begin":{path,size}} · raw CSV bytes · {"file_end":{path}} |
get response |
File download framing: after file_begin, the UI captures raw bytes until a
line beginning {"file_end" arrives, then saves the buffer as a .csv.
Single self-contained file using the Web Serial API (Chrome/Edge over USB — no WiFi, no server, no build step). Just open it and click Connect.
Features: live force/mA/raw readout + rolling chart, fault pills (broken wire / saturated / uncalibrated), Tare, Calibrate span, labelled Start/Stop, SD series browser with per-series Download, the big NO-SD warning with a browser-side fallback recorder (+ "Download browser recording"), and a raw log.
- Set
FNOM_Ninconfig.hif your cell isn't 2 kN (this one is). - Install the two libraries (Library Manager): Adafruit ADS1X15, ArduinoJson.
- Open
firmware/loadcell/loadcell.ino, select your ESP32 board, Upload. - Open
frontend/index.htmlin Chrome/Edge → Connect → pick the ESP32 port. (The Arduino Serial Monitor must be closed — only one app can hold the port.)
- POST runs at boot; check
loop_okandmodein the log. - With no load applied, Tare.
- Apply a known reference load, type its value (N), Calibrate span.
- Enter a label, Start → record → Stop.
- Refresh the series list and Download the CSV, or pull it later off the card.