Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save bakueikozo/84dc8fb3d59af18271abb9350d220901 to your computer and use it in GitHub Desktop.

Select an option

Save bakueikozo/84dc8fb3d59af18271abb9350d220901 to your computer and use it in GitHub Desktop.
#include <Arduino.h>
#include <HX711.h>
#include <WiFi.h>
#include <WebServer.h>
static constexpr int kHx711DoutPin = 5;
static constexpr int kHx711SckPin = 18;
// WiFi(自宅/社内APのSSID/PASSに書き換え)
static const char* kWifiSsid = "***";
static const char* kWifiPass = "***";
static HX711 scale;
static WebServer server(80);
// 1秒平均(生値)の直近約1分を保存
static constexpr uint32_t kBucketMs = 1000;
static constexpr size_t kHistorySeconds = 60;
static long g_history[kHistorySeconds] = {};
static size_t g_histWrite = 0; // 次に書く位置(リング)
static size_t g_histCount = 0; // 溜まっている個数(最大60)
// アルゴリズム検証用: 1秒平均の直近1時間(3600点)を保持
static constexpr size_t kAlgoHistorySeconds = 3600;
static long g_algoHistory[kAlgoHistorySeconds] = {};
static size_t g_algoWrite = 0;
static size_t g_algoCount = 0;
static uint32_t g_bucketStartMs = 0;
static int64_t g_bucketSum = 0;
static uint32_t g_bucketN = 0;
static void pushHistory(long v) {
g_history[g_histWrite] = v;
g_histWrite = (g_histWrite + 1) % kHistorySeconds;
if (g_histCount < kHistorySeconds) g_histCount++;
}
static void pushAlgoHistory(long v) {
g_algoHistory[g_algoWrite] = v;
g_algoWrite = (g_algoWrite + 1) % kAlgoHistorySeconds;
if (g_algoCount < kAlgoHistorySeconds) g_algoCount++;
}
struct AnalyzeParams {
long thr = 500; // 安定判定: max-min <= thr
long mthr = 200; // 安定区間の結合: mean差 <= mthr なら間のピークを無視して結合
size_t win = 10; // 判定窓(秒)
size_t hold = 2; // 安定開始/復帰に必要な連続安定秒数
size_t seconds = 600; // 解析対象(直近N秒)
};
static AnalyzeParams readAnalyzeParams() {
AnalyzeParams p;
if (server.hasArg("thr")) p.thr = server.arg("thr").toInt();
if (server.hasArg("mthr")) p.mthr = server.arg("mthr").toInt();
if (server.hasArg("win")) p.win = (size_t)server.arg("win").toInt();
if (server.hasArg("hold")) p.hold = (size_t)server.arg("hold").toInt();
if (server.hasArg("sec")) p.seconds = (size_t)server.arg("sec").toInt();
if (p.thr < 0) p.thr = -p.thr;
if (p.mthr < 0) p.mthr = -p.mthr;
if (p.win < 2) p.win = 2;
if (p.hold < 1) p.hold = 1;
if (p.seconds < 1) p.seconds = 1;
if (p.seconds > kAlgoHistorySeconds) p.seconds = kAlgoHistorySeconds;
return p;
}
static void handleRoot() {
// CDNのChart.jsで簡単に折れ線表示
const char* html = R"HTML(
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HX711 raw (last 180s)</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 16px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
button { padding: 8px 12px; }
#wrap { max-width: 1000px; }
#analysis { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
#lists { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 8px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 6px 8px; font-size: 12px; }
th { background: #f6f6f6; text-align: left; }
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<div id="wrap">
<h2>HX711 raw(直近180秒の1秒平均)</h2>
<div class="row">
<button id="reload">更新</button>
<div id="meta"></div>
</div>
<canvas id="c"></canvas>
<h3>解析(/analyze)</h3>
<div class="row">
<div id="anMeta"></div>
</div>
<div id="analysis"></div>
<div id="lists">
<div>
<h3>安定区間リスト</h3>
<div id="stableList"></div>
</div>
<div>
<h3>ピークリスト</h3>
<div id="peakList"></div>
</div>
</div>
</div>
<script>
let chart;
let yBase = null; // 初期値(最初に取得したraw)
const SEC = 180;
const ANALYZE_QUERY = `/analyze?thr=500&mthr=200&win=10&hold=2&sec=${SEC}`;
function buildNullArray(n) {
const a = new Array(n);
for (let i = 0; i < n; i++) a[i] = null;
return a;
}
function renderTable(targetId, headers, rows) {
const el = document.getElementById(targetId);
if (!rows.length) {
el.innerHTML = '<div>(none)</div>';
return;
}
const thead = `<thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>`;
const tbody = `<tbody>${rows.map(r => `<tr>${r.map(c => `<td>${c}</td>`).join('')}</tr>`).join('')}</tbody>`;
el.innerHTML = `<table>${thead}${tbody}</table>`;
}
function ensureChart(labels, data) {
const ctx = document.getElementById('c').getContext('2d');
if (!chart) {
chart = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'raw (avg/1s)',
data,
borderWidth: 2,
pointRadius: 0,
tension: 0.15
},
{
label: 'stable mean',
data: buildNullArray(data.length),
borderWidth: 3,
pointRadius: 0,
tension: 0,
borderColor: 'rgba(46, 125, 50, 0.9)'
},
{
type: 'scatter',
label: 'peak max',
data: [],
pointRadius: 4,
pointBackgroundColor: 'rgba(211, 47, 47, 0.95)'
},
{
type: 'scatter',
label: 'peak min',
data: [],
pointRadius: 4,
pointBackgroundColor: 'rgba(25, 118, 210, 0.95)'
}]
},
options: {
animation: false,
responsive: true,
scales: {
x: { title: { display: true, text: 'seconds ago' } },
y: {
title: { display: true, text: 'raw' },
min: (yBase === null) ? undefined : (yBase - 50000),
max: (yBase === null) ? undefined : (yBase + 50000)
}
}
}
});
} else {
chart.data.labels = labels;
chart.data.datasets[0].data = data;
// overlayはloadAnalyzeで毎回作り直すので、ここではクリアだけしておく
chart.data.datasets[1].data = buildNullArray(data.length);
chart.data.datasets[2].data = [];
chart.data.datasets[3].data = [];
chart.options.scales.y.min = (yBase === null) ? undefined : (yBase - 50000);
chart.options.scales.y.max = (yBase === null) ? undefined : (yBase + 50000);
chart.update('none');
}
}
async function load() {
const r = await fetch(`/series?sec=${SEC}`, { cache: 'no-store' });
const j = await r.json();
const labels = j.points.map(p => p.t);
const data = j.points.map(p => p.v);
document.getElementById('meta').textContent =
`points=${j.points.length} age_sec=${j.age_sec}`;
if (yBase === null && data.length) {
yBase = data[data.length - 1];
}
ensureChart(labels, data);
}
async function loadAnalyze() {
// パラメータは必要ならここで変更(thr/win/hold/sec)
const r = await fetch(ANALYZE_QUERY, { cache: 'no-store' });
const j = await r.json();
if (j.error) {
document.getElementById('anMeta').textContent = `error=${j.error}`;
document.getElementById('analysis').textContent = '';
return;
}
document.getElementById('anMeta').textContent =
`stable=${j.count?.stable ?? 0} peaks=${j.count?.peaks ?? 0} (sec=${j.params?.sec})`;
const lastStable = (j.stable && j.stable.length) ? j.stable[j.stable.length - 1] : null;
const lastPeak = (j.peaks && j.peaks.length) ? j.peaks[j.peaks.length - 1] : null;
const lines = [];
if (lastStable) {
lines.push(`last_stable: t=[${lastStable.t_start}..${lastStable.t_end}] mean=${lastStable.mean}`);
} else {
lines.push('last_stable: (none)');
}
if (lastPeak) {
lines.push(`last_peak: t=[${lastPeak.t_start}..${lastPeak.t_end}] max=${lastPeak.max} (t=${lastPeak.t_max}) min=${lastPeak.min} (t=${lastPeak.t_min})`);
} else {
lines.push('last_peak: (none)');
}
document.getElementById('analysis').textContent = lines.join('\n');
// グラフに安定区間・ピークを重ね描き
if (chart) {
const labelToIndex = new Map(chart.data.labels.map((t, i) => [t, i]));
const stableLine = buildNullArray(chart.data.labels.length);
for (const s of (j.stable || [])) {
const i0 = labelToIndex.get(s.t_start);
const i1 = labelToIndex.get(s.t_end);
if (i0 === undefined || i1 === undefined) continue;
const a = Math.min(i0, i1);
const b = Math.max(i0, i1);
for (let i = a; i <= b; i++) stableLine[i] = s.mean;
}
chart.data.datasets[1].data = stableLine;
const maxPts = [];
const minPts = [];
for (const p of (j.peaks || [])) {
maxPts.push({ x: p.t_max, y: p.max });
minPts.push({ x: p.t_min, y: p.min });
}
chart.data.datasets[2].data = maxPts;
chart.data.datasets[3].data = minPts;
chart.update('none');
}
// リスト表示
const stableRows = (j.stable || []).map(s => [
`${s.t_start}..${s.t_end}`,
String(s.mean),
`${s.start}..${s.end}`
]);
renderTable('stableList', ['t(sec ago)', 'mean', 'idx'], stableRows);
const peakRows = (j.peaks || []).map(p => [
`${p.t_start}..${p.t_end}`,
`max=${p.max} @t=${p.t_max}`,
`min=${p.min} @t=${p.t_min}`,
`${p.start}..${p.end}`
]);
renderTable('peakList', ['t(sec ago)', 'max', 'min', 'idx'], peakRows);
}
async function refreshAll() {
await load();
await loadAnalyze();
}
document.getElementById('reload').addEventListener('click', refreshAll);
refreshAll();
// 5秒ごとに「測定値+解析」を同期更新
setInterval(refreshAll, 5000);
</script>
</body>
</html>
)HTML";
server.send(200, "text/html; charset=utf-8", html);
}
static void handleData() {
// JSON: { points: [ {t:-59, v:123}, ... {t:0, v:456} ], age_sec:0 }
// tは「何秒前か」(0が最新)
String out;
out.reserve(4096);
out += "{\"points\":[";
const size_t n = g_histCount;
// リングの読み出し(古い→新しい)
const size_t start = (g_histWrite + kHistorySeconds - n) % kHistorySeconds;
for (size_t i = 0; i < n; i++) {
const size_t idx = (start + i) % kHistorySeconds;
const long v = g_history[idx];
const long t = (long)i - (long)(n - 1); // -(n-1) ... 0
if (i) out += ",";
out += "{\"t\":";
out += t;
out += ",\"v\":";
out += v;
out += "}";
}
out += "],\"age_sec\":0}";
server.send(200, "application/json; charset=utf-8", out);
}
static void handleSeries() {
// 解析/可視化用: 直近sec秒の1秒平均列(生値)
size_t sec = 180;
if (server.hasArg("sec")) sec = (size_t)server.arg("sec").toInt();
if (sec < 1) sec = 1;
if (sec > kAlgoHistorySeconds) sec = kAlgoHistorySeconds;
const size_t available = g_algoCount;
const size_t n = (available < sec) ? available : sec;
String out;
out.reserve(8192);
out += "{\"points\":[";
const size_t start = (g_algoWrite + kAlgoHistorySeconds - n) % kAlgoHistorySeconds;
for (size_t i = 0; i < n; i++) {
const long v = g_algoHistory[(start + i) % kAlgoHistorySeconds];
const long t = (long)i - (long)(n - 1); // -(n-1) ... 0
if (i) out += ",";
out += "{\"t\":";
out += t;
out += ",\"v\":";
out += v;
out += "}";
}
out += "],\"age_sec\":0}";
server.send(200, "application/json; charset=utf-8", out);
}
// 直近N秒の1秒平均列(g_algoHistory)を解析して、安定区間と変動区間のピークを返す
// 安定判定:
// - 直近win点の max-min <= thr を「その秒は安定」とする
// - その安定が hold 秒連続したら「安定区間開始」
// - 安定が崩れたら「変動区間」へ移行し、その間の max/min と出現位置を追跡
// - 再び hold 秒連続安定したら、その直前の変動区間のピークを確定し、新たな安定区間へ
static void handleAnalyze() {
const AnalyzeParams p = readAnalyzeParams();
const size_t available = g_algoCount;
const size_t n = (available < p.seconds) ? available : p.seconds;
if (n < (p.win + p.hold)) {
server.send(200, "application/json; charset=utf-8",
"{\"error\":\"not_enough_data\"}");
return;
}
// 古い→新しいの配列へ展開(最大3600)
static long series[kAlgoHistorySeconds];
const size_t start = (g_algoWrite + kAlgoHistorySeconds - n) % kAlgoHistorySeconds;
for (size_t i = 0; i < n; i++) {
series[i] = g_algoHistory[(start + i) % kAlgoHistorySeconds];
}
auto idxToT = [&](size_t idx) -> long {
// 解析窓の中で「何秒前か」: oldest=-(n-1), newest=0
return (long)idx - (long)(n - 1);
};
auto isStableAt = [&](size_t i) -> bool {
// i を右端とするwin幅の安定判定(i >= win-1 が必要)
long mx = series[i];
long mn = series[i];
for (size_t k = i + 1 - p.win; k <= i; k++) {
const long v = series[k];
if (v > mx) mx = v;
if (v < mn) mn = v;
}
return (mx - mn) <= p.thr;
};
// stableSegments: [a,b] と mean
static size_t segA[512];
static size_t segB[512];
static long segMean[512];
size_t segCount = 0;
enum class State : uint8_t { SeekingStable, InStable, InUnstable };
State st = State::SeekingStable;
size_t run = 0;
size_t a = 0;
int64_t sum = 0;
size_t cnt = 0;
for (size_t i = 0; i < n; i++) {
const bool stf = (i >= (p.win - 1)) ? isStableAt(i) : false;
if (stf) run++; else run = 0;
if (st == State::SeekingStable) {
if (stf && run == p.hold) {
a = i - p.hold + 1;
st = State::InStable;
sum = 0;
cnt = 0;
for (size_t k = a; k <= i; k++) {
sum += (int64_t)series[k];
cnt++;
}
}
continue;
}
if (st == State::InStable) {
if (stf) {
sum += (int64_t)series[i];
cnt++;
} else {
if (segCount < 512) {
segA[segCount] = a;
segB[segCount] = i - 1;
segMean[segCount] = (cnt > 0) ? (long)(sum / (int64_t)cnt) : 0;
segCount++;
}
st = State::InUnstable;
}
continue;
}
// InUnstable
if (stf && run == p.hold) {
a = i - p.hold + 1;
st = State::InStable;
sum = 0;
cnt = 0;
for (size_t k = a; k <= i; k++) {
sum += (int64_t)series[k];
cnt++;
}
}
}
if (st == State::InStable && segCount < 512) {
segA[segCount] = a;
segB[segCount] = n - 1;
segMean[segCount] = (cnt > 0) ? (long)(sum / (int64_t)cnt) : 0;
segCount++;
}
// meanがほぼ同じ安定区間は「同じ安定区間が揺れた」とみなして結合する
// - abs(mean差) <= mthr なら結合(間のピークは「無かったことにする」)
static size_t mA[512];
static size_t mB[512];
static long mMean[512];
size_t mCount = 0;
for (size_t i = 0; i < segCount; i++) {
if (mCount == 0) {
mA[0] = segA[i];
mB[0] = segB[i];
mMean[0] = segMean[i];
mCount = 1;
continue;
}
const long prevMean = mMean[mCount - 1];
const long curMean = segMean[i];
const long diff = (prevMean >= curMean) ? (prevMean - curMean) : (curMean - prevMean);
if (diff <= p.mthr) {
// 結合(平均は長さで重み付け)
const size_t prevLen = (mB[mCount - 1] >= mA[mCount - 1]) ? (mB[mCount - 1] - mA[mCount - 1] + 1) : 0;
const size_t curLen = (segB[i] >= segA[i]) ? (segB[i] - segA[i] + 1) : 0;
const int64_t wsum = (int64_t)prevMean * (int64_t)prevLen + (int64_t)curMean * (int64_t)curLen;
const size_t wlen = prevLen + curLen;
mB[mCount - 1] = segB[i];
mMean[mCount - 1] = (wlen > 0) ? (long)(wsum / (int64_t)wlen) : prevMean;
} else {
if (mCount < 512) {
mA[mCount] = segA[i];
mB[mCount] = segB[i];
mMean[mCount] = segMean[i];
mCount++;
}
}
}
String out;
out.reserve(16384);
out += "{";
out += "\"params\":{";
out += "\"thr\":"; out += p.thr;
out += ",\"mthr\":"; out += p.mthr;
out += ",\"win\":"; out += (long)p.win;
out += ",\"hold\":"; out += (long)p.hold;
out += ",\"sec\":"; out += (long)n;
out += "},";
out += "\"stable\":[";
for (size_t i = 0; i < mCount; i++) {
if (i) out += ",";
out += "{\"start\":";
out += (long)mA[i];
out += ",\"end\":";
out += (long)mB[i];
out += ",\"t_start\":";
out += idxToT(mA[i]);
out += ",\"t_end\":";
out += idxToT(mB[i]);
out += ",\"mean\":";
out += mMean[i];
out += "}";
}
out += "],";
// 変動区間: stable[k] の後〜 stable[k+1] の前 → その間の max/min を「ピーク」として記録
out += "\"peaks\":[";
size_t peakCount = 0;
for (size_t k = 0; k + 1 < mCount; k++) {
const size_t uStart = mB[k] + 1;
const size_t uEnd = (mA[k + 1] > 0) ? (mA[k + 1] - 1) : 0;
if (uStart >= n || uStart > uEnd) continue;
long mx = series[uStart];
long mn = series[uStart];
size_t mxAt = uStart;
size_t mnAt = uStart;
for (size_t i = uStart; i <= uEnd; i++) {
const long v = series[i];
if (v > mx) {
mx = v;
mxAt = i;
}
if (v < mn) {
mn = v;
mnAt = i;
}
}
if (peakCount) out += ",";
peakCount++;
out += "{\"start\":";
out += (long)uStart;
out += ",\"end\":";
out += (long)uEnd;
out += ",\"t_start\":";
out += idxToT(uStart);
out += ",\"t_end\":";
out += idxToT(uEnd);
out += ",\"max\":";
out += mx;
out += ",\"max_at\":";
out += (long)mxAt;
out += ",\"t_max\":";
out += idxToT(mxAt);
out += ",\"min\":";
out += mn;
out += ",\"min_at\":";
out += (long)mnAt;
out += ",\"t_min\":";
out += idxToT(mnAt);
out += "}";
}
out += "],";
out += "\"count\":{";
out += "\"stable\":";
out += (long)mCount;
out += ",\"peaks\":";
out += (long)peakCount;
out += "}";
out += "}";
server.send(200, "application/json; charset=utf-8", out);
}
void setup() {
Serial.begin(115200);
delay(500);
scale.begin(kHx711DoutPin, kHx711SckPin);
if (!scale.is_ready()) {
Serial.println("HX711: 応答なし(配線・電源を確認)");
for (;;) {
delay(1000);
}
}
scale.tare(10);
Serial.println("HX711: 準備完了(生値 raw を扱う)");
WiFi.mode(WIFI_STA);
WiFi.begin(kWifiSsid, kWifiPass);
Serial.print("WiFi: connecting");
while (WiFi.status() != WL_CONNECTED) {
delay(300);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi: connected, IP=");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/data", handleData);
server.on("/series", handleSeries);
server.on("/analyze", handleAnalyze);
server.begin();
Serial.println("HTTP: started (open '/' in browser)");
g_bucketStartMs = millis();
}
void loop() {
server.handleClient();
if (scale.is_ready()) {
// 生値をできるだけ多く集めて、1秒ごとに平均を確定する
const long raw = scale.read();
g_bucketSum += (int64_t)raw;
g_bucketN++;
}
const uint32_t now = millis();
if ((uint32_t)(now - g_bucketStartMs) >= kBucketMs) {
if (g_bucketN > 0) {
const long avg = (long)(g_bucketSum / (int64_t)g_bucketN);
pushHistory(avg);
pushAlgoHistory(avg);
Serial.print("raw_avg_1s: ");
Serial.println(avg);
}
g_bucketStartMs += kBucketMs;
g_bucketSum = 0;
g_bucketN = 0;
}
delay(2);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment