From d4034fedd606609bacd8b3229247916d2dd58a42 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Fri, 26 Dec 2025 23:30:02 +0100 Subject: [PATCH] Mid progress --- go.mod | 3 ++ main.go | 86 +++++++++++++++++++++---------------- main/CMakeLists.txt | 2 +- main/station_example_main.c | 86 +++++++++++++++++++++++-------------- plot-times.py | 67 +++++++++++++++++++++++++++++ plot.py | 57 ++++++++++++++---------- 6 files changed, 207 insertions(+), 94 deletions(-) create mode 100644 go.mod create mode 100644 plot-times.py diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b09bc9e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.ascyii.de/jonas/voltage + +go 1.25.5 diff --git a/main.go b/main.go index 4e2f998..a63ce8a 100644 --- a/main.go +++ b/main.go @@ -1,38 +1,32 @@ package main import ( + "encoding/csv" "encoding/json" - "fmt" - "io" "log" "net/http" "os" "path/filepath" + "strconv" "time" ) -type VoltageBatch struct { - Voltages []float64 `json:"voltages"` +type Summary struct { + StartMS float64 `json:"start_ms"` + EndMS float64 `json:"end_ms"` + Values []float64 `json:"values"` // [max, mean+std, mean, mean-std, min] } -var dataFile *os.File -var berlinTZ *time.Location - -func init() { - var err error - berlinTZ, err = time.LoadLocation("Europe/Berlin") - if err != nil { - log.Fatal(err) - } -} +var ( + dataFile *os.File + writer *csv.Writer +) func initDataFile() { _ = os.MkdirAll("data", 0755) - filename := filepath.Join( "data", - fmt.Sprintf("voltages_%s.csv", - time.Now().In(berlinTZ).Format("20060102_150405")), + time.Now().Format("summary_20060102_150405.csv"), ) f, err := os.OpenFile(filename, @@ -42,7 +36,19 @@ func initDataFile() { } dataFile = f - dataFile.WriteString("timestamp,voltage\n") + writer = csv.NewWriter(dataFile) + + // header for plotting + writer.Write([]string{ + "start_ms", + "end_ms", + "max", + "mean_plus_std", + "mean", + "mean_minus_std", + "min", + }) + writer.Flush() } func handler(w http.ResponseWriter, r *http.Request) { @@ -51,41 +57,45 @@ func handler(w http.ResponseWriter, r *http.Request) { return } - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "read error", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var batch VoltageBatch - if err := json.Unmarshal(body, &batch); err != nil { + var s Summary + if err := json.NewDecoder(r.Body).Decode(&s); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } - start := time.Now().In(berlinTZ) - - for i, v := range batch.Voltages { - ts := start.Add(time.Duration(i) * 33 * time.Millisecond) - dataFile.WriteString( - fmt.Sprintf("%s,%.6f\n", - ts.Format("2006-01-02 15:04:05.000"), v), - ) + if len(s.Values) != 5 { + http.Error(w, "need 5 values", http.StatusBadRequest) + return } - log.Println("Got batch!") + row := []string{ + formatFloat(s.StartMS), + formatFloat(s.EndMS), + formatFloat(s.Values[0]), + formatFloat(s.Values[1]), + formatFloat(s.Values[2]), + formatFloat(s.Values[3]), + formatFloat(s.Values[4]), + } + + writer.Write(row) + writer.Flush() + + log.Println("Summary appended") w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } +func formatFloat(v float64) string { + return strconv.FormatFloat(v, 'f', 6, 64) +} + func main() { initDataFile() defer dataFile.Close() http.HandleFunc("/data", handler) - + log.Println("Listening on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } - diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ccde765..8c5ad55 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "station_example_main.c" - PRIV_REQUIRES esp_wifi nvs_flash esp_adc esp_http_client + PRIV_REQUIRES esp_wifi nvs_flash esp_adc esp_http_client esp_timer INCLUDE_DIRS ".") diff --git a/main/station_example_main.c b/main/station_example_main.c index 17d72b3..34cc204 100755 --- a/main/station_example_main.c +++ b/main/station_example_main.c @@ -1,5 +1,6 @@ /* - Voltage measurement device over WiFi (batched, ~30 Hz with jitter) + Voltage summary device (~20 Hz, 1000 samples) + Sends: start_ms, end_ms, [max, mean+std, mean, mean-std, min] */ #include @@ -7,6 +8,8 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" +#include +#include "math.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" @@ -15,14 +18,13 @@ #include "esp_adc/adc_oneshot.h" #include "esp_adc/adc_cali_scheme.h" #include "esp_http_client.h" +#include "esp_timer.h" /* ===================== CONSTS ===================== */ #define SERVER_URL "http://192.168.178.157:8080/data" -#define SAMPLE_RATE_HZ 20 -#define SAMPLE_PERIOD_MS (1000 / SAMPLE_RATE_HZ) -#define BATCH_SIZE 100 +#define NUM_MEASUREMENTS 100000 #define CHANNEL ADC_CHANNEL_6 #define CALIBRATION_FACTOR 1600.0f @@ -38,7 +40,6 @@ static const char *TAG = "VoltageSensor"; /* ===================== WIFI ===================== */ static EventGroupHandle_t s_wifi_event_group; - #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 @@ -67,7 +68,6 @@ static void event_handler(void* arg, esp_event_base_t event_base, void wifi_init_sta(void) { s_wifi_event_group = xEventGroupCreate(); - ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); @@ -137,6 +137,9 @@ static void adc_init(void) } } +const double upper = 0.24107260247486464 * CALIBRATION_FACTOR + CALIBRATION_FACTOR; +const double lower = -0.2247819992266048 * CALIBRATION_FACTOR + CALIBRATION_FACTOR; + static float read_voltage(void) { int raw, mv = 0; @@ -144,24 +147,24 @@ static float read_voltage(void) if (cali_enabled) { ESP_ERROR_CHECK(adc_cali_raw_to_voltage(cali_handle, raw, &mv)); } - return (mv - CALIBRATION_FACTOR) / CALIBRATION_FACTOR; + + return (float)mv; + // return (mv - lower) / (upper - lower) * 2.0 * 325.0 - 325.0; } /* ===================== HTTP ===================== */ -static void send_voltage_batch(esp_http_client_handle_t client, - const float *data, - size_t n) +static void send_summary(esp_http_client_handle_t client, + double start_ms, double end_ms, + double values[5]) { - char payload[1024]; - char *p = payload; - - p += sprintf(p, "{\"voltages\":["); - for (size_t i = 0; i < n; i++) { - p += sprintf(p, "%.4f%s", data[i], - (i + 1 < n) ? "," : ""); - } - p += sprintf(p, "]}"); + char payload[256]; + snprintf(payload, sizeof(payload), + "{\"start_ms\":%.0f," + "\"end_ms\":%.0f," + "\"values\":[%.6f,%.6f,%.6f,%.6f,%.6f]}", + start_ms, end_ms, + values[0], values[1], values[2], values[3], values[4]); esp_http_client_set_post_field(client, payload, strlen(payload)); esp_http_client_perform(client); @@ -174,28 +177,45 @@ static void measure_task(void *arg) esp_http_client_config_t cfg = { .url = SERVER_URL, .method = HTTP_METHOD_POST, - .timeout_ms = 3000, + .timeout_ms = 5000, }; esp_http_client_handle_t client = esp_http_client_init(&cfg); esp_http_client_set_header(client, "Content-Type", "application/json"); - float batch[BATCH_SIZE]; - size_t count = 0; - - TickType_t last_wake = xTaskGetTickCount(); - while (1) { - batch[count++] = read_voltage(); + double sum = 0, ssum = 0, max = -1e9, min = 1e9; + int i = 0; + double start_us = esp_timer_get_time(); + double start_ms = start_us / 1000.0; - if (count == BATCH_SIZE) { - send_voltage_batch(client, batch, count); - count = 0; - } + for (i = 0; i < NUM_MEASUREMENTS; i++) { + float m = read_voltage(); + sum += m; + ssum += m * m; + if (m > max) max = m; + if (m < min) min = m; - int jitter = rand() % 41; // 0..40 ms - vTaskDelayUntil(&last_wake, - pdMS_TO_TICKS(SAMPLE_PERIOD_MS + jitter)); + vTaskDelay(pdMS_TO_TICKS(2)); } + + double end_us = esp_timer_get_time(); + double end_ms = end_us / 1000.0; + + double mean = sum / NUM_MEASUREMENTS; + double variance = (ssum / NUM_MEASUREMENTS) - (mean * mean); + double std = variance > 0 ? sqrt(variance) : 0; + + double values[5]; + values[0] = max; + values[1] = mean + std; + values[2] = mean; + values[3] = mean - std; + values[4] = min; + + send_summary(client, start_ms, end_ms, values); + + vTaskDelay(pdMS_TO_TICKS(20)); + } } /* ===================== MAIN ===================== */ diff --git a/plot-times.py b/plot-times.py new file mode 100644 index 0000000..187b578 --- /dev/null +++ b/plot-times.py @@ -0,0 +1,67 @@ +import os +import numpy as np +import matplotlib.pyplot as plt + +DATA_DIR = "data" + +# find newest summary file +files = [f for f in os.listdir(DATA_DIR) + if f.startswith("summary_") and f.endswith(".csv")] +if not files: + raise FileNotFoundError("No summary files found in 'data/'") + +latest = max(files, key=lambda f: os.path.getmtime(os.path.join(DATA_DIR, f))) +path = os.path.join(DATA_DIR, latest) + +print(f"Using {path}") + +starts = [] +ends = [] + +with open(path, "r") as f: + next(f) # skip header + for line in f: + parts = line.strip().split(",") + if len(parts) < 2: + continue + start_ms = float(parts[0]) + end_ms = float(parts[1]) + starts.append(start_ms) + ends.append(end_ms) + +starts = np.array(starts) +ends = np.array(ends) + +# measurement durations +measurement = ends - starts + +# gap durations (between batches) +# drop last batch because there is no next-start to compute gap against +gaps = starts[1:] - ends[:-1] + +# align measurement durations to gaps dimension +# (both arrays indexed by batch interval "between measurements") +measurement = measurement[:-1] + +# percentages +total = measurement + gaps +measurement_pct = (measurement / total) * 100.0 +downtime_pct = (gaps / total) * 100.0 + +# time axis: batch index (no absolute time in summaries) +x = np.arange(len(measurement_pct)) + +# plot +plt.figure(figsize=(12, 6)) +plt.plot(x, measurement_pct, label="measurement time (%)") +plt.plot(x, downtime_pct, label="downtime (%)") + +plt.xlabel("Batch index") +plt.ylabel("Percentage of interval") +plt.title(f"Measurement vs. Downtime Percentage ({latest})") +plt.grid(True) +plt.legend() + +plt.tight_layout() +plt.show() + diff --git a/plot.py b/plot.py index 30e9ce7..cefc78e 100644 --- a/plot.py +++ b/plot.py @@ -1,45 +1,58 @@ import os import matplotlib.pyplot as plt -import matplotlib.dates as mdates import numpy as np -from datetime import datetime - DATA_DIR = "data" -# Find the latest data file -files = [f for f in os.listdir(DATA_DIR) if f.startswith("voltages_") and f.endswith(".csv")] +# Find the newest summary CSV +files = [f for f in os.listdir(DATA_DIR) if f.startswith("summary_") and f.endswith(".csv")] if not files: - raise FileNotFoundError("No voltage data files found in 'data/'") + raise FileNotFoundError("No summary files found in 'data/'") latest_file = max(files, key=lambda f: os.path.getmtime(os.path.join(DATA_DIR, f))) filepath = os.path.join(DATA_DIR, latest_file) print(f"Plotting data from {filepath}") -# Load CSV data -timestamps = [] -voltages = [] +# Load CSV +start_ms = [] +end_ms = [] +vals = [] # rows of [max, mean+std, mean, mean-std, min] with open(filepath, "r") as f: next(f) # skip header for line in f: - ts_str, v_str = line.strip().split(",") - ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S.%f") - timestamps.append(ts) - voltages.append(float(v_str)) + parts = line.strip().split(",") + if len(parts) != 7: + continue + s_ms, e_ms, *rest = parts + start_ms.append(float(s_ms)) + end_ms.append(float(e_ms)) + vals.append([float(x) for x in rest]) + +vals = np.array(vals) + +# Time axis: relative seconds since first interval start +start_ms = np.array(start_ms) # Plot -plt.figure(figsize=(12, 5)) -plt.scatter(timestamps, voltages, marker='o', linestyle='-', s=0.2) +plt.figure(figsize=(12, 6)) -plt.xlabel("Time (CET)") -plt.ylabel("Voltage (V)") -plt.title(f"Voltage Measurements ({latest_file})") +max_tot = vals[:, 1].mean() +min_tot = vals[:, 3].mean() + +print(max_tot, min_tot) +plt.scatter(start_ms, vals[:, 0], label="max", s=3) +plt.scatter(start_ms, vals[:, 1], label="mean+std", s=3) +plt.scatter(start_ms, vals[:, 2], label="mean", s=3) +plt.scatter(start_ms, vals[:, 3], label="mean-std", s=3) +plt.scatter(start_ms, vals[:, 4], label="min", s=3 ) + +plt.xlabel("Time since start (s)") +plt.ylabel("Voltage summary (V)") +plt.title(f"Voltage Summary ({latest_file})") plt.grid(True) - -# Format x-axis for readable time -plt.gcf().autofmt_xdate() -plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S")) +plt.legend() plt.tight_layout() plt.show() +