Compare commits

..

4 Commits

Author SHA1 Message Date
d0d3386ea0 Just formatting 2025-12-26 23:45:34 +01:00
48b9cc62e3 Change ip to the gullfoss server 2025-12-26 23:42:34 +01:00
af1ba47c4f Working 2025-12-26 23:40:25 +01:00
d4034fedd6 Mid progress 2025-12-26 23:30:02 +01:00
7 changed files with 245 additions and 136 deletions

5
.clang-format Normal file
View File

@@ -0,0 +1,5 @@
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.ascyii.de/jonas/voltage
go 1.25.5

86
main.go
View File

@@ -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))
}

View File

@@ -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 ".")

View File

@@ -1,34 +1,36 @@
/*
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>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_event.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
#include "math.h"
#include "nvs_flash.h"
#include <stdlib.h>
#include <string.h>
#include <time.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
#define EXAMPLE_ESP_WIFI_SSID "Jaguar"
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_ESP_WIFI_SSID "Jaguar"
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
#define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_BOTH
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA2_PSK
@@ -38,15 +40,13 @@ static const char *TAG = "VoltageSensor";
/* ===================== WIFI ===================== */
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
#define WIFI_FAIL_BIT BIT1
static int s_retry_num = 0;
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT &&
@@ -57,17 +57,14 @@ static void event_handler(void* arg, esp_event_base_t event_base,
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
} else if (event_base == IP_EVENT &&
event_id == IP_EVENT_STA_GOT_IP) {
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void)
{
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();
@@ -78,27 +75,25 @@ void wifi_init_sta(void)
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL,
&instance_any_id));
WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL,
&instance_got_ip));
IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS,
.threshold.authmode = ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD,
.sae_pwe_h2e = ESP_WIFI_SAE_MODE,
},
.sta =
{
.ssid = EXAMPLE_ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS,
.threshold.authmode = ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD,
.sae_pwe_h2e = ESP_WIFI_SAE_MODE,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE, pdFALSE, portMAX_DELAY);
}
@@ -108,8 +103,7 @@ static adc_oneshot_unit_handle_t adc_handle;
static adc_cali_handle_t cali_handle;
static bool cali_enabled = false;
static void adc_init(void)
{
static void adc_init(void) {
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = ADC_UNIT_1,
.ulp_mode = ADC_ULP_MODE_DISABLE,
@@ -137,31 +131,32 @@ static void adc_init(void)
}
}
static float read_voltage(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;
ESP_ERROR_CHECK(adc_oneshot_read(adc_handle, CHANNEL, &raw));
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)
{
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, "]}");
static void send_summary(esp_http_client_handle_t client, double start_ms,
double end_ms, double values[5]) {
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);
@@ -169,43 +164,59 @@ static void send_voltage_batch(esp_http_client_handle_t client,
/* ===================== TASK ===================== */
static void measure_task(void *arg)
{
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;
vTaskDelay(pdMS_TO_TICKS(2));
}
int jitter = rand() % 41; // 0..40 ms
vTaskDelayUntil(&last_wake,
pdMS_TO_TICKS(SAMPLE_PERIOD_MS + jitter));
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 ===================== */
void app_main(void)
{
void app_main(void) {
ESP_ERROR_CHECK(nvs_flash_init());
wifi_init_sta();
adc_init();
xTaskCreate(measure_task, "measure", 4096, NULL, 5, NULL);
}

67
plot-times.py Normal file
View 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
View File

@@ -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()