Compare commits
3 Commits
batch
...
48b9cc62e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 48b9cc62e3 | |||
| af1ba47c4f | |||
| d4034fedd6 |
86
main.go
86
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ".")
|
||||
|
||||
@@ -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 <string.h>
|
||||
@@ -7,6 +8,8 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include <time.h>
|
||||
#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 SERVER_URL "http://192.168.178.20:8080/data"
|
||||
|
||||
#define SAMPLE_RATE_HZ 20
|
||||
#define SAMPLE_PERIOD_MS (1000 / SAMPLE_RATE_HZ)
|
||||
#define BATCH_SIZE 100
|
||||
#define NUM_MEASUREMENTS 20000
|
||||
|
||||
#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,11 @@ static void adc_init(void)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const double upper = 1931.009445512987;
|
||||
const double lower = 1371.4123048766235;
|
||||
const double nominal_v = 230.0;
|
||||
|
||||
static float read_voltage(void)
|
||||
{
|
||||
int raw, mv = 0;
|
||||
@@ -144,24 +149,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 * nominal_v - nominal_v;
|
||||
}
|
||||
|
||||
/* ===================== 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 +179,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 ===================== */
|
||||
|
||||
67
plot-times.py
Normal file
67
plot-times.py
Normal file
@@ -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()
|
||||
|
||||
57
plot.py
57
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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user