summaryrefslogtreecommitdiff
path: root/api_layers/firmware/esp32_stringPot/esp32_stringPot.ino
blob: 63d434968966c4ccbc2ea5da5d63d512e8501e94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/*
  LabUI ESP32 Firmware v1.1
  ─────────────────────────────────────────────────────────────────────────
  Author: Adam Shatila
  Co-Author: Christian Kolset

  Upload this sketch to your ESP32 with a TE SP1 spring potensiometer.

  Required library: esp32 by Espressif Systems

  Calibration procedure:
    Send  calibrate  and follow the prompts (recommended — pauses the data
    stream and walks through both points). Or drive it manually:
      1. Move sensor to known position #1, send  CAL:0:<actual_position>
         Board captures its current raw reading and pairs it with that position.
      2. Move sensor to known position #2, send  CAL:1:<actual_position>
         Board recomputes calibration_factor / calibration_offset and saves
         to EEPROM.
    Send  FILT:<cutoffHz>  to change the Butterworth low-pass cutoff
    (also saved to EEPROM). Send  config  at any time to print the current
    calibration and filter configuration.

  Serial commands (newline-terminated):
    calibrate       Guided calibration wizard (recommended)
    CAL:0:<pos>     Capture current raw reading as calibration point 0
    CAL:1:<pos>     Capture current raw reading as calibration point 1
    FILT:<hz>       Set Butterworth cutoff frequency
    config          Print current calibration + filter configuration
    help            Print this command list

  Replies:
    ACK:<command>\n   Command executed
    ERR:<message>\n   Command failed / not recognized

  ─────────────────────────────────────────────────────────────────────────
 */
#include <EEPROM.h>

const int sensorPin = A0;
const unsigned long sampleInterval = 2000;
unsigned long lastSampleTime = 0;

// EEPROM-persisted configuration
#define EEPROM_SIZE 64
#define EEPROM_MAGIC 0xC1

struct StoredConfig {
  uint8_t magic;
  float calibrationPosition[2];
  float calibrationRange[2];
  float butterworth_cutoffHz;
};

StoredConfig cfg = {
  EEPROM_MAGIC,
  {0, 150},
  {260.26, 421.3},
  0.5
};

// Calibration (mirrors cfg, kept as separate globals for fast access in loop())
float calibration_factor = 1.0;
float calibration_offset = 0.0;

// 2nd-order Butterworth low-pass (biquad, direct form II transposed)
const float butterworth_sampleHz = 1000000.0 / sampleInterval;
float butter_b0, butter_b1, butter_b2, butter_a1, butter_a2;
float butter_z1 = 0.0, butter_z2 = 0.0;

String serialLine;
bool streamingEnabled = true;
bool calibPointSet[2] = {false, false};

void setup() {
  Serial.begin(115200);

  while (!Serial) {
    delay(10);
  }

  analogReadResolution(12);

  EEPROM.begin(EEPROM_SIZE);
  loadConfig();
  computeButterworthCoeffs(cfg.butterworth_cutoffHz, butterworth_sampleHz);
  computeCalibrationParameters(cfg.calibrationPosition, cfg.calibrationRange);
}

void loop() {
  unsigned long currentTime = micros();

  if (currentTime - lastSampleTime >= sampleInterval) {
    lastSampleTime = currentTime;

    float filtered_displacement = butterworthLowPass(readCalibratedPosition());
    if (streamingEnabled) {
      Serial.println(filtered_displacement, 2);
    }
  }

  readSerialCommands();
}

void readSerialCommands() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n') {
      serialLine.trim();
      if (serialLine.length() > 0) {
        handleCommand(serialLine);
      }
      serialLine = "";
    } else if (c != '\r') {
      serialLine += c;
    }
  }
}

void handleCommand(String line) {
  if (line.startsWith("CAL:0:") || line.startsWith("CAL:1:")) {
    int idx = line.charAt(4) - '0';
    float actualPos = line.substring(6).toFloat();
    float rawReading = readRawPosition();
    int otherIdx = 1 - idx;

    if (calibPointSet[otherIdx] && rawReading == cfg.calibrationRange[otherIdx]) {
      Serial.println("ERR:Calibration points cannot have equal raw reading");
      return;
    }

    cfg.calibrationPosition[idx] = actualPos;
    cfg.calibrationRange[idx] = rawReading;
    calibPointSet[idx] = true;

    if (!calibPointSet[otherIdx]) {
      Serial.println("ACK:" + line + " (waiting on CAL:" + String(otherIdx) + ")");
      return;
    }

    computeCalibrationParameters(cfg.calibrationPosition, cfg.calibrationRange);
    saveConfig();

    Serial.println("ACK:" + line);
  } else if (line.startsWith("FILT:")) {
    float hz = line.substring(5).toFloat();
    if (hz <= 0) {
      Serial.println("ERR:Invalid cutoff frequency");
      return;
    }
    cfg.butterworth_cutoffHz = hz;
    computeButterworthCoeffs(cfg.butterworth_cutoffHz, butterworth_sampleHz);
    saveConfig();

    Serial.println("ACK:" + line);
  } else if (line == "config") {
    printConfig();
  } else if (line == "help") {
    printHelp();
  } else if (line == "calibrate") {
    calibrationWizard();
  } else {
    Serial.println("ERR:Unknown command");
  }
}

void loadConfig() {
  EEPROM.get(0, cfg);
  if (cfg.magic != EEPROM_MAGIC) {
    cfg = { EEPROM_MAGIC, {0, 150}, {260.26, 421.3}, 0.5 };
    saveConfig();
  }
}

void saveConfig() {
  EEPROM.put(0, cfg);
  EEPROM.commit();
}

// Fixed sensor scaling, pre calibration_factor/offset
float readRawPosition() {
  uint32_t millivolts = analogReadMilliVolts(sensorPin);
  float voltage = millivolts / 1000.0;
  float raw_val = voltage * (635.0 / 2.863);
  return raw_val * (635.0 / 637.0);
}

float readCalibratedPosition() {
  return calibration_factor * readRawPosition() + calibration_offset;
}

void computeCalibrationParameters(float x[2], float y[2]) {
  calibration_factor = (x[1] - x[0]) / (y[1] - y[0]);
  calibration_offset = x[0] - calibration_factor * y[0];
}

void computeButterworthCoeffs(float cutoffHz, float sampleHz) {
  // 2nd-order Butterworth LPF via bilinear transform
  float omega = tan(PI * cutoffHz / sampleHz);
  float omega2 = omega * omega;
  float sqrt2 = 1.41421356f;
  float a0 = omega2 + sqrt2 * omega + 1.0f;

  butter_b0 = omega2 / a0;
  butter_b1 = 2.0f * butter_b0;
  butter_b2 = butter_b0;
  butter_a1 = 2.0f * (omega2 - 1.0f) / a0;
  butter_a2 = (omega2 - sqrt2 * omega + 1.0f) / a0;
}

float butterworthLowPass(float newSample) {
  // Direct form II transposed biquad
  float output = butter_b0 * newSample + butter_z1;
  butter_z1 = butter_b1 * newSample - butter_a1 * output + butter_z2;
  butter_z2 = butter_b2 * newSample - butter_a2 * output;
  return output;
}

void printConfig() {
  Serial.println("Calibration:");
  Serial.print("\tPosition:\t");
  Serial.print(cfg.calibrationPosition[0], 3);
  Serial.print(",\t");
  Serial.println(cfg.calibrationPosition[1], 3);
  Serial.print("\tRaw values:\t");
  Serial.print(cfg.calibrationRange[0], 3);
  Serial.print(",\t");
  Serial.println(cfg.calibrationRange[1], 3);
  Serial.print("\tFactor:\t");
  Serial.println(calibration_factor, 6);
  Serial.print("\tOffset:\t");
  Serial.println(calibration_offset, 6);
  Serial.print("Butterworth cutoff (Hz):\t");
  Serial.println(cfg.butterworth_cutoffHz, 3);
}

void printHelp() {
  Serial.println("Commands:");
  Serial.println("\tcalibrate     Guided calibration wizard (recommended)");
  Serial.println("\tCAL:0:<pos>   Capture current raw reading as calibration point 0");
  Serial.println("\tCAL:1:<pos>   Capture current raw reading as calibration point 1");
  Serial.println("\tFILT:<hz>     Set Butterworth cutoff frequency");
  Serial.println("\tconfig        Print current calibration + filter configuration");
  Serial.println("\thelp          Print this command list");
  interruptibleDelay(10000);
}

// Waits up to ms, returns early (without consuming input) if a command starts arriving.
void interruptibleDelay(unsigned long ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    if (Serial.available()) {
      return;
    }
  }
}

// Blocks until a newline-terminated line arrives; used only inside calibrationWizard(),
// where halting loop() to walk the user through steps is the point.
String blockingReadLine() {
  String buf = "";
  while (true) {
    if (Serial.available()) {
      char c = Serial.read();
      if (c == '\n') {
        buf.trim();
        return buf;
      } else if (c != '\r') {
        buf += c;
      }
    }
  }
}

void calibrationWizard() {
  bool wasStreaming = streamingEnabled;
  streamingEnabled = false;

  Serial.println("=== Calibration wizard (type 'cancel' anytime to abort) ===");

  float pos[2];
  float raw[2];

  for (int i = 0; i < 2; i++) {
    Serial.println("Move sensor to known position #" + String(i + 1) + ", enter actual position, press Enter:");
    String input = blockingReadLine();

    if (input.equalsIgnoreCase("cancel")) {
      Serial.println("Calibration cancelled. Previous calibration unchanged.");
      streamingEnabled = wasStreaming;
      return;
    }

    pos[i] = input.toFloat();
    raw[i] = readRawPosition();

    if (i == 1 && raw[1] == raw[0]) {
      Serial.println("ERR:Calibration points cannot have equal raw reading. Wizard aborted, previous calibration unchanged.");
      streamingEnabled = wasStreaming;
      return;
    }

    Serial.println("Point " + String(i) + " captured: position=" + String(pos[i], 3) + " raw=" + String(raw[i], 3));
  }

  cfg.calibrationPosition[0] = pos[0];
  cfg.calibrationPosition[1] = pos[1];
  cfg.calibrationRange[0] = raw[0];
  cfg.calibrationRange[1] = raw[1];
  calibPointSet[0] = true;
  calibPointSet[1] = true;

  computeCalibrationParameters(cfg.calibrationPosition, cfg.calibrationRange);
  saveConfig();

  Serial.println("Calibration saved.");
  printConfig();

  streamingEnabled = wasStreaming;
}