Added old project files in _Sort folders
BIN
Documentation/Codigo Fenix_20260115_092010_0000.pdf
Normal file
BIN
Documentation/Codigo_Fenix_logo.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
Documentation/DFPlayer_pinout.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
Documentation/ESP32_C6_PCB_Image.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
Documentation/ESP32_C6_PCB_Image_Dimentions.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
Documentation/ESP32_C6_info.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
Documentation/ESP32_C6_pinouts.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
5202
Documentation/GuardPhoenix.dxf
Normal file
9
Documentation/GuardPhoenix.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 659 752">
|
||||||
|
<!-- Generator: Adobe Illustrator 30.1.0, SVG Export Plug-In . SVG Version: 2.1.1 Build 136) -->
|
||||||
|
<path d="M57.33,331.27c11.49-.83,20.65-6.18,30.43-11.5,7.97-4.33,16.52-7.44,24.74-10.68l-15.18-.88c-19.22-1.12-35.46-.04-53.73-8.56-7.19-3.35-14.7-7.98-19.18-13.4-.56-.68,1.18-2.56,1.85-3.15s2.56,1.13,3.45,1.2c5.2.36,10.44.34,15.62.12l8.14-.34c2.47-.1,6.54.18,8.66-.06l17.16-1.97,18.05-1.95,27.18-3.07c-4.49-1.19-8.69-2.25-12.92-3.19l-9.74-2.15-17.76-4.25c-12.61-3.01-1.88-3.07-14.89-6.06-2.9-.67-5.02-1.87-7.49-3.88l-9.86-8.05c-2.25-1.84-3.56-3.68-6.34-5.34-2.99-3.64-5.25-9.21-7-14.82,13.88-.56,20.16,8.92,27.84,6.95,1.81,1.22,4.45,2.68,6.8,2.36,3.59-.48,6.25.88,9.23,1.37l8.75,1.45,8.7,1.39,29.42,6.18-27.47-17.54c-3.64-2.33-8.25-4.15-10.18-7.97-9.1-7.3-4.43-1.72-12.67-9.08l-10.16-9.07c-1.56-1.39-3.27-3.23-3.54-5.23-.3-2.18,6.83-13.82,12.69-10.57l6.23,3.46c5.17,2.87,10.19,6.79,15.64,9.07,2.54,1.07,4.72,1.92,6.9,3.14l9.61,5.39,1.77-.45c.5-.13,1.71,1.58,2.25,1.83l9.24,4.17c4.24,1.91,8.05,3.72,12.09,6.09l61.66,36.28,22.46,12.69c19.24,10.87,37.52,23.19,52.68,39.24,8.27,8.76,14.4,19,16.72,30.58,1.65,8.25-.19,16.48-1.56,24.55l-5.53,32.53c-4.46,26.19-9.7,55.96-30.88,75.36,1.82-11.45,5.54-20.94,4.57-31.72-2.67-29.82-22.28-41.41-16.3-74.3l-27.91,25.48c-9.68,8.84-20.44,15.6-32.33,20.86-10.58,3.11-21.38,6.05-31.92-.34,9.86-2.12,17.07-5.88,23.27-12.58,3.83-4.13,7.88-8.16,11.07-12.83l20.72-30.27c-5.32,1.07-10.06,2.33-15.08,4.11l-33.99,12.01c-11.37,4.02-39.5,8.56-50.43-.5,21.8-5.37,33.38-21.37,53.48-30.63-8.59.14-16.78.9-25.28,1.79l-20.9,2.18c-19.69,2.05-43.44-.44-60.14-11.08-.75-.48-1.65-2.01-1.81-2.63-.22-.82,2.03-1.64,3.11-1.72Z"/>
|
||||||
|
<path d="M447.44,357.98c-.73-.25-1.22,1.94-.86,2.47l16.54,24.72c4.35,6.51,9.49,12.2,15.16,17.64,6.47,6.21,14.68,9.09,23.22,10.25-22.19,11.86-49.76-5.22-65.71-19.68l-27.88-25.29c.7,13.89-1.54,25.18-8.14,36.74l-14.49,25.38c-7.65,13.4-7.88,27.38-4.78,42.91-12.43-18.37-14.09-41.31-6.86-62.01,6.34-18.16,8.1-35.44,4.46-54.5-4.55-23.79,13.47-49,30.21-64.11,4.98-4.5,9.64-8.94,15.21-12.84l47.71-33.39c21.07-14.75,42.96-26.49,65.92-38.04,17.06-8.58,32.46-18.64,46.81-30.96,10.62-9.13,17.07-19.94,22.05-34.09,8.73,30.69-7.22,58.76-33.28,75l-48.64,30.31,46.09-9.37c19.63-3.99,36.67-11.26,53.1-25.37-3.89,24.61-21.22,43.14-44.42,50.68-16.48,5.36-33.03,8.58-49.6,12.75l51.13,5.21c18.43,1.88,35.65.99,54.34-3.12-9.6,17.29-32.61,25.97-50.46,27.3l-41.54,3.1c10.38,4.38,18.9,7.84,27.76,12.7,10.13,5.55,20.05,8.96,32.64,9.44-18.47,13.22-40.56,17.47-62.77,15.22l-49.98-5.06c20.94,10.53,32.55,26.72,55.17,31.12-15.52,10.19-40.64,4.82-57.29-.95l-40.81-14.16Z"/>
|
||||||
|
<path d="M329.28,638.04c-30.37-33.68-46.47-76.77-48.67-121.7-2.11-43.11,7-85.59,16.83-127.37,6.37-27.06,13.64-59.43,15.29-86.81.9-15.04-3.3-45.97-16.39-56.64-17.19,4.02-34.82.91-47.6-12.03,36.6,5.86,60.95-22.86,83.98-28.78,6.12-1.57,12.41.27,18.71.55,15.89.7,33.28-11.1,46.83,2.53-3.34.98-6.48,1.09-8.71,2.17-9.34,4.55-17.45,10.07-25.31,16.52-27.41,22.51-17.91,53.02-7.81,82.49,4.84,14.13,8.97,27.86,11.29,42.68,4.75,30.43-5.84,51.45-18.13,77.87l-21.6,46.46c-19.86,42.71-21.89,92.55-6.91,136.92,2.45,8.5,5.99,15.28,8.22,25.11Z"/>
|
||||||
|
<path d="M406.33,281.91c15.31-33.72,17.09-69.47-1.81-103.02-30.42-53.99-101.68-68.91-150.61-34.73-1.18.82-2.93.6-3.15.08s-.11-1.77,1.37-3.13c36.79-33.74,97.63-37.5,136.78-3.01,43.03,32.28,52.82,100.86,17.42,143.81Z"/>
|
||||||
|
<path d="M273.79,164.39c-34.86,25.86-45.84,68.19-29.91,109.38-12.34-13.88-15.38-33.03-14.81-51.17.63-19.98,8.45-38.96,21.26-53.8,17.39-20.15,40.91-31.14,67.63-30.96,25.2.17,51.02,10.99,66.96,31.93-16.01-11.3-31.05-19.35-50.09-21.37-21.47-2.28-43.01,3.48-61.04,15.99Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
185
SupportProjects/ModbusRTU_EnvSim.html
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>FLL Umbrella Env BLE Control</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; margin: 16px; }
|
||||||
|
.row { margin: 10px 0; }
|
||||||
|
label { display:block; margin-bottom:6px; }
|
||||||
|
input[type="range"] { width: 100%; }
|
||||||
|
button { padding: 10px 12px; margin-right: 8px; }
|
||||||
|
code { background:#f3f3f3; padding:2px 6px; border-radius:6px; }
|
||||||
|
.pill { display:inline-block; padding:2px 8px; border-radius: 999px; background:#eee; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>FLL Umbrella Env BLE Control</h2>
|
||||||
|
<div class="row">
|
||||||
|
<button id="btnConnect">Connect</button>
|
||||||
|
<button id="btnDisconnect" disabled>Disconnect</button>
|
||||||
|
<span class="pill" id="status">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button id="btnLive" disabled>Set LIVE</button>
|
||||||
|
<button id="btnSim" disabled>Set SIM</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Temp (°C): <span id="tVal"></span></label>
|
||||||
|
<input id="temp" type="range" min="-10.00" max="60.00" step="0.01" value="25.00">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>RH (%): <span id="rhVal"></span></label>
|
||||||
|
<input id="rh" type="range" min="0.00" max="100.00" step="0.01" value="50.00">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Pressure (hPa): <span id="pVal"></span></label>
|
||||||
|
<input id="press" type="range" min="900.0" max="1100.0" step="0.1" value="1013.2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>IAQ override (0..500) [optional]: <span id="iaqVal"></span></label>
|
||||||
|
<input id="iaq" type="range" min="0" max="500" step="1" value="25">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button id="btnSend" disabled>Send SIM Values</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
BLE Service: <code>12345678-1234-5678-1234-56789abcdef0</code><br/>
|
||||||
|
Characteristic: <code>12345678-1234-5678-1234-56789abcdef1</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const UUID_SVC = "12345678-1234-5678-1234-56789abcdef0";
|
||||||
|
const UUID_CHR = "12345678-1234-5678-1234-56789abcdef1";
|
||||||
|
|
||||||
|
let device = null;
|
||||||
|
let server = null;
|
||||||
|
let chr = null;
|
||||||
|
|
||||||
|
const elStatus = document.getElementById("status");
|
||||||
|
const btnConnect = document.getElementById("btnConnect");
|
||||||
|
const btnDisconnect = document.getElementById("btnDisconnect");
|
||||||
|
const btnLive = document.getElementById("btnLive");
|
||||||
|
const btnSim = document.getElementById("btnSim");
|
||||||
|
const btnSend = document.getElementById("btnSend");
|
||||||
|
|
||||||
|
const sTemp = document.getElementById("temp");
|
||||||
|
const sRh = document.getElementById("rh");
|
||||||
|
const sPress = document.getElementById("press");
|
||||||
|
const sIaq = document.getElementById("iaq");
|
||||||
|
|
||||||
|
const tVal = document.getElementById("tVal");
|
||||||
|
const rhVal = document.getElementById("rhVal");
|
||||||
|
const pVal = document.getElementById("pVal");
|
||||||
|
const iaqVal = document.getElementById("iaqVal");
|
||||||
|
|
||||||
|
function setUiConnected(connected) {
|
||||||
|
btnDisconnect.disabled = !connected;
|
||||||
|
btnLive.disabled = !connected;
|
||||||
|
btnSim.disabled = !connected;
|
||||||
|
btnSend.disabled = !connected;
|
||||||
|
elStatus.textContent = connected ? "Connected" : "Disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLabels() {
|
||||||
|
tVal.textContent = Number(sTemp.value).toFixed(2);
|
||||||
|
rhVal.textContent = Number(sRh.value).toFixed(2);
|
||||||
|
pVal.textContent = Number(sPress.value).toFixed(1);
|
||||||
|
iaqVal.textContent = Number(sIaq.value).toFixed(0);
|
||||||
|
}
|
||||||
|
[sTemp, sRh, sPress, sIaq].forEach(x => x.addEventListener("input", refreshLabels));
|
||||||
|
refreshLabels();
|
||||||
|
|
||||||
|
async function writeBytes(u8) {
|
||||||
|
if (!chr) throw new Error("Not connected");
|
||||||
|
// Use writeValueWithoutResponse if available, otherwise writeValue
|
||||||
|
if (chr.writeValueWithoutResponse) return chr.writeValueWithoutResponse(u8);
|
||||||
|
return chr.writeValue(u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function packMode(mode) {
|
||||||
|
return new Uint8Array([0x01, mode ? 1 : 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function packSim(tempC, rhPct, pressHpa, iaqOverride) {
|
||||||
|
const temp_x100 = Math.round(tempC * 100);
|
||||||
|
const rh_x100 = Math.round(rhPct * 100);
|
||||||
|
const press_x10 = Math.round(pressHpa * 10);
|
||||||
|
const iaq = Math.round(iaqOverride);
|
||||||
|
|
||||||
|
const buf = new ArrayBuffer(9);
|
||||||
|
const dv = new DataView(buf);
|
||||||
|
dv.setUint8(0, 0x02);
|
||||||
|
dv.setInt16(1, temp_x100, true);
|
||||||
|
dv.setUint16(3, rh_x100, true);
|
||||||
|
dv.setUint16(5, press_x10, true);
|
||||||
|
dv.setUint16(7, iaq, true);
|
||||||
|
return new Uint8Array(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
btnConnect.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{ services: [UUID_SVC] }],
|
||||||
|
optionalServices: [UUID_SVC]
|
||||||
|
});
|
||||||
|
|
||||||
|
device.addEventListener("gattserverdisconnected", () => {
|
||||||
|
server = null; chr = null;
|
||||||
|
setUiConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
server = await device.gatt.connect();
|
||||||
|
const svc = await server.getPrimaryService(UUID_SVC);
|
||||||
|
chr = await svc.getCharacteristic(UUID_CHR);
|
||||||
|
|
||||||
|
setUiConnected(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Connect failed: " + e);
|
||||||
|
setUiConnected(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDisconnect.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
if (device?.gatt?.connected) device.gatt.disconnect();
|
||||||
|
} catch {}
|
||||||
|
server = null; chr = null;
|
||||||
|
setUiConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnLive.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await writeBytes(packMode(0));
|
||||||
|
} catch (e) { alert("Write failed: " + e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
btnSim.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await writeBytes(packMode(1));
|
||||||
|
} catch (e) { alert("Write failed: " + e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
btnSend.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
const t = Number(sTemp.value);
|
||||||
|
const rh = Number(sRh.value);
|
||||||
|
const p = Number(sPress.value);
|
||||||
|
const iaq = Number(sIaq.value);
|
||||||
|
await writeBytes(packSim(t, rh, p, iaq));
|
||||||
|
} catch (e) { alert("Write failed: " + e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
setUiConnected(false);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
_Sort/3dCode/TestPart.stl
Normal file
106
_Sort/BLE_Acc/BLE_Acc.ino
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <Adafruit_Sensor.h>
|
||||||
|
#include <Adafruit_ADXL345_U.h>
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
|
||||||
|
// ================== I2C PINS (CHANGE THESE) ==================
|
||||||
|
static const int I2C_SDA = 4; // <-- change to your wiring
|
||||||
|
static const int I2C_SCL = 3; // <-- change to your wiring
|
||||||
|
|
||||||
|
// ================== ADXL345 ==================
|
||||||
|
Adafruit_ADXL345_Unified accel = Adafruit_ADXL345_Unified(12345);
|
||||||
|
|
||||||
|
// ================== BLE UUIDS ==================
|
||||||
|
static const char* BLE_DEVICE_NAME = "ADXL345-C6";
|
||||||
|
static NimBLEUUID SERVICE_UUID("7e2a1001-1111-2222-3333-444455556666");
|
||||||
|
static NimBLEUUID CHAR_UUID ("7e2a1002-1111-2222-3333-444455556666");
|
||||||
|
|
||||||
|
NimBLEServer* pServer = nullptr;
|
||||||
|
NimBLECharacteristic* pChar = nullptr;
|
||||||
|
volatile bool deviceConnected = false;
|
||||||
|
|
||||||
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||||
|
void onConnect(NimBLEServer* s, NimBLEConnInfo& connInfo) override {
|
||||||
|
deviceConnected = true;
|
||||||
|
Serial.println("BLE: Connected");
|
||||||
|
}
|
||||||
|
void onDisconnect(NimBLEServer* s, NimBLEConnInfo& connInfo, int reason) override {
|
||||||
|
deviceConnected = false;
|
||||||
|
Serial.println("BLE: Disconnected");
|
||||||
|
NimBLEDevice::startAdvertising();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setupBLE() {
|
||||||
|
NimBLEDevice::init(BLE_DEVICE_NAME);
|
||||||
|
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||||
|
|
||||||
|
pServer = NimBLEDevice::createServer();
|
||||||
|
pServer->setCallbacks(new ServerCallbacks());
|
||||||
|
|
||||||
|
NimBLEService* pService = pServer->createService(SERVICE_UUID);
|
||||||
|
|
||||||
|
pChar = pService->createCharacteristic(
|
||||||
|
CHAR_UUID,
|
||||||
|
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
||||||
|
);
|
||||||
|
pChar->createDescriptor("2902"); // CCCD for notifications
|
||||||
|
|
||||||
|
pService->start();
|
||||||
|
|
||||||
|
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||||
|
adv->addServiceUUID(SERVICE_UUID);
|
||||||
|
adv->start();
|
||||||
|
|
||||||
|
Serial.println("BLE: Advertising started");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
Serial.println("\nESP32-C6 ADXL345 BLE starting...");
|
||||||
|
|
||||||
|
Wire.begin(I2C_SDA, I2C_SCL);
|
||||||
|
|
||||||
|
if (!accel.begin()) {
|
||||||
|
Serial.println("ERROR: ADXL345 not detected. Check wiring/power/address.");
|
||||||
|
while (true) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range options: ADXL345_RANGE_2_G, 4_G, 8_G, 16_G
|
||||||
|
accel.setRange(ADXL345_RANGE_4_G);
|
||||||
|
|
||||||
|
// Data rate options: ADXL345_DATARATE_100_HZ, 50_HZ, 25_HZ, etc.
|
||||||
|
accel.setDataRate(ADXL345_DATARATE_50_HZ);
|
||||||
|
|
||||||
|
setupBLE();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// ~20 Hz notify
|
||||||
|
static uint32_t lastMs = 0;
|
||||||
|
if (millis() - lastMs < 50) return;
|
||||||
|
lastMs = millis();
|
||||||
|
|
||||||
|
sensors_event_t event;
|
||||||
|
accel.getEvent(&event);
|
||||||
|
|
||||||
|
// Adafruit returns m/s^2. Convert to g.
|
||||||
|
const float g = 9.80665f;
|
||||||
|
float ax_g = event.acceleration.x / g;
|
||||||
|
float ay_g = event.acceleration.y / g;
|
||||||
|
float az_g = event.acceleration.z / g;
|
||||||
|
|
||||||
|
// CSV payload
|
||||||
|
char payload[64];
|
||||||
|
snprintf(payload, sizeof(payload), "%.4f,%.4f,%.4f", ax_g, ay_g, az_g);
|
||||||
|
|
||||||
|
Serial.print("TX: ");
|
||||||
|
Serial.println(payload);
|
||||||
|
|
||||||
|
pChar->setValue((uint8_t*)payload, strlen(payload));
|
||||||
|
if (deviceConnected) {
|
||||||
|
pChar->notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
183
_Sort/BLE_Env/BLE_Env.ino
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <Adafruit_Sensor.h>
|
||||||
|
#include <Adafruit_BME680.h>
|
||||||
|
|
||||||
|
#include <ModbusRTU.h> // from modbus-esp8266 library
|
||||||
|
|
||||||
|
// -------------------- I2C (MATCH YOUR WORKING CODE) --------------------
|
||||||
|
static const int I2C_SDA = 22;
|
||||||
|
static const int I2C_SCL = 21;
|
||||||
|
static const uint32_t I2C_FREQ = 100000;
|
||||||
|
|
||||||
|
// Try both common addresses (your working sketch likely uses one of these)
|
||||||
|
static const uint8_t BME_ADDR_1 = 0x76;
|
||||||
|
static const uint8_t BME_ADDR_2 = 0x77;
|
||||||
|
|
||||||
|
// -------------------- MODBUS UART (MATCH YOUR WORKING UART PINS) --------
|
||||||
|
static const int MB_RX = 4;
|
||||||
|
static const int MB_TX = 5;
|
||||||
|
static const uint32_t MB_BAUD = 9600;
|
||||||
|
static const uint8_t SLAVE_ID = 10;
|
||||||
|
|
||||||
|
// -------------------- Modbus registers ---------------------------------
|
||||||
|
ModbusRTU mb;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
HR_COUNTER = 0,
|
||||||
|
HR_TEMP_X100 = 1,
|
||||||
|
HR_RH_X100 = 2,
|
||||||
|
HR_PRESS_HPA_X10 = 3,
|
||||||
|
HR_GAS_OHMS_LO = 4,
|
||||||
|
HR_GAS_OHMS_HI = 5,
|
||||||
|
HR_STATUS = 6,
|
||||||
|
HR_ERROR_COUNT = 7,
|
||||||
|
HR_REG_COUNT = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
// status bits
|
||||||
|
static const uint16_t ST_OK = (1 << 0);
|
||||||
|
static const uint16_t ST_FALLBACK = (1 << 1);
|
||||||
|
static const uint16_t ST_NEW_SAMPLE = (1 << 2);
|
||||||
|
|
||||||
|
// -------------------- BME680 -------------------------------------------
|
||||||
|
Adafruit_BME680 bme;
|
||||||
|
bool bme_ok = false;
|
||||||
|
|
||||||
|
// -------------------- Values / timing ----------------------------------
|
||||||
|
uint16_t counter = 0;
|
||||||
|
uint16_t error_count = 0;
|
||||||
|
bool sample_toggle = false;
|
||||||
|
|
||||||
|
int16_t temp_x100 = 2500; // fallback 25.00 C
|
||||||
|
uint16_t rh_x100 = 5000; // fallback 50.00 %
|
||||||
|
uint16_t press_x10 = 10132; // fallback 1013.2 hPa
|
||||||
|
uint32_t gas_ohms = 123456; // fallback
|
||||||
|
|
||||||
|
static const uint32_t SAMPLE_PERIOD_MS = 1000;
|
||||||
|
uint32_t last_sample_ms = 0;
|
||||||
|
|
||||||
|
static inline void write_u32_to_regs(uint16_t loReg, uint16_t hiReg, uint32_t v) {
|
||||||
|
mb.Hreg(loReg, (uint16_t)(v & 0xFFFF));
|
||||||
|
mb.Hreg(hiReg, (uint16_t)((v >> 16) & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_bme() {
|
||||||
|
Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ);
|
||||||
|
|
||||||
|
if (bme.begin(BME_ADDR_1)) {
|
||||||
|
bme_ok = true;
|
||||||
|
} else if (bme.begin(BME_ADDR_2)) {
|
||||||
|
bme_ok = true;
|
||||||
|
} else {
|
||||||
|
bme_ok = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oversampling (sane defaults)
|
||||||
|
bme.setTemperatureOversampling(BME680_OS_8X);
|
||||||
|
bme.setHumidityOversampling(BME680_OS_2X);
|
||||||
|
bme.setPressureOversampling(BME680_OS_4X);
|
||||||
|
|
||||||
|
// Heater needed for gas resistance
|
||||||
|
bme.setGasHeater(320, 150);
|
||||||
|
|
||||||
|
bme_ok = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_modbus() {
|
||||||
|
// Use Serial1 on ESP32-C6 (Serial0 is USB serial)
|
||||||
|
Serial1.begin(MB_BAUD, SERIAL_8N1, MB_RX, MB_TX);
|
||||||
|
|
||||||
|
mb.begin(&Serial1);
|
||||||
|
mb.slave(SLAVE_ID);
|
||||||
|
|
||||||
|
for (uint16_t r = 0; r < HR_REG_COUNT; r++) {
|
||||||
|
mb.addHreg(r, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
mb.Hreg(HR_COUNTER, counter);
|
||||||
|
mb.Hreg(HR_TEMP_X100, (uint16_t)temp_x100);
|
||||||
|
mb.Hreg(HR_RH_X100, rh_x100);
|
||||||
|
mb.Hreg(HR_PRESS_HPA_X10, press_x10);
|
||||||
|
write_u32_to_regs(HR_GAS_OHMS_LO, HR_GAS_OHMS_HI, gas_ohms);
|
||||||
|
mb.Hreg(HR_ERROR_COUNT, error_count);
|
||||||
|
|
||||||
|
uint16_t st = 0;
|
||||||
|
if (bme_ok) st |= ST_OK;
|
||||||
|
else st |= ST_FALLBACK;
|
||||||
|
mb.Hreg(HR_STATUS, st);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
Serial.println("=== ESP32-C6 BME680 -> Modbus RTU (Holding Registers) ===");
|
||||||
|
Serial.printf("I2C SDA/SCL: %d/%d\n", I2C_SDA, I2C_SCL);
|
||||||
|
Serial.printf("Modbus RX/TX: %d/%d @ %lu\n", MB_RX, MB_TX, (unsigned long)MB_BAUD);
|
||||||
|
Serial.printf("Slave ID: %u\n", SLAVE_ID);
|
||||||
|
|
||||||
|
setup_bme();
|
||||||
|
Serial.printf("BME680 OK: %s\n", bme_ok ? "YES" : "NO");
|
||||||
|
|
||||||
|
setup_modbus();
|
||||||
|
Serial.println("Modbus ready. Read HR0..HR7 with FC03.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
mb.task();
|
||||||
|
|
||||||
|
const uint32_t now = millis();
|
||||||
|
if (now - last_sample_ms >= SAMPLE_PERIOD_MS) {
|
||||||
|
last_sample_ms = now;
|
||||||
|
counter++;
|
||||||
|
|
||||||
|
bool used_fallback = false;
|
||||||
|
|
||||||
|
if (bme_ok) {
|
||||||
|
if (bme.performReading()) {
|
||||||
|
float t = bme.temperature; // C
|
||||||
|
float h = bme.humidity; // %
|
||||||
|
float p_hpa = bme.pressure / 100.0f; // Pa -> hPa
|
||||||
|
uint32_t g = (uint32_t)bme.gas_resistance; // ohms
|
||||||
|
|
||||||
|
temp_x100 = (int16_t)(t * 100.0f);
|
||||||
|
rh_x100 = (uint16_t)(h * 100.0f);
|
||||||
|
press_x10 = (uint16_t)(p_hpa * 10.0f);
|
||||||
|
gas_ohms = g;
|
||||||
|
} else {
|
||||||
|
error_count++;
|
||||||
|
used_fallback = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
used_fallback = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update modbus registers
|
||||||
|
mb.Hreg(HR_COUNTER, counter);
|
||||||
|
mb.Hreg(HR_TEMP_X100, (uint16_t)temp_x100);
|
||||||
|
mb.Hreg(HR_RH_X100, rh_x100);
|
||||||
|
mb.Hreg(HR_PRESS_HPA_X10, press_x10);
|
||||||
|
write_u32_to_regs(HR_GAS_OHMS_LO, HR_GAS_OHMS_HI, gas_ohms);
|
||||||
|
mb.Hreg(HR_ERROR_COUNT, error_count);
|
||||||
|
|
||||||
|
sample_toggle = !sample_toggle;
|
||||||
|
|
||||||
|
uint16_t st = 0;
|
||||||
|
if (bme_ok && !used_fallback) st |= ST_OK;
|
||||||
|
if (used_fallback) st |= ST_FALLBACK;
|
||||||
|
if (sample_toggle) st |= ST_NEW_SAMPLE;
|
||||||
|
mb.Hreg(HR_STATUS, st);
|
||||||
|
|
||||||
|
// debug
|
||||||
|
Serial.printf("Tick %u | T=%.2fC RH=%.2f%% P=%.1fhPa Gas=%luΩ Status=0x%X Err=%u\n",
|
||||||
|
counter,
|
||||||
|
temp_x100 / 100.0f,
|
||||||
|
rh_x100 / 100.0f,
|
||||||
|
press_x10 / 10.0f,
|
||||||
|
(unsigned long)gas_ohms,
|
||||||
|
st,
|
||||||
|
error_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
_Sort/BLE_Example-Index.html
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>ESP32 BLE Control</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 680px; margin: 24px auto; padding: 0 16px; }
|
||||||
|
button { padding: 12px 16px; margin: 6px 6px 6px 0; font-size: 16px; }
|
||||||
|
.row { margin-top: 10px; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 10px; padding: 14px; margin-top: 12px; }
|
||||||
|
.value { font-size: 40px; font-weight: 700; }
|
||||||
|
.status { white-space: pre-wrap; background:#f7f7f7; padding:10px; border-radius:8px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>ESP32 BLE Random Value + LED Control</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<button id="btnConnect">Connect</button>
|
||||||
|
<button id="btnDisconnect" disabled>Disconnect</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>Device: <span id="deviceName">—</span></div>
|
||||||
|
<div>Connection: <span id="connState">disconnected</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div>Random value (notify):</div>
|
||||||
|
<div class="value" id="randValue">—</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div>LED command (write):</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="cmd" data-cmd="0" disabled>OFF</button>
|
||||||
|
<button class="cmd" data-cmd="1" disabled>RED</button>
|
||||||
|
<button class="cmd" data-cmd="2" disabled>GREEN</button>
|
||||||
|
<button class="cmd" data-cmd="3" disabled>BLUE</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div>Log</div>
|
||||||
|
<div class="status" id="log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
// ====== YOUR UUIDs (from your MicroPython code) ======
|
||||||
|
const SERVICE_UUID = '19b10000-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const SENSOR_CHAR_UUID = '19b10001-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const LED_CHAR_UUID = '19b10002-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
|
||||||
|
// UI
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const logEl = $('log');
|
||||||
|
const randEl = $('randValue');
|
||||||
|
const connEl = $('connState');
|
||||||
|
const devNameEl = $('deviceName');
|
||||||
|
const btnConnect = $('btnConnect');
|
||||||
|
const btnDisconnect = $('btnDisconnect');
|
||||||
|
const cmdButtons = Array.from(document.querySelectorAll('button.cmd'));
|
||||||
|
|
||||||
|
let device = null;
|
||||||
|
let server = null;
|
||||||
|
let sensorChar = null;
|
||||||
|
let ledChar = null;
|
||||||
|
|
||||||
|
const td = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
function log(...args) {
|
||||||
|
const msg = args.join(' ');
|
||||||
|
logEl.textContent = (msg + '\n' + logEl.textContent).slice(0, 4000);
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnectedUI(isConnected) {
|
||||||
|
connEl.textContent = isConnected ? 'connected' : 'disconnected';
|
||||||
|
btnDisconnect.disabled = !isConnected;
|
||||||
|
cmdButtons.forEach(b => b.disabled = !isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
try {
|
||||||
|
if (!navigator.bluetooth) {
|
||||||
|
alert('Web Bluetooth not available. Use Chrome/Edge (Windows) or Chrome (Android).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Requesting device...');
|
||||||
|
device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{ services: [SERVICE_UUID] }],
|
||||||
|
// If you want to match by name instead, you can do:
|
||||||
|
// filters: [{ name: 'ESP32' }],
|
||||||
|
optionalServices: [SERVICE_UUID]
|
||||||
|
});
|
||||||
|
|
||||||
|
devNameEl.textContent = device.name || '(no name)';
|
||||||
|
device.addEventListener('gattserverdisconnected', onDisconnected);
|
||||||
|
|
||||||
|
log('Connecting GATT...');
|
||||||
|
server = await device.gatt.connect();
|
||||||
|
|
||||||
|
log('Getting service...');
|
||||||
|
const service = await server.getPrimaryService(SERVICE_UUID);
|
||||||
|
|
||||||
|
log('Getting characteristics...');
|
||||||
|
sensorChar = await service.getCharacteristic(SENSOR_CHAR_UUID);
|
||||||
|
ledChar = await service.getCharacteristic(LED_CHAR_UUID);
|
||||||
|
|
||||||
|
// Subscribe to notifications for random value
|
||||||
|
log('Starting notifications for sensor characteristic...');
|
||||||
|
await sensorChar.startNotifications();
|
||||||
|
sensorChar.addEventListener('characteristicvaluechanged', onSensorNotify);
|
||||||
|
|
||||||
|
setConnectedUI(true);
|
||||||
|
log('Connected.');
|
||||||
|
} catch (e) {
|
||||||
|
log('Connect error:', e);
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSensorNotify(event) {
|
||||||
|
try {
|
||||||
|
const dv = event.target.value; // DataView
|
||||||
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||||||
|
|
||||||
|
// Your ESP32 sends UTF-8 text like b"42"
|
||||||
|
const text = td.decode(bytes).trim();
|
||||||
|
randEl.textContent = text;
|
||||||
|
|
||||||
|
// If you prefer number parsing:
|
||||||
|
// const num = parseInt(text, 10);
|
||||||
|
// randEl.textContent = Number.isFinite(num) ? String(num) : text;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log('Notify decode error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLedCommand(cmd) {
|
||||||
|
try {
|
||||||
|
if (!ledChar) return;
|
||||||
|
|
||||||
|
// Your ESP32 expects int.from_bytes(data,'big') -> send 1 byte 0..3
|
||||||
|
const payload = new Uint8Array([cmd & 0xFF]);
|
||||||
|
await ledChar.writeValue(payload);
|
||||||
|
log('Wrote LED cmd:', cmd);
|
||||||
|
} catch (e) {
|
||||||
|
log('Write error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnect() {
|
||||||
|
try {
|
||||||
|
if (device?.gatt?.connected) {
|
||||||
|
log('Disconnecting...');
|
||||||
|
device.gatt.disconnect();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('Disconnect error:', e);
|
||||||
|
} finally {
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnected() {
|
||||||
|
log('Device disconnected.');
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire UI
|
||||||
|
btnConnect.addEventListener('click', connect);
|
||||||
|
btnDisconnect.addEventListener('click', disconnect);
|
||||||
|
|
||||||
|
cmdButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const cmd = parseInt(btn.dataset.cmd, 10);
|
||||||
|
sendLedCommand(cmd);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setConnectedUI(false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
130
_Sort/BLE_Example.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
# Rui Santos & Sara Santos - Random Nerd Tutorials
|
||||||
|
# Complete project details at https://RandomNerdTutorials.com/micropython-esp32-bluetooth-low-energy-ble/
|
||||||
|
|
||||||
|
from micropython import const
|
||||||
|
import asyncio
|
||||||
|
import aioble
|
||||||
|
import bluetooth
|
||||||
|
import struct
|
||||||
|
from machine import Pin
|
||||||
|
from random import randint
|
||||||
|
import machine, neopixel
|
||||||
|
|
||||||
|
n = 1
|
||||||
|
p = 8
|
||||||
|
|
||||||
|
np = neopixel.NeoPixel(machine.Pin(p), n)
|
||||||
|
|
||||||
|
np[0] = (255, 0, 0)
|
||||||
|
np.write()
|
||||||
|
|
||||||
|
# Init random value
|
||||||
|
value = 0
|
||||||
|
|
||||||
|
# See the following for generating UUIDs:
|
||||||
|
# https://www.uuidgenerator.net/
|
||||||
|
_BLE_SERVICE_UUID = bluetooth.UUID('19b10000-e8f2-537e-4f6c-d104768a1214')
|
||||||
|
_BLE_SENSOR_CHAR_UUID = bluetooth.UUID('19b10001-e8f2-537e-4f6c-d104768a1214')
|
||||||
|
_BLE_LED_UUID = bluetooth.UUID('19b10002-e8f2-537e-4f6c-d104768a1214')
|
||||||
|
# How frequently to send advertising beacons.
|
||||||
|
_ADV_INTERVAL_MS = 250_000
|
||||||
|
|
||||||
|
# Register GATT server, the service and characteristics
|
||||||
|
ble_service = aioble.Service(_BLE_SERVICE_UUID)
|
||||||
|
sensor_characteristic = aioble.Characteristic(ble_service, _BLE_SENSOR_CHAR_UUID, read=True, notify=True)
|
||||||
|
led_characteristic = aioble.Characteristic(ble_service, _BLE_LED_UUID, read=True, write=True, notify=True, capture=True)
|
||||||
|
|
||||||
|
# Register service(s)
|
||||||
|
aioble.register_services(ble_service)
|
||||||
|
|
||||||
|
# Helper to encode the data characteristic UTF-8
|
||||||
|
def _encode_data(data):
|
||||||
|
return str(data).encode('utf-8')
|
||||||
|
|
||||||
|
# Helper to decode the LED characteristic encoding (bytes).
|
||||||
|
def _decode_data(data):
|
||||||
|
try:
|
||||||
|
if data is not None:
|
||||||
|
# Decode the UTF-8 data
|
||||||
|
number = int.from_bytes(data, 'big')
|
||||||
|
return number
|
||||||
|
except Exception as e:
|
||||||
|
print("Error decoding temperature:", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get sensor readings
|
||||||
|
def get_random_value():
|
||||||
|
return randint(0,100)
|
||||||
|
|
||||||
|
# Get new value and update characteristic
|
||||||
|
async def sensor_task():
|
||||||
|
while True:
|
||||||
|
value = get_random_value()
|
||||||
|
sensor_characteristic.write(_encode_data(value), send_update=True)
|
||||||
|
print('New random value written: ', value)
|
||||||
|
await asyncio.sleep_ms(1000)
|
||||||
|
|
||||||
|
# Serially wait for connections. Don't advertise while a central is connected.
|
||||||
|
async def peripheral_task():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with await aioble.advertise(
|
||||||
|
_ADV_INTERVAL_MS,
|
||||||
|
name="ESP32",
|
||||||
|
services=[_BLE_SERVICE_UUID],
|
||||||
|
) as connection:
|
||||||
|
print("Connection from", connection.device)
|
||||||
|
await connection.disconnected()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Catch the CancelledError
|
||||||
|
print("Peripheral task cancelled")
|
||||||
|
except Exception as e:
|
||||||
|
print("Error in peripheral_task:", e)
|
||||||
|
finally:
|
||||||
|
# Ensure the loop continues to the next iteration
|
||||||
|
await asyncio.sleep_ms(100)
|
||||||
|
|
||||||
|
async def wait_for_write():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
connection, data = await led_characteristic.written()
|
||||||
|
print(data)
|
||||||
|
print(type)
|
||||||
|
data = _decode_data(data)
|
||||||
|
print('Connection: ', connection)
|
||||||
|
print('Data: ', data)
|
||||||
|
if data == 0:
|
||||||
|
print('Turning LED OFF')
|
||||||
|
np[0] = (0, 0, 0)
|
||||||
|
np.write()
|
||||||
|
elif data == 1:
|
||||||
|
print('Turning LED RED')
|
||||||
|
np[0] = (255, 0, 0)
|
||||||
|
np.write()
|
||||||
|
elif data == 2:
|
||||||
|
print('Turning LED GREEN')
|
||||||
|
np[0] = (0, 255, 0)
|
||||||
|
np.write()
|
||||||
|
elif data == 3:
|
||||||
|
print('Turning LED BLUE')
|
||||||
|
np[0] = (0, 0, 255)
|
||||||
|
np.write()
|
||||||
|
else:
|
||||||
|
print('Unknown command')
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Catch the CancelledError
|
||||||
|
print("Peripheral task cancelled")
|
||||||
|
except Exception as e:
|
||||||
|
print("Error in peripheral_task:", e)
|
||||||
|
finally:
|
||||||
|
# Ensure the loop continues to the next iteration
|
||||||
|
await asyncio.sleep_ms(100)
|
||||||
|
|
||||||
|
# Run tasks
|
||||||
|
async def main():
|
||||||
|
t1 = asyncio.create_task(sensor_task())
|
||||||
|
t2 = asyncio.create_task(peripheral_task())
|
||||||
|
t3 = asyncio.create_task(wait_for_write())
|
||||||
|
await asyncio.gather(t1, t2)
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
156
_Sort/BME680.py
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
from micropython import const
|
||||||
|
import asyncio
|
||||||
|
import aioble
|
||||||
|
import bluetooth
|
||||||
|
import machine
|
||||||
|
import neopixel
|
||||||
|
import ujson as json
|
||||||
|
import time
|
||||||
|
import _thread
|
||||||
|
|
||||||
|
from machine import Pin, I2C
|
||||||
|
import bme680
|
||||||
|
|
||||||
|
# ----------------- NeoPixel -----------------
|
||||||
|
NEO_PIN = 8
|
||||||
|
NEO_COUNT = 1
|
||||||
|
np = neopixel.NeoPixel(machine.Pin(NEO_PIN), NEO_COUNT)
|
||||||
|
np[0] = (127, 0, 127)
|
||||||
|
np.write()
|
||||||
|
|
||||||
|
def set_led(cmd: int):
|
||||||
|
if cmd == 0:
|
||||||
|
np[0] = (0, 0, 0)
|
||||||
|
elif cmd == 1:
|
||||||
|
np[0] = (255, 0, 0)
|
||||||
|
elif cmd == 2:
|
||||||
|
np[0] = (0, 255, 0)
|
||||||
|
elif cmd == 3:
|
||||||
|
np[0] = (0, 0, 255)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
np.write()
|
||||||
|
|
||||||
|
def decode_cmd(data: bytes):
|
||||||
|
try:
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
return int.from_bytes(data, "big")
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ----------------- I2C + BME680 -----------------
|
||||||
|
# Your wiring: GPIO5=SCL, GPIO4=SDA
|
||||||
|
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400_000)
|
||||||
|
sensor = bme680.BME680(i2c)
|
||||||
|
|
||||||
|
# Shared sensor data (written by thread, read by asyncio)
|
||||||
|
_lock = _thread.allocate_lock()
|
||||||
|
_latest = {
|
||||||
|
"t_c": None,
|
||||||
|
"h_pct": None,
|
||||||
|
"p_hpa": None,
|
||||||
|
"gas_ohms": None,
|
||||||
|
"gas_valid": True,
|
||||||
|
"ts": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def bme680_thread():
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Gas is slow: do it every 10 seconds
|
||||||
|
gas_on = (i % 10 == 0)
|
||||||
|
|
||||||
|
# Fast settings for demo
|
||||||
|
sensor.measure(
|
||||||
|
gas=gas_on,
|
||||||
|
t_os=1, p_os=1, h_os=1,
|
||||||
|
iir_filter=0,
|
||||||
|
gas_temp=300,
|
||||||
|
gas_ms=80
|
||||||
|
)
|
||||||
|
#sensor.measure(gas=True)
|
||||||
|
t = sensor.temperature()
|
||||||
|
h = sensor.humidity()
|
||||||
|
p = sensor.pressure()
|
||||||
|
g = sensor.gas() if gas_on else {"ohms": None, "valid": False}
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
_latest["t_c"] = round(t, 2)
|
||||||
|
_latest["h_pct"] = round(h, 2)
|
||||||
|
_latest["p_hpa"] = round(p, 2)
|
||||||
|
_latest["gas_ohms"] = g["ohms"]
|
||||||
|
_latest["gas_valid"] = bool(g["valid"])
|
||||||
|
_latest["ts"] = time.time()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
_thread.start_new_thread(bme680_thread, ())
|
||||||
|
|
||||||
|
# ----------------- BLE UUIDs -----------------
|
||||||
|
_BLE_SERVICE_UUID = bluetooth.UUID("19b10000-e8f2-537e-4f6c-d104768a1214")
|
||||||
|
_BLE_SENSOR_CHAR_UUID = bluetooth.UUID("19b10001-e8f2-537e-4f6c-d104768a1214") # notify
|
||||||
|
_BLE_LED_CHAR_UUID = bluetooth.UUID("19b10002-e8f2-537e-4f6c-d104768a1214") # write
|
||||||
|
|
||||||
|
_ADV_INTERVAL_MS = const(250_000)
|
||||||
|
|
||||||
|
ble_service = aioble.Service(_BLE_SERVICE_UUID)
|
||||||
|
sensor_characteristic = aioble.Characteristic(ble_service, _BLE_SENSOR_CHAR_UUID, read=True, notify=True)
|
||||||
|
led_characteristic = aioble.Characteristic(ble_service, _BLE_LED_CHAR_UUID, read=True, write=True, capture=True)
|
||||||
|
aioble.register_services(ble_service)
|
||||||
|
|
||||||
|
def encode_json(obj) -> bytes:
|
||||||
|
return json.dumps(obj).encode("utf-8")
|
||||||
|
|
||||||
|
# ----------------- BLE tasks -----------------
|
||||||
|
async def notify_task():
|
||||||
|
last = None
|
||||||
|
while True:
|
||||||
|
with _lock:
|
||||||
|
payload = dict(_latest)
|
||||||
|
|
||||||
|
b = encode_json(payload)
|
||||||
|
if b != last:
|
||||||
|
sensor_characteristic.write(b, send_update=True)
|
||||||
|
last = b
|
||||||
|
|
||||||
|
await asyncio.sleep_ms(500) # UI feels live at 2 Hz
|
||||||
|
|
||||||
|
async def peripheral_task():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with await aioble.advertise(
|
||||||
|
_ADV_INTERVAL_MS,
|
||||||
|
name="ESP32-BME680",
|
||||||
|
services=[_BLE_SERVICE_UUID],
|
||||||
|
) as connection:
|
||||||
|
print("Connection from", connection.device)
|
||||||
|
await connection.disconnected()
|
||||||
|
print("Disconnected")
|
||||||
|
except Exception as e:
|
||||||
|
print("peripheral_task error:", e)
|
||||||
|
await asyncio.sleep_ms(200)
|
||||||
|
|
||||||
|
async def led_write_task():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
connection, data = await led_characteristic.written()
|
||||||
|
cmd = decode_cmd(data)
|
||||||
|
print("LED cmd:", cmd, "from", connection.device)
|
||||||
|
if cmd is not None:
|
||||||
|
set_led(cmd)
|
||||||
|
except Exception as e:
|
||||||
|
print("led_write_task error:", e)
|
||||||
|
await asyncio.sleep_ms(100)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await asyncio.gather(
|
||||||
|
notify_task(),
|
||||||
|
peripheral_task(),
|
||||||
|
led_write_task()
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
186
_Sort/BME680Index - Copy.html
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>ESP32 BME680 BLE Dashboard</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 760px; margin: 24px auto; padding: 0 16px; }
|
||||||
|
button { padding: 12px 16px; margin: 6px 6px 6px 0; font-size: 16px; }
|
||||||
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 10px; padding: 14px; }
|
||||||
|
.big { font-size: 34px; font-weight: 700; }
|
||||||
|
.label { color: #666; margin-bottom: 6px; }
|
||||||
|
.status { white-space: pre-wrap; background:#f7f7f7; padding:10px; border-radius:8px; }
|
||||||
|
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>ESP32 BME680 (BLE)</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button id="btnConnect">Connect</button>
|
||||||
|
<button id="btnDisconnect" disabled>Disconnect</button>
|
||||||
|
<div>Device: <span id="deviceName">—</span></div>
|
||||||
|
<div>Connection: <span id="connState">disconnected</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:12px;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Temperature</div>
|
||||||
|
<div class="big"><span id="t">—</span> °C</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Humidity</div>
|
||||||
|
<div class="big"><span id="h">—</span> %</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Pressure</div>
|
||||||
|
<div class="big"><span id="p">—</span> hPa</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Gas Resistance</div>
|
||||||
|
<div class="big"><span id="g">—</span> Ω</div>
|
||||||
|
<div>Valid: <span id="gv">—</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:12px;">
|
||||||
|
<div class="label">LED Control</div>
|
||||||
|
<button class="cmd" data-cmd="0" disabled>OFF</button>
|
||||||
|
<button class="cmd" data-cmd="1" disabled>RED</button>
|
||||||
|
<button class="cmd" data-cmd="2" disabled>GREEN</button>
|
||||||
|
<button class="cmd" data-cmd="3" disabled>BLUE</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:12px;">
|
||||||
|
<div class="label">Log</div>
|
||||||
|
<div class="status" id="log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const SERVICE_UUID = '19b10000-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const SENSOR_CHAR_UUID = '19b10001-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const LED_CHAR_UUID = '19b10002-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const logEl = $('log');
|
||||||
|
|
||||||
|
const td = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
let device = null;
|
||||||
|
let server = null;
|
||||||
|
let sensorChar = null;
|
||||||
|
let ledChar = null;
|
||||||
|
|
||||||
|
function log(...args) {
|
||||||
|
const msg = args.join(' ');
|
||||||
|
logEl.textContent = (msg + '\n' + logEl.textContent).slice(0, 5000);
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnectedUI(isConnected) {
|
||||||
|
$('connState').textContent = isConnected ? 'connected' : 'disconnected';
|
||||||
|
$('btnDisconnect').disabled = !isConnected;
|
||||||
|
document.querySelectorAll('button.cmd').forEach(b => b.disabled = !isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI(obj) {
|
||||||
|
if (typeof obj.t_c === 'number') $('t').textContent = obj.t_c.toFixed(2);
|
||||||
|
if (typeof obj.h_pct === 'number') $('h').textContent = obj.h_pct.toFixed(2);
|
||||||
|
if (typeof obj.p_hpa === 'number') $('p').textContent = obj.p_hpa.toFixed(2);
|
||||||
|
|
||||||
|
if (obj.gas_ohms === null || obj.gas_ohms === undefined) {
|
||||||
|
$('g').textContent = '—';
|
||||||
|
} else {
|
||||||
|
$('g').textContent = String(obj.gas_ohms);
|
||||||
|
}
|
||||||
|
$('gv').textContent = obj.gas_valid ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNotify(event) {
|
||||||
|
try {
|
||||||
|
const dv = event.target.value;
|
||||||
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||||||
|
const text = td.decode(bytes).trim();
|
||||||
|
|
||||||
|
const obj = JSON.parse(text);
|
||||||
|
updateUI(obj);
|
||||||
|
} catch (e) {
|
||||||
|
log('Notify decode/JSON error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
try {
|
||||||
|
if (!navigator.bluetooth) {
|
||||||
|
alert('Web Bluetooth not available. Use Chrome/Edge (Windows) or Chrome (Android).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Requesting device...');
|
||||||
|
device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{ services: [SERVICE_UUID] }],
|
||||||
|
optionalServices: [SERVICE_UUID]
|
||||||
|
});
|
||||||
|
|
||||||
|
$('deviceName').textContent = device.name || '(no name)';
|
||||||
|
device.addEventListener('gattserverdisconnected', onDisconnected);
|
||||||
|
|
||||||
|
log('Connecting GATT...');
|
||||||
|
server = await device.gatt.connect();
|
||||||
|
|
||||||
|
const service = await server.getPrimaryService(SERVICE_UUID);
|
||||||
|
|
||||||
|
sensorChar = await service.getCharacteristic(SENSOR_CHAR_UUID);
|
||||||
|
ledChar = await service.getCharacteristic(LED_CHAR_UUID);
|
||||||
|
|
||||||
|
log('Starting notifications...');
|
||||||
|
await sensorChar.startNotifications();
|
||||||
|
sensorChar.addEventListener('characteristicvaluechanged', onNotify);
|
||||||
|
|
||||||
|
setConnectedUI(true);
|
||||||
|
log('Connected.');
|
||||||
|
} catch (e) {
|
||||||
|
log('Connect error:', e);
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLed(cmd) {
|
||||||
|
try {
|
||||||
|
if (!ledChar) return;
|
||||||
|
const payload = new Uint8Array([cmd & 0xFF]);
|
||||||
|
await ledChar.writeValue(payload);
|
||||||
|
log('LED cmd:', cmd);
|
||||||
|
} catch (e) {
|
||||||
|
log('Write error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnected() {
|
||||||
|
log('Device disconnected.');
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnect() {
|
||||||
|
try {
|
||||||
|
if (device?.gatt?.connected) device.gatt.disconnect();
|
||||||
|
} finally {
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('btnConnect').addEventListener('click', connect);
|
||||||
|
$('btnDisconnect').addEventListener('click', disconnect);
|
||||||
|
|
||||||
|
document.querySelectorAll('button.cmd').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => sendLed(parseInt(btn.dataset.cmd, 10)));
|
||||||
|
});
|
||||||
|
|
||||||
|
setConnectedUI(false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
186
_Sort/BME680Index.html
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>ESP32 BME680 BLE Dashboard</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 760px; margin: 24px auto; padding: 0 16px; }
|
||||||
|
button { padding: 12px 16px; margin: 6px 6px 6px 0; font-size: 16px; }
|
||||||
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 10px; padding: 14px; }
|
||||||
|
.big { font-size: 34px; font-weight: 700; }
|
||||||
|
.label { color: #666; margin-bottom: 6px; }
|
||||||
|
.status { white-space: pre-wrap; background:#f7f7f7; padding:10px; border-radius:8px; }
|
||||||
|
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>ESP32 BME680 (BLE)</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button id="btnConnect">Connect</button>
|
||||||
|
<button id="btnDisconnect" disabled>Disconnect</button>
|
||||||
|
<div>Device: <span id="deviceName">—</span></div>
|
||||||
|
<div>Connection: <span id="connState">disconnected</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:12px;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Temperature</div>
|
||||||
|
<div class="big"><span id="t">—</span> °C</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Humidity</div>
|
||||||
|
<div class="big"><span id="h">—</span> %</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Pressure</div>
|
||||||
|
<div class="big"><span id="p">—</span> hPa</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Gas Resistance</div>
|
||||||
|
<div class="big"><span id="g">—</span> Ω</div>
|
||||||
|
<div>Valid: <span id="gv">—</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:12px;">
|
||||||
|
<div class="label">LED Control</div>
|
||||||
|
<button class="cmd" data-cmd="0" disabled>OFF</button>
|
||||||
|
<button class="cmd" data-cmd="1" disabled>RED</button>
|
||||||
|
<button class="cmd" data-cmd="2" disabled>GREEN</button>
|
||||||
|
<button class="cmd" data-cmd="3" disabled>BLUE</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:12px;">
|
||||||
|
<div class="label">Log</div>
|
||||||
|
<div class="status" id="log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const SERVICE_UUID = '19b10000-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const SENSOR_CHAR_UUID = '19b10001-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const LED_CHAR_UUID = '19b10002-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const logEl = $('log');
|
||||||
|
|
||||||
|
const td = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
let device = null;
|
||||||
|
let server = null;
|
||||||
|
let sensorChar = null;
|
||||||
|
let ledChar = null;
|
||||||
|
|
||||||
|
function log(...args) {
|
||||||
|
const msg = args.join(' ');
|
||||||
|
logEl.textContent = (msg + '\n' + logEl.textContent).slice(0, 5000);
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnectedUI(isConnected) {
|
||||||
|
$('connState').textContent = isConnected ? 'connected' : 'disconnected';
|
||||||
|
$('btnDisconnect').disabled = !isConnected;
|
||||||
|
document.querySelectorAll('button.cmd').forEach(b => b.disabled = !isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI(obj) {
|
||||||
|
if (typeof obj.t_c === 'number') $('t').textContent = obj.t_c.toFixed(2);
|
||||||
|
if (typeof obj.h_pct === 'number') $('h').textContent = obj.h_pct.toFixed(2);
|
||||||
|
if (typeof obj.p_hpa === 'number') $('p').textContent = obj.p_hpa.toFixed(2);
|
||||||
|
|
||||||
|
if (obj.gas_ohms === null || obj.gas_ohms === undefined) {
|
||||||
|
$('g').textContent = '—';
|
||||||
|
} else {
|
||||||
|
$('g').textContent = String(obj.gas_ohms);
|
||||||
|
}
|
||||||
|
$('gv').textContent = obj.gas_valid ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNotify(event) {
|
||||||
|
try {
|
||||||
|
const dv = event.target.value;
|
||||||
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||||||
|
const text = td.decode(bytes).trim();
|
||||||
|
|
||||||
|
const obj = JSON.parse(text);
|
||||||
|
updateUI(obj);
|
||||||
|
} catch (e) {
|
||||||
|
log('Notify decode/JSON error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
try {
|
||||||
|
if (!navigator.bluetooth) {
|
||||||
|
alert('Web Bluetooth not available. Use Chrome/Edge (Windows) or Chrome (Android).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Requesting device...');
|
||||||
|
device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{ services: [SERVICE_UUID] }],
|
||||||
|
optionalServices: [SERVICE_UUID]
|
||||||
|
});
|
||||||
|
|
||||||
|
$('deviceName').textContent = device.name || '(no name)';
|
||||||
|
device.addEventListener('gattserverdisconnected', onDisconnected);
|
||||||
|
|
||||||
|
log('Connecting GATT...');
|
||||||
|
server = await device.gatt.connect();
|
||||||
|
|
||||||
|
const service = await server.getPrimaryService(SERVICE_UUID);
|
||||||
|
|
||||||
|
sensorChar = await service.getCharacteristic(SENSOR_CHAR_UUID);
|
||||||
|
ledChar = await service.getCharacteristic(LED_CHAR_UUID);
|
||||||
|
|
||||||
|
log('Starting notifications...');
|
||||||
|
await sensorChar.startNotifications();
|
||||||
|
sensorChar.addEventListener('characteristicvaluechanged', onNotify);
|
||||||
|
|
||||||
|
setConnectedUI(true);
|
||||||
|
log('Connected.');
|
||||||
|
} catch (e) {
|
||||||
|
log('Connect error:', e);
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLed(cmd) {
|
||||||
|
try {
|
||||||
|
if (!ledChar) return;
|
||||||
|
const payload = new Uint8Array([cmd & 0xFF]);
|
||||||
|
await ledChar.writeValue(payload);
|
||||||
|
log('LED cmd:', cmd);
|
||||||
|
} catch (e) {
|
||||||
|
log('Write error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnected() {
|
||||||
|
log('Device disconnected.');
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnect() {
|
||||||
|
try {
|
||||||
|
if (device?.gatt?.connected) device.gatt.disconnect();
|
||||||
|
} finally {
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('btnConnect').addEventListener('click', connect);
|
||||||
|
$('btnDisconnect').addEventListener('click', disconnect);
|
||||||
|
|
||||||
|
document.querySelectorAll('button.cmd').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => sendLed(parseInt(btn.dataset.cmd, 10)));
|
||||||
|
});
|
||||||
|
|
||||||
|
setConnectedUI(false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
212
_Sort/BothSensorsIndex.html
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>ESP32 BME680 + ADXL345 BLE Dashboard</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 760px; margin: 24px auto; padding: 0 16px; }
|
||||||
|
button { padding: 12px 16px; margin: 6px 6px 6px 0; font-size: 16px; }
|
||||||
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 10px; padding: 14px; }
|
||||||
|
.big { font-size: 34px; font-weight: 700; }
|
||||||
|
.label { color: #666; margin-bottom: 6px; }
|
||||||
|
.status { white-space: pre-wrap; background:#f7f7f7; padding:10px; border-radius:8px; }
|
||||||
|
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
|
||||||
|
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>ESP32 BME680 + ADXL345 (BLE)</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button id="btnConnect">Connect</button>
|
||||||
|
<button id="btnDisconnect" disabled>Disconnect</button>
|
||||||
|
<div>Device: <span id="deviceName">—</span></div>
|
||||||
|
<div>Connection: <span id="connState">disconnected</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:12px;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Temperature</div>
|
||||||
|
<div class="big"><span id="t">—</span> °C</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Humidity</div>
|
||||||
|
<div class="big"><span id="h">—</span> %</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Pressure</div>
|
||||||
|
<div class="big"><span id="p">—</span> hPa</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Gas Resistance</div>
|
||||||
|
<div class="big"><span id="g">—</span> Ω</div>
|
||||||
|
<div>Valid: <span id="gv">—</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="grid-column: 1 / -1;">
|
||||||
|
<div class="label">ADXL345</div>
|
||||||
|
<div class="big">Shake: <span id="shake">—</span></div>
|
||||||
|
<div class="mono">ax=<span id="ax">—</span> g ay=<span id="ay">—</span> g az=<span id="az">—</span> g</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:12px;">
|
||||||
|
<div class="label">LED Control</div>
|
||||||
|
<button class="cmd" data-cmd="0" disabled>OFF</button>
|
||||||
|
<button class="cmd" data-cmd="1" disabled>RED</button>
|
||||||
|
<button class="cmd" data-cmd="2" disabled>GREEN</button>
|
||||||
|
<button class="cmd" data-cmd="3" disabled>BLUE</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:12px;">
|
||||||
|
<div class="label">Log</div>
|
||||||
|
<div class="status" id="log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const SERVICE_UUID = '19b10000-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const SENSOR_CHAR_UUID = '19b10001-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
const LED_CHAR_UUID = '19b10002-e8f2-537e-4f6c-d104768a1214';
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const logEl = $('log');
|
||||||
|
const td = new TextDecoder('utf-8');
|
||||||
|
|
||||||
|
let device, server, sensorChar, ledChar;
|
||||||
|
|
||||||
|
function log(...args) {
|
||||||
|
const msg = args.join(' ');
|
||||||
|
console.log(...args);
|
||||||
|
logEl.textContent = (msg + '\n' + logEl.textContent).slice(0, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnectedUI(on) {
|
||||||
|
$('connState').textContent = on ? 'connected' : 'disconnected';
|
||||||
|
$('btnDisconnect').disabled = !on;
|
||||||
|
document.querySelectorAll('button.cmd').forEach(b => b.disabled = !on);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setText(id, v) { $(id).textContent = (v === null || v === undefined) ? '—' : String(v); }
|
||||||
|
function setNum(id, v, d=2) {
|
||||||
|
if (v === null || v === undefined) return setText(id, '—');
|
||||||
|
if (typeof v === 'number') return setText(id, v.toFixed(d));
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isFinite(n)) return setText(id, v);
|
||||||
|
setText(id, n.toFixed(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI(o) {
|
||||||
|
// BME
|
||||||
|
setNum('t', o.t_c, 2);
|
||||||
|
setNum('h', o.h_pct, 2);
|
||||||
|
setNum('p', o.p_hpa, 2);
|
||||||
|
setText('g', (o.gas_ohms === null || o.gas_ohms === undefined) ? '—' : o.gas_ohms);
|
||||||
|
setText('gv', o.gas_valid ? 'true' : 'false');
|
||||||
|
|
||||||
|
// ADXL
|
||||||
|
setNum('ax', o.ax, 3);
|
||||||
|
setNum('ay', o.ay, 3);
|
||||||
|
setNum('az', o.az, 3);
|
||||||
|
setNum('shake', o.shake, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractJson(text) {
|
||||||
|
// sometimes you get extra junk; grab first {...} block
|
||||||
|
const s = text.indexOf('{');
|
||||||
|
const e = text.lastIndexOf('}');
|
||||||
|
if (s >= 0 && e > s) return text.slice(s, e + 1);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNotify(event) {
|
||||||
|
try {
|
||||||
|
const dv = event.target.value;
|
||||||
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||||||
|
let text = td.decode(bytes).trim();
|
||||||
|
|
||||||
|
// show raw for debugging
|
||||||
|
log('RX raw:', text.slice(0, 120));
|
||||||
|
|
||||||
|
text = extractJson(text);
|
||||||
|
const obj = JSON.parse(text);
|
||||||
|
updateUI(obj);
|
||||||
|
} catch (e) {
|
||||||
|
log('Notify parse error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
try {
|
||||||
|
log('Requesting device...');
|
||||||
|
device = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [{ services: [SERVICE_UUID] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
$('deviceName').textContent = device.name || '(no name)';
|
||||||
|
device.addEventListener('gattserverdisconnected', () => {
|
||||||
|
log('Disconnected.');
|
||||||
|
setConnectedUI(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
log('Connecting GATT...');
|
||||||
|
server = await device.gatt.connect();
|
||||||
|
|
||||||
|
log('Getting service...');
|
||||||
|
const service = await server.getPrimaryService(SERVICE_UUID);
|
||||||
|
|
||||||
|
log('Getting characteristics...');
|
||||||
|
sensorChar = await service.getCharacteristic(SENSOR_CHAR_UUID);
|
||||||
|
ledChar = await service.getCharacteristic(LED_CHAR_UUID);
|
||||||
|
|
||||||
|
log('Start notifications...');
|
||||||
|
await sensorChar.startNotifications();
|
||||||
|
sensorChar.addEventListener('characteristicvaluechanged', onNotify);
|
||||||
|
|
||||||
|
// also do one read immediately
|
||||||
|
try {
|
||||||
|
const dv = await sensorChar.readValue();
|
||||||
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||||||
|
let text = td.decode(bytes).trim();
|
||||||
|
log('Initial read:', text.slice(0, 120));
|
||||||
|
text = extractJson(text);
|
||||||
|
updateUI(JSON.parse(text));
|
||||||
|
} catch (e) {
|
||||||
|
log('Initial read failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setConnectedUI(true);
|
||||||
|
log('Connected OK.');
|
||||||
|
} catch (e) {
|
||||||
|
log('Connect error:', e);
|
||||||
|
setConnectedUI(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnect() {
|
||||||
|
try { if (device?.gatt?.connected) device.gatt.disconnect(); }
|
||||||
|
finally { setConnectedUI(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLed(cmd) {
|
||||||
|
try {
|
||||||
|
if (!ledChar) return;
|
||||||
|
await ledChar.writeValue(new Uint8Array([cmd & 0xFF]));
|
||||||
|
log('TX LED:', cmd);
|
||||||
|
} catch (e) {
|
||||||
|
log('LED write error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('btnConnect').addEventListener('click', connect);
|
||||||
|
$('btnDisconnect').addEventListener('click', disconnect);
|
||||||
|
document.querySelectorAll('button.cmd').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => sendLed(parseInt(btn.dataset.cmd, 10)));
|
||||||
|
});
|
||||||
|
|
||||||
|
setConnectedUI(false);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
33
_Sort/ModbusRTU_BME680/ModbusRTU_BME680.ino
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
static const int SDA_PIN = 5;
|
||||||
|
static const int SCL_PIN = 4;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(300);
|
||||||
|
|
||||||
|
Wire.begin(SDA_PIN, SCL_PIN);
|
||||||
|
Wire.setClock(100000);
|
||||||
|
|
||||||
|
pinMode(SDA_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(SCL_PIN, INPUT_PULLUP);
|
||||||
|
|
||||||
|
Serial.printf("SDA=%d reads %d | SCL=%d reads %d\n",
|
||||||
|
SDA_PIN, digitalRead(SDA_PIN),
|
||||||
|
SCL_PIN, digitalRead(SCL_PIN));
|
||||||
|
|
||||||
|
Serial.println("Scanning...");
|
||||||
|
int found = 0;
|
||||||
|
for (uint8_t addr = 1; addr < 127; addr++) {
|
||||||
|
Wire.beginTransmission(addr);
|
||||||
|
uint8_t err = Wire.endTransmission();
|
||||||
|
if (err == 0) {
|
||||||
|
Serial.printf("Found 0x%02X\n", addr);
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.printf("Done. Found %d device(s)\n", found);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
390
_Sort/ModbusRTU_BME680_1/ModbusRTU_BME680_1.ino
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
/*
|
||||||
|
ESP32-C6 Umbrella Env Sensor (FLL)
|
||||||
|
- BME680 (LIVE) OR BLE-controlled SIM mode
|
||||||
|
- Modbus RTU (Holding Registers) clean map HR0..HR7
|
||||||
|
- BLE GATT control (NimBLE) with compact binary packets
|
||||||
|
|
||||||
|
Modbus HR map (Holding Registers):
|
||||||
|
HR0 counter
|
||||||
|
HR1 status (bitfield)
|
||||||
|
HR2 mode (0=LIVE, 1=SIM)
|
||||||
|
HR3 error_count
|
||||||
|
HR4 temp_x100 (int16 stored in 16-bit register)
|
||||||
|
HR5 rh_x100 (uint16)
|
||||||
|
HR6 press_x10 (uint16)
|
||||||
|
HR7 iaq (uint16, 0..500, higher=worse, AQI-style)
|
||||||
|
|
||||||
|
BLE control packets (little-endian):
|
||||||
|
CMD 0x01 (set mode): [0]=0x01, [1]=mode(0/1)
|
||||||
|
CMD 0x02 (set sim): [0]=0x02,
|
||||||
|
[1..2]=temp_x100 (int16 LE),
|
||||||
|
[3..4]=rh_x100 (uint16 LE),
|
||||||
|
[5..6]=press_x10 (uint16 LE),
|
||||||
|
[7..8]=iaq (uint16 LE) OPTIONAL
|
||||||
|
If iaq not provided, device computes a simple heuristic IAQ for SIM.
|
||||||
|
|
||||||
|
Libraries:
|
||||||
|
- Adafruit BME680 library
|
||||||
|
- modbus-esp8266 (ModbusRTU.h)
|
||||||
|
- NimBLE-Arduino
|
||||||
|
|
||||||
|
Pins (match your working setup):
|
||||||
|
I2C SDA=22, SCL=21
|
||||||
|
RS-485 UART: RX=4, TX=5 @ 9600
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <Adafruit_Sensor.h>
|
||||||
|
#include <Adafruit_BME680.h>
|
||||||
|
|
||||||
|
#include <ModbusRTU.h> // modbus-esp8266 library
|
||||||
|
#include <NimBLEDevice.h> // NimBLE-Arduino
|
||||||
|
|
||||||
|
// -------------------- I2C --------------------
|
||||||
|
static const int I2C_SDA = 22;
|
||||||
|
static const int I2C_SCL = 21;
|
||||||
|
static const uint32_t I2C_FREQ = 100000;
|
||||||
|
static const uint8_t BME_ADDR_1 = 0x76;
|
||||||
|
static const uint8_t BME_ADDR_2 = 0x77;
|
||||||
|
|
||||||
|
// -------------------- MODBUS UART -----------
|
||||||
|
static const int MB_RX = 4;
|
||||||
|
static const int MB_TX = 5;
|
||||||
|
static const uint32_t MB_BAUD = 9600;
|
||||||
|
static const uint8_t SLAVE_ID = 10;
|
||||||
|
|
||||||
|
// -------------------- Modbus registers -------
|
||||||
|
enum {
|
||||||
|
HR_COUNTER = 0,
|
||||||
|
HR_STATUS = 1,
|
||||||
|
HR_MODE = 2,
|
||||||
|
HR_ERROR_COUNT = 3,
|
||||||
|
HR_TEMP_X100 = 4,
|
||||||
|
HR_RH_X100 = 5,
|
||||||
|
HR_PRESS_X10 = 6,
|
||||||
|
HR_IAQ = 7,
|
||||||
|
HR_REG_COUNT = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
// status bits (HR1)
|
||||||
|
static const uint16_t ST_OK = (1 << 0);
|
||||||
|
static const uint16_t ST_FALLBACK = (1 << 1);
|
||||||
|
static const uint16_t ST_NEW_SAMPLE = (1 << 2);
|
||||||
|
static const uint16_t ST_SIM_ACTIVE = (1 << 3);
|
||||||
|
static const uint16_t ST_BLE_CONNECTED = (1 << 4);
|
||||||
|
|
||||||
|
// mode (HR2)
|
||||||
|
static const uint16_t MODE_LIVE = 0;
|
||||||
|
static const uint16_t MODE_SIM = 1;
|
||||||
|
|
||||||
|
// -------------------- Data model -------------
|
||||||
|
struct EnvData {
|
||||||
|
uint16_t counter;
|
||||||
|
uint16_t status;
|
||||||
|
uint16_t mode;
|
||||||
|
uint16_t error_count;
|
||||||
|
int16_t temp_x100;
|
||||||
|
uint16_t rh_x100;
|
||||||
|
uint16_t press_x10;
|
||||||
|
uint16_t iaq;
|
||||||
|
};
|
||||||
|
|
||||||
|
EnvData env; // published
|
||||||
|
EnvData sim; // simulated target values
|
||||||
|
|
||||||
|
// -------------------- BME680 -----------------
|
||||||
|
Adafruit_BME680 bme;
|
||||||
|
bool bme_ok = false;
|
||||||
|
|
||||||
|
// -------------------- Modbus -----------------
|
||||||
|
ModbusRTU mb;
|
||||||
|
|
||||||
|
// -------------------- Timing -----------------
|
||||||
|
static const uint32_t SAMPLE_PERIOD_MS = 1000;
|
||||||
|
uint32_t last_sample_ms = 0;
|
||||||
|
bool heartbeat = false;
|
||||||
|
|
||||||
|
// -------------------- IAQ-ish baseline --------
|
||||||
|
// NOTE: This is NOT official AQI (PM2.5 etc.). It's an AQI-style 0..500 indicator
|
||||||
|
// derived from BME680 gas resistance relative to a slowly moving baseline.
|
||||||
|
float gas_baseline = 0.0f;
|
||||||
|
bool baseline_ready = false;
|
||||||
|
|
||||||
|
static const float BASELINE_ALPHA = 0.005f; // slower drift baseline (0.5% new per sample)
|
||||||
|
static const float HUM_TARGET = 40.0f; // %RH target
|
||||||
|
static const float HUM_PENALTY_GAIN = 0.5f; // penalty per %RH away from target (scaled later)
|
||||||
|
|
||||||
|
// -------------------- BLE UUIDs --------------
|
||||||
|
// Use your own if you prefer, but keep them stable once deployed.
|
||||||
|
static const char* BLE_DEV_NAME = "FLL-Umbrella-Env";
|
||||||
|
static const char* UUID_SVC_CTRL = "12345678-1234-5678-1234-56789abcdef0";
|
||||||
|
static const char* UUID_CHR_CTRL = "12345678-1234-5678-1234-56789abcdef1";
|
||||||
|
|
||||||
|
volatile bool ble_connected = false;
|
||||||
|
|
||||||
|
// ---------- Helpers ----------
|
||||||
|
static inline uint16_t clamp_u16(int32_t v, uint16_t lo, uint16_t hi) {
|
||||||
|
if (v < (int32_t)lo) return lo;
|
||||||
|
if (v > (int32_t)hi) return hi;
|
||||||
|
return (uint16_t)v;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t computeIAQFromGas(uint32_t gas_ohms, float humidity) {
|
||||||
|
if (gas_ohms < 100) gas_ohms = 100; // avoid divide weirdness
|
||||||
|
|
||||||
|
if (!baseline_ready) {
|
||||||
|
gas_baseline = (float)gas_ohms;
|
||||||
|
baseline_ready = true;
|
||||||
|
} else {
|
||||||
|
gas_baseline = gas_baseline * (1.0f - BASELINE_ALPHA) + (float)gas_ohms * BASELINE_ALPHA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// worse air tends to LOWER gas resistance => baseline/gas increases
|
||||||
|
float ratio = (gas_baseline > 1.0f) ? (gas_baseline / (float)gas_ohms) : 1.0f;
|
||||||
|
|
||||||
|
// map ratio to 0..500-ish, tuned to be stable and demo-friendly
|
||||||
|
float score = (ratio - 1.0f) * 120.0f;
|
||||||
|
|
||||||
|
// humidity compensation (far from ~40% adds penalty)
|
||||||
|
score += fabsf(humidity - HUM_TARGET) * HUM_PENALTY_GAIN;
|
||||||
|
|
||||||
|
if (score < 0) score = 0;
|
||||||
|
if (score > 500) score = 500;
|
||||||
|
return (uint16_t)(score + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If SIM IAQ not provided, compute a simple heuristic so it still "moves"
|
||||||
|
uint16_t computeIAQHeuristicSIM(int16_t temp_x100, uint16_t rh_x100, uint16_t press_x10) {
|
||||||
|
// Start "good"
|
||||||
|
int32_t score = 25;
|
||||||
|
|
||||||
|
float t = temp_x100 / 100.0f;
|
||||||
|
float rh = rh_x100 / 100.0f;
|
||||||
|
float p = press_x10 / 10.0f;
|
||||||
|
|
||||||
|
// Heuristic penalties (demo knobs)
|
||||||
|
// Too hot or too cold => worse
|
||||||
|
if (t > 30) score += (int32_t)((t - 30) * 12);
|
||||||
|
if (t < 15) score += (int32_t)((15 - t) * 8);
|
||||||
|
|
||||||
|
// Humidity far from 40-55 => worse
|
||||||
|
score += (int32_t)(fabsf(rh - 45.0f) * 2.0f);
|
||||||
|
|
||||||
|
// Pressure unusually low (storm vibe) => worse (just for demo)
|
||||||
|
if (p < 990) score += (int32_t)((990 - p) * 1.5f);
|
||||||
|
|
||||||
|
return clamp_u16(score, 0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
void publishModbus() {
|
||||||
|
mb.Hreg(HR_COUNTER, env.counter);
|
||||||
|
mb.Hreg(HR_STATUS, env.status);
|
||||||
|
mb.Hreg(HR_MODE, env.mode);
|
||||||
|
mb.Hreg(HR_ERROR_COUNT, env.error_count);
|
||||||
|
mb.Hreg(HR_TEMP_X100, (uint16_t)env.temp_x100); // int16 stored as uint16
|
||||||
|
mb.Hreg(HR_RH_X100, env.rh_x100);
|
||||||
|
mb.Hreg(HR_PRESS_X10, env.press_x10);
|
||||||
|
mb.Hreg(HR_IAQ, env.iaq);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_bme() {
|
||||||
|
Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ);
|
||||||
|
|
||||||
|
if (bme.begin(BME_ADDR_1)) bme_ok = true;
|
||||||
|
else if (bme.begin(BME_ADDR_2)) bme_ok = true;
|
||||||
|
else bme_ok = false;
|
||||||
|
|
||||||
|
if (!bme_ok) return;
|
||||||
|
|
||||||
|
bme.setTemperatureOversampling(BME680_OS_8X);
|
||||||
|
bme.setHumidityOversampling(BME680_OS_2X);
|
||||||
|
bme.setPressureOversampling(BME680_OS_4X);
|
||||||
|
bme.setGasHeater(320, 150); // needed for gas resistance
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_modbus() {
|
||||||
|
// ESP32-C6: Serial0 is USB serial; Serial1 for RS-485 UART
|
||||||
|
// If your board core uses different serial mapping, adjust here.
|
||||||
|
Serial1.begin(MB_BAUD, SERIAL_8N1, MB_RX, MB_TX);
|
||||||
|
|
||||||
|
mb.begin(&Serial1);
|
||||||
|
mb.slave(SLAVE_ID);
|
||||||
|
|
||||||
|
for (uint16_t r = 0; r < HR_REG_COUNT; r++) {
|
||||||
|
mb.addHreg(r, 0);
|
||||||
|
}
|
||||||
|
publishModbus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- BLE Server ----------
|
||||||
|
class CtrlCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
|
void onWrite(NimBLECharacteristic* pChr) override {
|
||||||
|
std::string v = pChr->getValue();
|
||||||
|
if (v.size() < 2) return;
|
||||||
|
|
||||||
|
const uint8_t* b = (const uint8_t*)v.data();
|
||||||
|
uint8_t cmd = b[0];
|
||||||
|
|
||||||
|
if (cmd == 0x01) { // set mode
|
||||||
|
uint16_t m = b[1] ? MODE_SIM : MODE_LIVE;
|
||||||
|
env.mode = m;
|
||||||
|
// keep sim values as-is; just switching source
|
||||||
|
} else if (cmd == 0x02) { // set sim values
|
||||||
|
if (v.size() < 7) return;
|
||||||
|
|
||||||
|
auto u16le = [&](int idx) -> uint16_t {
|
||||||
|
return (uint16_t)b[idx] | ((uint16_t)b[idx + 1] << 8);
|
||||||
|
};
|
||||||
|
auto i16le = [&](int idx) -> int16_t {
|
||||||
|
return (int16_t)u16le(idx);
|
||||||
|
};
|
||||||
|
|
||||||
|
sim.temp_x100 = i16le(1);
|
||||||
|
sim.rh_x100 = u16le(3);
|
||||||
|
sim.press_x10 = u16le(5);
|
||||||
|
|
||||||
|
if (v.size() >= 9) {
|
||||||
|
sim.iaq = u16le(7);
|
||||||
|
} else {
|
||||||
|
sim.iaq = computeIAQHeuristicSIM(sim.temp_x100, sim.rh_x100, sim.press_x10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||||
|
void onConnect(NimBLEServer* pServer) override { ble_connected = true; }
|
||||||
|
void onDisconnect(NimBLEServer* pServer) override {
|
||||||
|
ble_connected = false;
|
||||||
|
// keep advertising
|
||||||
|
NimBLEDevice::startAdvertising();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setup_ble() {
|
||||||
|
NimBLEDevice::init(BLE_DEV_NAME);
|
||||||
|
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||||
|
|
||||||
|
NimBLEServer* server = NimBLEDevice::createServer();
|
||||||
|
server->setCallbacks(new ServerCallbacks());
|
||||||
|
|
||||||
|
NimBLEService* svc = server->createService(UUID_SVC_CTRL);
|
||||||
|
|
||||||
|
NimBLECharacteristic* chr = svc->createCharacteristic(
|
||||||
|
UUID_CHR_CTRL,
|
||||||
|
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
|
||||||
|
);
|
||||||
|
chr->setCallbacks(new CtrlCallbacks());
|
||||||
|
|
||||||
|
svc->start();
|
||||||
|
|
||||||
|
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||||
|
adv->addServiceUUID(UUID_SVC_CTRL);
|
||||||
|
adv->setScanResponse(true);
|
||||||
|
adv->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Main ----------
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
Serial.println("\n=== ESP32-C6 BME680 + BLE SIM -> Modbus RTU (HR0..HR7) ===");
|
||||||
|
Serial.printf("I2C SDA/SCL: %d/%d\n", I2C_SDA, I2C_SCL);
|
||||||
|
Serial.printf("Modbus RX/TX: %d/%d @ %lu\n", MB_RX, MB_TX, (unsigned long)MB_BAUD);
|
||||||
|
Serial.printf("Slave ID: %u\n", SLAVE_ID);
|
||||||
|
|
||||||
|
// init defaults
|
||||||
|
env.counter = 0;
|
||||||
|
env.status = 0;
|
||||||
|
env.mode = MODE_LIVE;
|
||||||
|
env.error_count = 0;
|
||||||
|
env.temp_x100 = 2500;
|
||||||
|
env.rh_x100 = 5000;
|
||||||
|
env.press_x10 = 10132;
|
||||||
|
env.iaq = 25;
|
||||||
|
|
||||||
|
// default sim values
|
||||||
|
sim = env;
|
||||||
|
|
||||||
|
setup_bme();
|
||||||
|
Serial.printf("BME680 OK: %s\n", bme_ok ? "YES" : "NO");
|
||||||
|
|
||||||
|
setup_modbus();
|
||||||
|
setup_ble();
|
||||||
|
|
||||||
|
Serial.println("Modbus ready (FC03 HR0..HR7). BLE control ready.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
mb.task();
|
||||||
|
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now - last_sample_ms < SAMPLE_PERIOD_MS) return;
|
||||||
|
last_sample_ms = now;
|
||||||
|
|
||||||
|
// heartbeat
|
||||||
|
heartbeat = !heartbeat;
|
||||||
|
|
||||||
|
env.counter++;
|
||||||
|
|
||||||
|
// rebuild status fresh each tick (don’t accumulate stale bits)
|
||||||
|
uint16_t st = 0;
|
||||||
|
if (heartbeat) st |= ST_NEW_SAMPLE;
|
||||||
|
if (ble_connected) st |= ST_BLE_CONNECTED;
|
||||||
|
if (env.mode == MODE_SIM) st |= ST_SIM_ACTIVE;
|
||||||
|
|
||||||
|
bool used_fallback = false;
|
||||||
|
bool update_ok = false;
|
||||||
|
|
||||||
|
if (env.mode == MODE_SIM) {
|
||||||
|
// publish simulated values
|
||||||
|
env.temp_x100 = sim.temp_x100;
|
||||||
|
env.rh_x100 = sim.rh_x100;
|
||||||
|
env.press_x10 = sim.press_x10;
|
||||||
|
env.iaq = sim.iaq; // may be written by BLE or auto heuristic
|
||||||
|
update_ok = true;
|
||||||
|
} else {
|
||||||
|
// LIVE mode
|
||||||
|
if (bme_ok && bme.performReading()) {
|
||||||
|
float t = bme.temperature; // C
|
||||||
|
float h = bme.humidity; // %
|
||||||
|
float p_hpa = bme.pressure / 100.0f; // Pa -> hPa
|
||||||
|
uint32_t gas = (uint32_t)bme.gas_resistance;
|
||||||
|
|
||||||
|
env.temp_x100 = (int16_t)(t * 100.0f);
|
||||||
|
env.rh_x100 = (uint16_t)(h * 100.0f);
|
||||||
|
env.press_x10 = (uint16_t)(p_hpa * 10.0f);
|
||||||
|
env.iaq = computeIAQFromGas(gas, h);
|
||||||
|
|
||||||
|
update_ok = true;
|
||||||
|
} else {
|
||||||
|
env.error_count++;
|
||||||
|
used_fallback = true;
|
||||||
|
update_ok = false;
|
||||||
|
|
||||||
|
// keep last good values (or defaults if never had good)
|
||||||
|
// env.* remain unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update_ok) st |= ST_OK;
|
||||||
|
if (used_fallback) st |= ST_FALLBACK;
|
||||||
|
|
||||||
|
env.status = st;
|
||||||
|
|
||||||
|
publishModbus();
|
||||||
|
|
||||||
|
// debug print (kept short)
|
||||||
|
Serial.printf("HR0=%u HR2=%u T=%.2f RH=%.2f P=%.1f IAQ=%u ST=0x%04X Err=%u BLE=%c\n",
|
||||||
|
env.counter,
|
||||||
|
env.mode,
|
||||||
|
env.temp_x100 / 100.0f,
|
||||||
|
env.rh_x100 / 100.0f,
|
||||||
|
env.press_x10 / 10.0f,
|
||||||
|
env.iaq,
|
||||||
|
env.status,
|
||||||
|
env.error_count,
|
||||||
|
ble_connected ? 'Y' : 'N'
|
||||||
|
);
|
||||||
|
}
|
||||||
392
_Sort/ModbusRTU_ENV/ModbusRTU_ENV.ino
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
/*
|
||||||
|
ESP32-C6 Umbrella Env Sensor (FLL)
|
||||||
|
- BME680 (LIVE) OR BLE-controlled SIM mode
|
||||||
|
- Modbus RTU (Holding Registers) clean map HR0..HR7
|
||||||
|
- BLE GATT control (NimBLE) with compact binary packets
|
||||||
|
|
||||||
|
Modbus HR map (Holding Registers):
|
||||||
|
HR0 counter
|
||||||
|
HR1 status (bitfield)
|
||||||
|
HR2 mode (0=LIVE, 1=SIM)
|
||||||
|
HR3 error_count
|
||||||
|
HR4 temp_x100 (int16 stored in 16-bit register)
|
||||||
|
HR5 rh_x100 (uint16)
|
||||||
|
HR6 press_x10 (uint16)
|
||||||
|
HR7 iaq (uint16, 0..500, higher=worse, AQI-style)
|
||||||
|
|
||||||
|
BLE control packets (little-endian):
|
||||||
|
CMD 0x01 (set mode): [0]=0x01, [1]=mode(0/1)
|
||||||
|
CMD 0x02 (set sim): [0]=0x02,
|
||||||
|
[1..2]=temp_x100 (int16 LE),
|
||||||
|
[3..4]=rh_x100 (uint16 LE),
|
||||||
|
[5..6]=press_x10 (uint16 LE),
|
||||||
|
[7..8]=iaq (uint16 LE) OPTIONAL
|
||||||
|
If iaq not provided, device computes a simple heuristic IAQ for SIM.
|
||||||
|
|
||||||
|
Libraries:
|
||||||
|
- Adafruit BME680 library
|
||||||
|
- modbus-esp8266 (ModbusRTU.h)
|
||||||
|
- NimBLE-Arduino
|
||||||
|
|
||||||
|
Pins (match your working setup):
|
||||||
|
I2C SDA=22, SCL=21
|
||||||
|
RS-485 UART: RX=4, TX=5 @ 9600
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <Adafruit_Sensor.h>
|
||||||
|
#include <Adafruit_BME680.h>
|
||||||
|
|
||||||
|
#include <ModbusRTU.h> // modbus-esp8266 library
|
||||||
|
#include <NimBLEDevice.h> // NimBLE-Arduino
|
||||||
|
|
||||||
|
// -------------------- I2C --------------------
|
||||||
|
static const int I2C_SDA = 22;
|
||||||
|
static const int I2C_SCL = 21;
|
||||||
|
static const uint32_t I2C_FREQ = 100000;
|
||||||
|
static const uint8_t BME_ADDR_1 = 0x76;
|
||||||
|
static const uint8_t BME_ADDR_2 = 0x77;
|
||||||
|
|
||||||
|
// -------------------- MODBUS UART -----------
|
||||||
|
static const int MB_RX = 4;
|
||||||
|
static const int MB_TX = 5;
|
||||||
|
static const uint32_t MB_BAUD = 9600;
|
||||||
|
static const uint8_t SLAVE_ID = 10;
|
||||||
|
|
||||||
|
// -------------------- Modbus registers -------
|
||||||
|
enum {
|
||||||
|
HR_COUNTER = 0,
|
||||||
|
HR_STATUS = 1,
|
||||||
|
HR_MODE = 2,
|
||||||
|
HR_ERROR_COUNT = 3,
|
||||||
|
HR_TEMP_X100 = 4,
|
||||||
|
HR_RH_X100 = 5,
|
||||||
|
HR_PRESS_X10 = 6,
|
||||||
|
HR_IAQ = 7,
|
||||||
|
HR_REG_COUNT = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
// status bits (HR1)
|
||||||
|
static const uint16_t ST_OK = (1 << 0);
|
||||||
|
static const uint16_t ST_FALLBACK = (1 << 1);
|
||||||
|
static const uint16_t ST_NEW_SAMPLE = (1 << 2);
|
||||||
|
static const uint16_t ST_SIM_ACTIVE = (1 << 3);
|
||||||
|
static const uint16_t ST_BLE_CONNECTED = (1 << 4);
|
||||||
|
|
||||||
|
// mode (HR2)
|
||||||
|
static const uint16_t MODE_LIVE = 0;
|
||||||
|
static const uint16_t MODE_SIM = 1;
|
||||||
|
|
||||||
|
// -------------------- Data model -------------
|
||||||
|
struct EnvData {
|
||||||
|
uint16_t counter;
|
||||||
|
uint16_t status;
|
||||||
|
uint16_t mode;
|
||||||
|
uint16_t error_count;
|
||||||
|
int16_t temp_x100;
|
||||||
|
uint16_t rh_x100;
|
||||||
|
uint16_t press_x10;
|
||||||
|
uint16_t iaq;
|
||||||
|
};
|
||||||
|
|
||||||
|
EnvData env; // published
|
||||||
|
EnvData sim; // simulated target values
|
||||||
|
|
||||||
|
// -------------------- BME680 -----------------
|
||||||
|
Adafruit_BME680 bme;
|
||||||
|
bool bme_ok = false;
|
||||||
|
|
||||||
|
// -------------------- Modbus -----------------
|
||||||
|
ModbusRTU mb;
|
||||||
|
|
||||||
|
// -------------------- Timing -----------------
|
||||||
|
static const uint32_t SAMPLE_PERIOD_MS = 1000;
|
||||||
|
uint32_t last_sample_ms = 0;
|
||||||
|
bool heartbeat = false;
|
||||||
|
|
||||||
|
// -------------------- IAQ-ish baseline --------
|
||||||
|
// NOTE: This is NOT official AQI (PM2.5 etc.). It's an AQI-style 0..500 indicator
|
||||||
|
// derived from BME680 gas resistance relative to a slowly moving baseline.
|
||||||
|
float gas_baseline = 0.0f;
|
||||||
|
bool baseline_ready = false;
|
||||||
|
|
||||||
|
static const float BASELINE_ALPHA = 0.005f; // slower drift baseline (0.5% new per sample)
|
||||||
|
static const float HUM_TARGET = 40.0f; // %RH target
|
||||||
|
static const float HUM_PENALTY_GAIN = 0.5f; // penalty per %RH away from target (added directly)
|
||||||
|
|
||||||
|
// -------------------- BLE UUIDs --------------
|
||||||
|
// Keep stable once deployed.
|
||||||
|
static const char* BLE_DEV_NAME = "FLL-Umbrella-Env";
|
||||||
|
static const char* UUID_SVC_CTRL = "12345678-1234-5678-1234-56789abcdef0";
|
||||||
|
static const char* UUID_CHR_CTRL = "12345678-1234-5678-1234-56789abcdef1";
|
||||||
|
|
||||||
|
volatile bool ble_connected = false;
|
||||||
|
|
||||||
|
// ---------- Helpers ----------
|
||||||
|
static inline uint16_t clamp_u16(int32_t v, uint16_t lo, uint16_t hi) {
|
||||||
|
if (v < (int32_t)lo) return lo;
|
||||||
|
if (v > (int32_t)hi) return hi;
|
||||||
|
return (uint16_t)v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AQI-style 0..500 (higher=worse) based on gas relative to baseline + humidity penalty
|
||||||
|
uint16_t computeIAQFromGas(uint32_t gas_ohms, float humidity) {
|
||||||
|
if (gas_ohms < 100) gas_ohms = 100; // avoid divide weirdness
|
||||||
|
|
||||||
|
if (!baseline_ready) {
|
||||||
|
gas_baseline = (float)gas_ohms;
|
||||||
|
baseline_ready = true;
|
||||||
|
} else {
|
||||||
|
gas_baseline = gas_baseline * (1.0f - BASELINE_ALPHA) + (float)gas_ohms * BASELINE_ALPHA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// worse air tends to LOWER gas resistance => baseline/gas increases
|
||||||
|
float ratio = (gas_baseline > 1.0f) ? (gas_baseline / (float)gas_ohms) : 1.0f;
|
||||||
|
|
||||||
|
// map ratio to 0..500-ish (demo-friendly)
|
||||||
|
float score = (ratio - 1.0f) * 120.0f;
|
||||||
|
|
||||||
|
// humidity compensation (far from ~40% adds penalty)
|
||||||
|
score += fabsf(humidity - HUM_TARGET) * HUM_PENALTY_GAIN;
|
||||||
|
|
||||||
|
if (score < 0) score = 0;
|
||||||
|
if (score > 500) score = 500;
|
||||||
|
return (uint16_t)(score + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If SIM IAQ not provided, compute a heuristic so it still "moves"
|
||||||
|
uint16_t computeIAQHeuristicSIM(int16_t temp_x100, uint16_t rh_x100, uint16_t press_x10) {
|
||||||
|
int32_t score = 25; // start good-ish
|
||||||
|
|
||||||
|
float t = temp_x100 / 100.0f;
|
||||||
|
float rh = rh_x100 / 100.0f;
|
||||||
|
float p = press_x10 / 10.0f;
|
||||||
|
|
||||||
|
if (t > 30) score += (int32_t)((t - 30) * 12);
|
||||||
|
if (t < 15) score += (int32_t)((15 - t) * 8);
|
||||||
|
|
||||||
|
score += (int32_t)(fabsf(rh - 45.0f) * 2.0f);
|
||||||
|
|
||||||
|
if (p < 990) score += (int32_t)((990 - p) * 1.5f);
|
||||||
|
|
||||||
|
return clamp_u16(score, 0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
void publishModbus() {
|
||||||
|
mb.Hreg(HR_COUNTER, env.counter);
|
||||||
|
mb.Hreg(HR_STATUS, env.status);
|
||||||
|
mb.Hreg(HR_MODE, env.mode);
|
||||||
|
mb.Hreg(HR_ERROR_COUNT, env.error_count);
|
||||||
|
mb.Hreg(HR_TEMP_X100, (uint16_t)env.temp_x100); // int16 stored as uint16
|
||||||
|
mb.Hreg(HR_RH_X100, env.rh_x100);
|
||||||
|
mb.Hreg(HR_PRESS_X10, env.press_x10);
|
||||||
|
mb.Hreg(HR_IAQ, env.iaq);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_bme() {
|
||||||
|
Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ);
|
||||||
|
|
||||||
|
if (bme.begin(BME_ADDR_1)) bme_ok = true;
|
||||||
|
else if (bme.begin(BME_ADDR_2)) bme_ok = true;
|
||||||
|
else bme_ok = false;
|
||||||
|
|
||||||
|
if (!bme_ok) return;
|
||||||
|
|
||||||
|
bme.setTemperatureOversampling(BME680_OS_8X);
|
||||||
|
bme.setHumidityOversampling(BME680_OS_2X);
|
||||||
|
bme.setPressureOversampling(BME680_OS_4X);
|
||||||
|
bme.setGasHeater(320, 150); // needed for gas resistance
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_modbus() {
|
||||||
|
// ESP32-C6: Serial0 is USB serial; Serial1 for RS-485 UART
|
||||||
|
Serial1.begin(MB_BAUD, SERIAL_8N1, MB_RX, MB_TX);
|
||||||
|
|
||||||
|
mb.begin(&Serial1);
|
||||||
|
mb.slave(SLAVE_ID);
|
||||||
|
|
||||||
|
for (uint16_t r = 0; r < HR_REG_COUNT; r++) {
|
||||||
|
mb.addHreg(r, 0);
|
||||||
|
}
|
||||||
|
publishModbus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- BLE Server (newer NimBLE-Arduino signatures) ----------
|
||||||
|
class CtrlCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
|
void onWrite(NimBLECharacteristic* pChr, NimBLEConnInfo& connInfo) override {
|
||||||
|
(void)connInfo;
|
||||||
|
|
||||||
|
std::string v = pChr->getValue();
|
||||||
|
if (v.size() < 2) return;
|
||||||
|
|
||||||
|
const uint8_t* b = (const uint8_t*)v.data();
|
||||||
|
uint8_t cmd = b[0];
|
||||||
|
|
||||||
|
if (cmd == 0x01) { // set mode
|
||||||
|
env.mode = b[1] ? MODE_SIM : MODE_LIVE;
|
||||||
|
|
||||||
|
} else if (cmd == 0x02) { // set sim values
|
||||||
|
if (v.size() < 7) return;
|
||||||
|
|
||||||
|
auto u16le = [&](int idx) -> uint16_t {
|
||||||
|
return (uint16_t)b[idx] | ((uint16_t)b[idx + 1] << 8);
|
||||||
|
};
|
||||||
|
auto i16le = [&](int idx) -> int16_t {
|
||||||
|
return (int16_t)u16le(idx);
|
||||||
|
};
|
||||||
|
|
||||||
|
sim.temp_x100 = i16le(1);
|
||||||
|
sim.rh_x100 = u16le(3);
|
||||||
|
sim.press_x10 = u16le(5);
|
||||||
|
|
||||||
|
if (v.size() >= 9) sim.iaq = u16le(7);
|
||||||
|
else sim.iaq = computeIAQHeuristicSIM(sim.temp_x100, sim.rh_x100, sim.press_x10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||||
|
void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override {
|
||||||
|
(void)pServer;
|
||||||
|
(void)connInfo;
|
||||||
|
ble_connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override {
|
||||||
|
(void)pServer;
|
||||||
|
(void)connInfo;
|
||||||
|
(void)reason;
|
||||||
|
ble_connected = false;
|
||||||
|
NimBLEDevice::startAdvertising();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setup_ble() {
|
||||||
|
Serial.println("[BLE] init...");
|
||||||
|
|
||||||
|
NimBLEDevice::init(BLE_DEV_NAME);
|
||||||
|
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||||
|
|
||||||
|
NimBLEServer* server = NimBLEDevice::createServer();
|
||||||
|
server->setCallbacks(new ServerCallbacks());
|
||||||
|
|
||||||
|
NimBLEService* svc = server->createService(UUID_SVC_CTRL);
|
||||||
|
|
||||||
|
NimBLECharacteristic* chr = svc->createCharacteristic(
|
||||||
|
UUID_CHR_CTRL,
|
||||||
|
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
|
||||||
|
);
|
||||||
|
chr->setCallbacks(new CtrlCallbacks());
|
||||||
|
|
||||||
|
// (Optional) not required for writes; harmless if you omit it:
|
||||||
|
// chr->createDescriptor("2902");
|
||||||
|
|
||||||
|
svc->start();
|
||||||
|
|
||||||
|
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||||
|
adv->addServiceUUID(UUID_SVC_CTRL);
|
||||||
|
adv->start();
|
||||||
|
|
||||||
|
Serial.println("[BLE] Advertising started");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Main ----------
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
Serial.println("\n=== ESP32-C6 BME680 + BLE SIM -> Modbus RTU (HR0..HR7) ===");
|
||||||
|
Serial.printf("I2C SDA/SCL: %d/%d\n", I2C_SDA, I2C_SCL);
|
||||||
|
Serial.printf("Modbus RX/TX: %d/%d @ %lu\n", MB_RX, MB_TX, (unsigned long)MB_BAUD);
|
||||||
|
Serial.printf("Slave ID: %u\n", SLAVE_ID);
|
||||||
|
|
||||||
|
// init defaults
|
||||||
|
env.counter = 0;
|
||||||
|
env.status = 0;
|
||||||
|
env.mode = MODE_LIVE;
|
||||||
|
env.error_count = 0;
|
||||||
|
env.temp_x100 = 2500; // 25.00 C
|
||||||
|
env.rh_x100 = 5000; // 50.00 %
|
||||||
|
env.press_x10 = 10132; // 1013.2 hPa
|
||||||
|
env.iaq = 25; // "good"
|
||||||
|
|
||||||
|
// default sim values = same as env
|
||||||
|
sim = env;
|
||||||
|
|
||||||
|
setup_bme();
|
||||||
|
Serial.printf("BME680 OK: %s\n", bme_ok ? "YES" : "NO");
|
||||||
|
|
||||||
|
setup_modbus();
|
||||||
|
setup_ble();
|
||||||
|
|
||||||
|
Serial.println("Modbus ready (FC03 HR0..HR7). BLE control ready.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
mb.task();
|
||||||
|
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now - last_sample_ms < SAMPLE_PERIOD_MS) return;
|
||||||
|
last_sample_ms = now;
|
||||||
|
|
||||||
|
heartbeat = !heartbeat;
|
||||||
|
env.counter++;
|
||||||
|
|
||||||
|
// rebuild status fresh each tick
|
||||||
|
uint16_t st = 0;
|
||||||
|
if (heartbeat) st |= ST_NEW_SAMPLE;
|
||||||
|
if (ble_connected) st |= ST_BLE_CONNECTED;
|
||||||
|
if (env.mode == MODE_SIM) st |= ST_SIM_ACTIVE;
|
||||||
|
|
||||||
|
bool used_fallback = false;
|
||||||
|
bool update_ok = false;
|
||||||
|
|
||||||
|
if (env.mode == MODE_SIM) {
|
||||||
|
// publish simulated values
|
||||||
|
env.temp_x100 = sim.temp_x100;
|
||||||
|
env.rh_x100 = sim.rh_x100;
|
||||||
|
env.press_x10 = sim.press_x10;
|
||||||
|
env.iaq = sim.iaq;
|
||||||
|
update_ok = true;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// LIVE mode
|
||||||
|
if (bme_ok && bme.performReading()) {
|
||||||
|
float t = bme.temperature; // C
|
||||||
|
float h = bme.humidity; // %
|
||||||
|
float p_hpa = bme.pressure / 100.0f; // Pa -> hPa
|
||||||
|
uint32_t gas = (uint32_t)bme.gas_resistance;
|
||||||
|
|
||||||
|
env.temp_x100 = (int16_t)(t * 100.0f);
|
||||||
|
env.rh_x100 = (uint16_t)(h * 100.0f);
|
||||||
|
env.press_x10 = (uint16_t)(p_hpa * 10.0f);
|
||||||
|
env.iaq = computeIAQFromGas(gas, h);
|
||||||
|
|
||||||
|
update_ok = true;
|
||||||
|
} else {
|
||||||
|
env.error_count++;
|
||||||
|
used_fallback = true;
|
||||||
|
update_ok = false;
|
||||||
|
// keep last good env values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update_ok) st |= ST_OK;
|
||||||
|
if (used_fallback) st |= ST_FALLBACK;
|
||||||
|
|
||||||
|
env.status = st;
|
||||||
|
publishModbus();
|
||||||
|
|
||||||
|
// debug
|
||||||
|
Serial.printf("HR0=%u HR2=%u T=%.2f RH=%.2f P=%.1f IAQ=%u ST=0x%04X Err=%u BLE=%c\n",
|
||||||
|
env.counter,
|
||||||
|
env.mode,
|
||||||
|
env.temp_x100 / 100.0f,
|
||||||
|
env.rh_x100 / 100.0f,
|
||||||
|
env.press_x10 / 10.0f,
|
||||||
|
env.iaq,
|
||||||
|
env.status,
|
||||||
|
env.error_count,
|
||||||
|
ble_connected ? 'Y' : 'N'
|
||||||
|
);
|
||||||
|
}
|
||||||
9
_Sort/TTS/main.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from gtts import gTTS
|
||||||
|
|
||||||
|
# English
|
||||||
|
#tts_en = gTTS("Warning! Abnormal Vibration", lang="en")
|
||||||
|
#tts_en.save("VibWarning_en.mp3")
|
||||||
|
|
||||||
|
# Spanish
|
||||||
|
tts_es = gTTS("Atención: la calidad del aire ha vuelto a la normalidad.", lang="es")
|
||||||
|
tts_es.save("106.mp3")
|
||||||
180
_Sort/TTS/mp_DFplayerExample.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
from machine import UART, Pin
|
||||||
|
import time
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# CONFIG (change to your pins)
|
||||||
|
# ----------------------------
|
||||||
|
UART_ID = 1 # ESP32-C6 typically has UART(0) + UART(1)
|
||||||
|
TX_PIN = 20 # ESP32 TX -> DFPlayer RX
|
||||||
|
RX_PIN = 21 # ESP32 RX <- DFPlayer TX
|
||||||
|
BUSY_PIN = 22 # Optional: DFPlayer BUSY pin -> ESP32 GPIO (set int like 4), or None
|
||||||
|
|
||||||
|
BAUD = 9600
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# DFPlayer protocol helpers
|
||||||
|
# ----------------------------
|
||||||
|
_START = 0x7E
|
||||||
|
_VER = 0xFF
|
||||||
|
_LEN = 0x06
|
||||||
|
_ACK = 0x00 # 0x01 = request ACK (some clones get flaky). Keep 0x00 for test jig.
|
||||||
|
_END = 0xEF
|
||||||
|
|
||||||
|
# Commands (common)
|
||||||
|
CMD_SET_VOLUME = 0x06
|
||||||
|
CMD_PLAY_TRACK = 0x03 # Play track number (0001..2999) from root (depends on SD structure)
|
||||||
|
CMD_PLAY_FOLDER = 0x0F # Play folder/file (01..99 folder, 001..255 file)
|
||||||
|
CMD_STOP = 0x16
|
||||||
|
CMD_RESET = 0x0C
|
||||||
|
CMD_SET_EQ = 0x07
|
||||||
|
CMD_SET_SOURCE = 0x09 # (rarely needed)
|
||||||
|
CMD_QUERY_STATUS = 0x42 # not always supported consistently
|
||||||
|
|
||||||
|
def _checksum(cmd, p1, p2):
|
||||||
|
# checksum = 0 - (ver + len + cmd + ack + p1 + p2)
|
||||||
|
s = _VER + _LEN + cmd + _ACK + p1 + p2
|
||||||
|
c = (0 - s) & 0xFFFF
|
||||||
|
return (c >> 8) & 0xFF, c & 0xFF
|
||||||
|
|
||||||
|
class DFPlayer:
|
||||||
|
def __init__(self, uart: UART, busy_pin=None):
|
||||||
|
self.uart = uart
|
||||||
|
self.busy = None
|
||||||
|
if busy_pin is not None:
|
||||||
|
self.busy = Pin(busy_pin, Pin.IN, Pin.PULL_UP) # BUSY is often LOW when playing (depends on module)
|
||||||
|
# Small settle time after power-up
|
||||||
|
time.sleep_ms(800)
|
||||||
|
|
||||||
|
def _send(self, cmd, param=0):
|
||||||
|
p1 = (param >> 8) & 0xFF
|
||||||
|
p2 = param & 0xFF
|
||||||
|
ch, cl = _checksum(cmd, p1, p2)
|
||||||
|
frame = bytes([_START, _VER, _LEN, cmd, _ACK, p1, p2, ch, cl, _END])
|
||||||
|
self.uart.write(frame)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._send(CMD_RESET, 0)
|
||||||
|
time.sleep_ms(1200) # DFPlayer needs a moment after reset
|
||||||
|
|
||||||
|
def volume(self, level: int):
|
||||||
|
# 0..30
|
||||||
|
level = max(0, min(30, level))
|
||||||
|
self._send(CMD_SET_VOLUME, level)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._send(CMD_STOP, 0)
|
||||||
|
|
||||||
|
def play_track(self, track_num: int):
|
||||||
|
# Track numbers: 1..2999 typically
|
||||||
|
self._send(CMD_PLAY_TRACK, track_num)
|
||||||
|
|
||||||
|
def play_folder_file(self, folder: int, file: int):
|
||||||
|
# folder: 1..99, file: 1..255
|
||||||
|
folder = max(1, min(99, folder))
|
||||||
|
file = max(1, min(255, file))
|
||||||
|
param = (folder << 8) | file
|
||||||
|
self._send(CMD_PLAY_FOLDER, param)
|
||||||
|
|
||||||
|
def is_playing(self):
|
||||||
|
if self.busy is None:
|
||||||
|
return None
|
||||||
|
# Many modules: BUSY = 0 while playing, 1 when idle
|
||||||
|
return self.busy.value() == 0
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# UART + Test Jig
|
||||||
|
# ----------------------------
|
||||||
|
uart = UART(
|
||||||
|
UART_ID,
|
||||||
|
baudrate=BAUD,
|
||||||
|
bits=8,
|
||||||
|
parity=None,
|
||||||
|
stop=1,
|
||||||
|
tx=Pin(TX_PIN),
|
||||||
|
rx=Pin(RX_PIN),
|
||||||
|
timeout=100
|
||||||
|
)
|
||||||
|
|
||||||
|
player = DFPlayer(uart, busy_pin=BUSY_PIN)
|
||||||
|
|
||||||
|
def demo():
|
||||||
|
print("DFPlayer test jig starting...")
|
||||||
|
print("UART:", UART_ID, "TX:", TX_PIN, "RX:", RX_PIN, "BUSY:", BUSY_PIN)
|
||||||
|
|
||||||
|
# Optional: Reset (sometimes helpful on clones; comment out if it causes issues)
|
||||||
|
# player.reset()
|
||||||
|
|
||||||
|
player.volume(20)
|
||||||
|
time.sleep_ms(200)
|
||||||
|
|
||||||
|
|
||||||
|
# English
|
||||||
|
# Temp
|
||||||
|
player.play_folder_file(1, 101)
|
||||||
|
#time.sleep(2)
|
||||||
|
while busy.value() == 1:
|
||||||
|
print("Gate1",busy.value())
|
||||||
|
# now wait until playback finishes
|
||||||
|
while busy.value() == 0:
|
||||||
|
print("Gate2",busy.value())
|
||||||
|
# 30
|
||||||
|
player.play_folder_file(1, 30)
|
||||||
|
#time.sleep(1)
|
||||||
|
while busy.value() == 1:
|
||||||
|
print("Gate1",busy.value())
|
||||||
|
# now wait until playback finishes
|
||||||
|
while busy.value() == 0:
|
||||||
|
print("Gate2",busy.value())
|
||||||
|
# 7
|
||||||
|
player.play_folder_file(1, 7)
|
||||||
|
#time.sleep(1.5)
|
||||||
|
while busy.value() == 1:
|
||||||
|
print("Gate1",busy.value())
|
||||||
|
# now wait until playback finishes
|
||||||
|
while busy.value() == 0:
|
||||||
|
print("Gate2",busy.value())
|
||||||
|
# Vibration Warning
|
||||||
|
player.play_folder_file(1, 102)
|
||||||
|
#time.sleep(3)
|
||||||
|
while busy.value() == 1:
|
||||||
|
print("Gate1",busy.value())
|
||||||
|
# now wait until playback finishes
|
||||||
|
while busy.value() == 0:
|
||||||
|
print("Gate2",busy.value())
|
||||||
|
time.sleep(0.5)
|
||||||
|
#Spanish
|
||||||
|
# Temp
|
||||||
|
player.play_folder_file(2, 101)
|
||||||
|
#time.sleep(2)
|
||||||
|
while busy.value() == 1:
|
||||||
|
print("Gate1",busy.value())
|
||||||
|
# now wait until playback finishes
|
||||||
|
while busy.value() == 0:
|
||||||
|
print("Gate2",busy.value())
|
||||||
|
# 30
|
||||||
|
player.play_folder_file(2, 30)
|
||||||
|
#time.sleep(1)
|
||||||
|
while busy.value() == 1:
|
||||||
|
print("Gate1",busy.value())
|
||||||
|
# now wait until playback finishes
|
||||||
|
while busy.value() == 0:
|
||||||
|
print("Gate2",busy.value())
|
||||||
|
# 7
|
||||||
|
player.play_folder_file(2, 7)
|
||||||
|
time.sleep(1.5)
|
||||||
|
# Vibration Warning
|
||||||
|
player.play_folder_file(2, 102)
|
||||||
|
while busy.value() == 1:
|
||||||
|
print("Gate1",busy.value())
|
||||||
|
# now wait until playback finishes
|
||||||
|
while busy.value() == 0:
|
||||||
|
print("Gate2",busy.value())
|
||||||
|
|
||||||
|
|
||||||
|
print("Stop.")
|
||||||
|
player.stop()
|
||||||
|
|
||||||
|
|
||||||
|
busy = Pin(BUSY_PIN, Pin.IN, Pin.PULL_UP)
|
||||||
|
demo()
|
||||||
|
quit()
|
||||||
0
_Sort/TTS/mp_ModbusMaster.py
Normal file
623
_Sort/WebSite/Phoenix Guardian.html
Normal file
|
|
@ -0,0 +1,623 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- saved from url=(0060)https://mendozarodriguezs124-debug.github.io/guardian-f-nix/ -->
|
||||||
|
<html lang="en" class="translated-ltr"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>Phoenix Guardian</title>
|
||||||
|
|
||||||
|
<script src="./Phoenix Guardian_files/chart.js.download"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
body{
|
||||||
|
|
||||||
|
font-family:Arial,Helvetica,sans-serif;
|
||||||
|
|
||||||
|
margin:0;
|
||||||
|
|
||||||
|
background:#fdf2f2;
|
||||||
|
|
||||||
|
color:#333;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
header{
|
||||||
|
|
||||||
|
background:linear-gradient(90deg,#8b0000,#c62828);
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
padding:15px;
|
||||||
|
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav{
|
||||||
|
|
||||||
|
display:flex;
|
||||||
|
|
||||||
|
background:#8b0000;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav button{
|
||||||
|
|
||||||
|
flex:1;
|
||||||
|
|
||||||
|
padding:12px;
|
||||||
|
|
||||||
|
border:none;
|
||||||
|
|
||||||
|
background:#8b0000;
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
font-weight:bold;
|
||||||
|
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav button.active{
|
||||||
|
|
||||||
|
background:#c62828;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.section{
|
||||||
|
|
||||||
|
display:none;
|
||||||
|
|
||||||
|
padding:10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.section.active{
|
||||||
|
|
||||||
|
display:block;
|
||||||
|
|
||||||
|
}
|
||||||
|
.card{
|
||||||
|
background:#fff;
|
||||||
|
padding:22px 20px;
|
||||||
|
margin:16px 12px;
|
||||||
|
border-radius:16px;
|
||||||
|
box-shadow:0 4px 10px rgba(0,0,0,.15);
|
||||||
|
line-height:1.6;
|
||||||
|
|
||||||
|
}
|
||||||
|
.logo{
|
||||||
|
width:70%;
|
||||||
|
max-width:280px;
|
||||||
|
display:block;
|
||||||
|
margin:16px auto;
|
||||||
|
}
|
||||||
|
h2{color:#8b0000}
|
||||||
|
|
||||||
|
.data{margin:6px 0}
|
||||||
|
|
||||||
|
button.action{
|
||||||
|
|
||||||
|
background:#c62828;
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
border:none;
|
||||||
|
|
||||||
|
padding:10px 15px;
|
||||||
|
|
||||||
|
border-radius:5px;
|
||||||
|
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
margin-top:8px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
button.action:hover{background:#8b0000}
|
||||||
|
|
||||||
|
.alerta{
|
||||||
|
|
||||||
|
display:none;
|
||||||
|
|
||||||
|
background:#fdeaea;
|
||||||
|
|
||||||
|
border-left:6px solid #c62828;
|
||||||
|
|
||||||
|
padding:15px;
|
||||||
|
|
||||||
|
margin-top:12px;
|
||||||
|
|
||||||
|
font-weight:bold;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table{
|
||||||
|
|
||||||
|
width:100%;
|
||||||
|
|
||||||
|
border-collapse:collapse;
|
||||||
|
|
||||||
|
margin-top:10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
th,td{
|
||||||
|
|
||||||
|
border:1px solid #ccc;
|
||||||
|
|
||||||
|
padding:6px;
|
||||||
|
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
th{
|
||||||
|
|
||||||
|
background:#c62828;
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<link type="text/css" rel="stylesheet" charset="UTF-8" href="./Phoenix Guardian_files/m=el_main_css"></head>
|
||||||
|
|
||||||
|
<body data-new-gr-c-s-check-loaded="14.1270.0" data-gr-ext-installed="">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
|
||||||
|
<h1><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">🛡️ Phoenix Guardian 🔥</font></font></h1><img <img="" src="./Phoenix Guardian_files/freepik_23c11d8d-723a-469e-ae09-1d9d4a639e0a.png" alt="Phoenix Guardian" class="logo">
|
||||||
|
<p><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Environmental and vibration monitoring prototype</font></font></p>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
|
||||||
|
<button class="active" onclick="mostrar('inicio',this)"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Start</font></font></button>
|
||||||
|
|
||||||
|
<button onclick="mostrar('sensores',this)"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Sensors</font></font></button>
|
||||||
|
|
||||||
|
<button onclick="mostrar('graficas',this)"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Graphics</font></font></button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INICIO -->
|
||||||
|
|
||||||
|
<div id="inicio" class="section active">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Project description</font></font></h2>
|
||||||
|
|
||||||
|
<p><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">
|
||||||
|
|
||||||
|
Guardian Phoenix is a monitoring prototype designed for remote sites without electrical infrastructure. The system simulates physical sensors using the sensors of a mobile phone.
|
||||||
|
|
||||||
|
</font></font></p>
|
||||||
|
|
||||||
|
<p><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">
|
||||||
|
|
||||||
|
The device's battery represents the system's backup battery, while the connection to an electrical source simulates an active solar panel.
|
||||||
|
|
||||||
|
</font></font></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Energy state</font></font></h2>
|
||||||
|
|
||||||
|
<p class="data"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">System battery: </font></font><span id="bat"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">100</font></font></span><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;"> %</font></font></p>
|
||||||
|
|
||||||
|
<p class="data"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Power source: </font></font><span id="energia"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Active solar panel</font></font></span></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SENSORES -->
|
||||||
|
|
||||||
|
<div id="sensores" class="section">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Environmental Sensor</font></font></h2>
|
||||||
|
|
||||||
|
<p class="data"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Temperature: </font></font><span id="temp"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">--</font></font></span><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;"> °C</font></font></p>
|
||||||
|
|
||||||
|
<p class="data"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Humidity: </font></font><span id="hum"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">--</font></font></span><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;"> %</font></font></p>
|
||||||
|
|
||||||
|
<p class="data"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Pressure: </font></font><span id="pres"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">--</font></font></span><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;"> hPa</font></font></p>
|
||||||
|
|
||||||
|
<button class="action" onclick="clima()"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Update weather</font></font></button>
|
||||||
|
|
||||||
|
<p id="errorClima" style="color:#b00000;font-size:0.85em;"></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Vibration Sensor</font></font></h2>
|
||||||
|
|
||||||
|
<p class="data"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Vibration: </font></font><span id="vib"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">0.00</font></font></span></p>
|
||||||
|
|
||||||
|
<p class="data"><font dir="auto" style="vertical-align: inherit;"><span id="estado"><font dir="auto" style="vertical-align: inherit;">Steady</font></span><font dir="auto" style="vertical-align: inherit;"> state</font></font><span id="estado"><font dir="auto" style="vertical-align: inherit;"></font></span></p>
|
||||||
|
|
||||||
|
<button class="action" id="btnVib" onclick="toggleVibracion()"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Activate sensor</font></font></button>
|
||||||
|
|
||||||
|
<div class="alerta" id="alerta"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">
|
||||||
|
|
||||||
|
🚨 Dangerous vibration detected</font></font><br><br>
|
||||||
|
|
||||||
|
<button class="action" onclick="alertar()"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Alert nearby users</font></font></button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Records</font></font></h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<th><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Hour</font></font></th>
|
||||||
|
|
||||||
|
<th><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Temp</font></font></th>
|
||||||
|
|
||||||
|
<th><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Hum</font></font></th>
|
||||||
|
|
||||||
|
<th><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">President</font></font></th>
|
||||||
|
|
||||||
|
<th><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Event</font></font></th>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody id="tabla"></tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button class="action" onclick="guardar()"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Save record</font></font></button>
|
||||||
|
|
||||||
|
<button class="action" style="background:#555" onclick="borrarUltimo()"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Delete last</font></font></button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GRAFICAS -->
|
||||||
|
|
||||||
|
<div id="graficas" class="section">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Humidity</font></font></h2>
|
||||||
|
|
||||||
|
<canvas id="gHum"></canvas>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Temperature</font></font></h2>
|
||||||
|
|
||||||
|
<canvas id="gTemp"></canvas>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Pressure</font></font></h2>
|
||||||
|
|
||||||
|
<canvas id="gPres"></canvas>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio id="sonido" loop="">
|
||||||
|
|
||||||
|
<source src="https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg">
|
||||||
|
|
||||||
|
</audio>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const apiKey="729294e1fe04354f62449218961044b8";
|
||||||
|
|
||||||
|
let sensorActivo=false;
|
||||||
|
|
||||||
|
let alertaActiva=false;
|
||||||
|
|
||||||
|
let vibracion=0;
|
||||||
|
|
||||||
|
let charts={};
|
||||||
|
|
||||||
|
// Navegación
|
||||||
|
|
||||||
|
function mostrar(id,btn){
|
||||||
|
|
||||||
|
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
|
||||||
|
|
||||||
|
document.querySelectorAll('.nav button').forEach(b=>b.classList.remove('active'));
|
||||||
|
|
||||||
|
document.getElementById(id).classList.add('active');
|
||||||
|
|
||||||
|
btn.classList.add('active');
|
||||||
|
|
||||||
|
actualizarGraficas();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batería y panel solar
|
||||||
|
|
||||||
|
if(navigator.getBattery){
|
||||||
|
|
||||||
|
navigator.getBattery().then(b=>{
|
||||||
|
|
||||||
|
function actualizar(){
|
||||||
|
|
||||||
|
bat.innerText=Math.round(b.level*100);
|
||||||
|
|
||||||
|
energia.innerText=b.charging?"Panel solar activo":"Batería de respaldo";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
actualizar();
|
||||||
|
|
||||||
|
b.addEventListener("levelchange",actualizar);
|
||||||
|
|
||||||
|
b.addEventListener("chargingchange",actualizar);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clima
|
||||||
|
|
||||||
|
async function clima(){
|
||||||
|
|
||||||
|
errorClima.innerText="";
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
const r=await fetch(`https://api.openweathermap.org/data/2.5/weather?lat=25.6866&lon=-100.3161&appid=${apiKey}&units=metric`);
|
||||||
|
|
||||||
|
const d=await r.json();
|
||||||
|
|
||||||
|
temp.innerText=Math.round(d.main.temp);
|
||||||
|
|
||||||
|
hum.innerText=d.main.humidity;
|
||||||
|
|
||||||
|
pres.innerText=d.main.pressure;
|
||||||
|
|
||||||
|
}catch{
|
||||||
|
|
||||||
|
errorClima.innerText="No se pudo acceder al clima";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vibración
|
||||||
|
|
||||||
|
function toggleVibracion(){
|
||||||
|
|
||||||
|
if(!sensorActivo){
|
||||||
|
|
||||||
|
window.addEventListener("devicemotion",leerVibracion);
|
||||||
|
|
||||||
|
btnVib.innerText="Desactivar sensor";
|
||||||
|
|
||||||
|
}else{
|
||||||
|
|
||||||
|
window.removeEventListener("devicemotion",leerVibracion);
|
||||||
|
|
||||||
|
btnVib.innerText="Activar sensor";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sensorActivo=!sensorActivo;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function leerVibracion(e){
|
||||||
|
|
||||||
|
let x=e.accelerationIncludingGravity?.x||0;
|
||||||
|
|
||||||
|
let y=e.accelerationIncludingGravity?.y||0;
|
||||||
|
|
||||||
|
let z=e.accelerationIncludingGravity?.z||0;
|
||||||
|
|
||||||
|
vibracion=Math.sqrt(x*x+y*y+z*z).toFixed(2);
|
||||||
|
|
||||||
|
vib.innerText=vibracion;
|
||||||
|
|
||||||
|
if(vibracion>12 && !alertaActiva){
|
||||||
|
|
||||||
|
alertaActiva=true;
|
||||||
|
|
||||||
|
estado.innerText="Inestable";
|
||||||
|
|
||||||
|
alerta.style.display="block";
|
||||||
|
|
||||||
|
sonido.play();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function alertar(){
|
||||||
|
|
||||||
|
alerta.style.display="none";
|
||||||
|
|
||||||
|
sonido.pause();
|
||||||
|
|
||||||
|
sonido.currentTime=0;
|
||||||
|
|
||||||
|
estado.innerText="Alerta atendida";
|
||||||
|
|
||||||
|
alertaActiva=false;
|
||||||
|
|
||||||
|
alert("Usuarios cercanos alertados");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registros
|
||||||
|
|
||||||
|
function guardar(){
|
||||||
|
|
||||||
|
const r={
|
||||||
|
|
||||||
|
h:new Date().toLocaleTimeString(),
|
||||||
|
|
||||||
|
t:temp.innerText,
|
||||||
|
|
||||||
|
hu:hum.innerText,
|
||||||
|
|
||||||
|
p:pres.innerText,
|
||||||
|
|
||||||
|
e:estado.innerText
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
let d=JSON.parse(localStorage.getItem("reg"))||[];
|
||||||
|
|
||||||
|
d.push(r);
|
||||||
|
|
||||||
|
localStorage.setItem("reg",JSON.stringify(d));
|
||||||
|
|
||||||
|
cargarTabla();
|
||||||
|
|
||||||
|
actualizarGraficas();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function cargarTabla(){
|
||||||
|
|
||||||
|
tabla.innerHTML="";
|
||||||
|
|
||||||
|
(JSON.parse(localStorage.getItem("reg"))||[]).forEach(r=>{
|
||||||
|
|
||||||
|
tabla.innerHTML+=`
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td>${r.h}</td>
|
||||||
|
|
||||||
|
<td>${r.t}</td>
|
||||||
|
|
||||||
|
<td>${r.hu}</td>
|
||||||
|
|
||||||
|
<td>${r.p}</td>
|
||||||
|
|
||||||
|
<td>${r.e}</td>
|
||||||
|
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarTabla();
|
||||||
|
|
||||||
|
function borrarUltimo(){
|
||||||
|
|
||||||
|
let d=JSON.parse(localStorage.getItem("reg"))||[];
|
||||||
|
|
||||||
|
d.pop();
|
||||||
|
|
||||||
|
localStorage.setItem("reg",JSON.stringify(d));
|
||||||
|
|
||||||
|
cargarTabla();
|
||||||
|
|
||||||
|
actualizarGraficas();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gráficas automáticas
|
||||||
|
|
||||||
|
function graf(id,label,data,color){
|
||||||
|
|
||||||
|
if(charts[id]) charts[id].destroy();
|
||||||
|
|
||||||
|
charts[id]=new Chart(document.getElementById(id),{
|
||||||
|
|
||||||
|
type:"line",
|
||||||
|
|
||||||
|
data:{
|
||||||
|
|
||||||
|
labels:data.map((_,i)=>i+1),
|
||||||
|
|
||||||
|
datasets:[{
|
||||||
|
|
||||||
|
label:label,
|
||||||
|
|
||||||
|
data:data,
|
||||||
|
|
||||||
|
borderColor:color,
|
||||||
|
|
||||||
|
backgroundColor:color+"33",
|
||||||
|
|
||||||
|
fill:true,
|
||||||
|
|
||||||
|
tension:0.3
|
||||||
|
|
||||||
|
}]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function actualizarGraficas(){
|
||||||
|
|
||||||
|
const d=JSON.parse(localStorage.getItem("reg"))||[];
|
||||||
|
|
||||||
|
graf("gHum","Humedad (%)",d.map(x=>x.hu),"#1976d2");
|
||||||
|
|
||||||
|
graf("gTemp","Temperatura (°C)",d.map(x=>x.t),"#d32f2f");
|
||||||
|
|
||||||
|
graf("gPres","Presión (hPa)",d.map(x=>x.p),"#388e3c");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script><div id="goog-gt-tt" class="VIpgJd-yAWNEb-L7lbkb skiptranslate" style="border-radius: 12px; margin: 0 0 0 -23px; padding: 0; font-family: 'Google Sans', Arial, sans-serif;" data-id=""><div id="goog-gt-vt" class="VIpgJd-yAWNEb-hvhgNd"><div class="VIpgJd-yAWNEb-hvhgNd-Ud7fr"><img src="./Phoenix Guardian_files/24px.svg" width="24" height="24" alt=""><div class=" VIpgJd-yAWNEb-hvhgNd-IuizWc-i3jM8c " dir="ltr">Original text</div></div><div class="VIpgJd-yAWNEb-hvhgNd-k77Iif"><div id="goog-gt-original-text" class="VIpgJd-yAWNEb-nVMfcd-fmcmS VIpgJd-yAWNEb-hvhgNd-axAV1"></div></div><div class="VIpgJd-yAWNEb-hvhgNd-N7Eqid ltr"><div class="VIpgJd-yAWNEb-hvhgNd-N7Eqid-B7I4Od ltr" dir="ltr"><div class="VIpgJd-yAWNEb-hvhgNd-UTujCb">Rate this translation</div><div class="VIpgJd-yAWNEb-hvhgNd-eO9mKe">Your feedback will be used to help improve Google Translate</div></div><div class="VIpgJd-yAWNEb-hvhgNd-xgov5 ltr"><button id="goog-gt-thumbUpButton" type="button" class="VIpgJd-yAWNEb-hvhgNd-bgm6sf" title="Good translation" aria-label="Good translation" aria-pressed="false"><span id="goog-gt-thumbUpIcon"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M21 7h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 0S7.08 6.85 7 7H2v13h16c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73V9c0-1.1-.9-2-2-2zM7 18H4V9h3v9zm14-7l-3 7H9V8l4.34-4.34L12 9h9v2z"></path></svg></span><span id="goog-gt-thumbUpIconFilled"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M21 7h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 0S7.08 6.85 7 7v13h11c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73V9c0-1.1-.9-2-2-2zM5 7H1v13h4V7z"></path></svg></span></button><button id="goog-gt-thumbDownButton" type="button" class="VIpgJd-yAWNEb-hvhgNd-bgm6sf" title="Poor translation" aria-label="Poor translation" aria-pressed="false"><span id="goog-gt-thumbDownIcon"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M3 17h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 24s7.09-6.85 7.17-7h5V4H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2zM17 6h3v9h-3V6zM3 13l3-7h9v10l-4.34 4.34L12 15H3v-2z"></path></svg></span><span id="goog-gt-thumbDownIconFilled"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M3 17h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 24s7.09-6.85 7.17-7V4H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2zm16 0h4V4h-4v13z"></path></svg></span></button></div></div><div id="goog-gt-votingHiddenPane" class="VIpgJd-yAWNEb-hvhgNd-aXYTce"><form id="goog-gt-votingForm" action="https://translate.googleapis.com/translate_voting?client=te_lib" method="post" target="votingFrame" class="VIpgJd-yAWNEb-hvhgNd-aXYTce"><input type="text" name="sl" id="goog-gt-votingInputSrcLang"><input type="text" name="tl" id="goog-gt-votingInputTrgLang"><input type="text" name="query" id="goog-gt-votingInputSrcText"><input type="text" name="gtrans" id="goog-gt-votingInputTrgText"><input type="text" name="vote" id="goog-gt-votingInputVote"></form><iframe name="votingFrame" frameborder="0" src="./Phoenix Guardian_files/saved_resource.html"></iframe></div></div></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body><grammarly-desktop-integration data-grammarly-shadow-root="true"><template shadowrootmode="open"><style>
|
||||||
|
div.grammarly-desktop-integration {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select:none;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grammarly-desktop-integration:before {
|
||||||
|
content: attr(data-content);
|
||||||
|
}
|
||||||
|
</style><div aria-label="grammarly-integration" role="group" tabindex="-1" class="grammarly-desktop-integration" data-content="{"mode":"full","isActive":true,"isUserDisabled":false}"></div></template></grammarly-desktop-integration></html>
|
||||||
1
_Sort/WebSite/Phoenix Guardian_files/24px.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
14
_Sort/WebSite/Phoenix Guardian_files/chart.js.download
Normal file
|
After Width: | Height: | Size: 146 KiB |
1
_Sort/WebSite/Phoenix Guardian_files/m=el_main_css
Normal file
23
_Sort/WebSite/Phoenix Guardian_files/saved_resource.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
<!-- saved from url=(0011)about:blank -->
|
||||||
|
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head><body data-new-gr-c-s-check-loaded="14.1270.0" data-gr-ext-installed=""></body><grammarly-desktop-integration data-grammarly-shadow-root="true"><template shadowrootmode="open"><style>
|
||||||
|
div.grammarly-desktop-integration {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select:none;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grammarly-desktop-integration:before {
|
||||||
|
content: attr(data-content);
|
||||||
|
}
|
||||||
|
</style><div aria-label="grammarly-integration" role="group" tabindex="-1" class="grammarly-desktop-integration" data-content="{"mode":"full","isActive":true,"isUserDisabled":false}"></div></template></grammarly-desktop-integration></html>
|
||||||
613
_Sort/WebSite/index.html
Normal file
|
|
@ -0,0 +1,613 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="es">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>Guardián Fénix</title>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
body{
|
||||||
|
|
||||||
|
font-family:Arial,Helvetica,sans-serif;
|
||||||
|
|
||||||
|
margin:0;
|
||||||
|
|
||||||
|
background:#fdf2f2;
|
||||||
|
|
||||||
|
color:#333;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
header{
|
||||||
|
|
||||||
|
background:linear-gradient(90deg,#8b0000,#c62828);
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
padding:15px;
|
||||||
|
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav{
|
||||||
|
|
||||||
|
display:flex;
|
||||||
|
|
||||||
|
background:#8b0000;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav button{
|
||||||
|
|
||||||
|
flex:1;
|
||||||
|
|
||||||
|
padding:12px;
|
||||||
|
|
||||||
|
border:none;
|
||||||
|
|
||||||
|
background:#8b0000;
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
font-weight:bold;
|
||||||
|
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav button.active{
|
||||||
|
|
||||||
|
background:#c62828;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.section{
|
||||||
|
|
||||||
|
display:none;
|
||||||
|
|
||||||
|
padding:10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.section.active{
|
||||||
|
|
||||||
|
display:block;
|
||||||
|
|
||||||
|
}
|
||||||
|
.card{
|
||||||
|
background:#fff;
|
||||||
|
padding:22px 20px;
|
||||||
|
margin:16px 12px;
|
||||||
|
border-radius:16px;
|
||||||
|
box-shadow:0 4px 10px rgba(0,0,0,.15);
|
||||||
|
line-height:1.6;
|
||||||
|
|
||||||
|
}
|
||||||
|
.logo{
|
||||||
|
width:70%;
|
||||||
|
max-width:280px;
|
||||||
|
display:block;
|
||||||
|
margin:16px auto;
|
||||||
|
}
|
||||||
|
h2{color:#8b0000}
|
||||||
|
|
||||||
|
.data{margin:6px 0}
|
||||||
|
|
||||||
|
button.action{
|
||||||
|
|
||||||
|
background:#c62828;
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
border:none;
|
||||||
|
|
||||||
|
padding:10px 15px;
|
||||||
|
|
||||||
|
border-radius:5px;
|
||||||
|
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
margin-top:8px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
button.action:hover{background:#8b0000}
|
||||||
|
|
||||||
|
.alerta{
|
||||||
|
|
||||||
|
display:none;
|
||||||
|
|
||||||
|
background:#fdeaea;
|
||||||
|
|
||||||
|
border-left:6px solid #c62828;
|
||||||
|
|
||||||
|
padding:15px;
|
||||||
|
|
||||||
|
margin-top:12px;
|
||||||
|
|
||||||
|
font-weight:bold;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table{
|
||||||
|
|
||||||
|
width:100%;
|
||||||
|
|
||||||
|
border-collapse:collapse;
|
||||||
|
|
||||||
|
margin-top:10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
th,td{
|
||||||
|
|
||||||
|
border:1px solid #ccc;
|
||||||
|
|
||||||
|
padding:6px;
|
||||||
|
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
th{
|
||||||
|
|
||||||
|
background:#c62828;
|
||||||
|
|
||||||
|
color:#fff;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
|
||||||
|
<h1>🛡️ Guardián Fénix 🔥</h1><img
|
||||||
|
<img src="freepik_23c11d8d-723a-469e-ae09-1d9d4a639e0a.png"
|
||||||
|
alt="Guardián Fénix"
|
||||||
|
class="logo">
|
||||||
|
<p>Prototipo de monitoreo ambiental y vibración</p>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
|
||||||
|
<button class="active" onclick="mostrar('inicio',this)">Inicio</button>
|
||||||
|
|
||||||
|
<button onclick="mostrar('sensores',this)">Sensores</button>
|
||||||
|
|
||||||
|
<button onclick="mostrar('graficas',this)">Gráficas</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INICIO -->
|
||||||
|
|
||||||
|
<div id="inicio" class="section active">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Descripción del proyecto</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
Guardián Fénix es un prototipo de monitoreo diseñado para sitios remotos
|
||||||
|
|
||||||
|
sin infraestructura eléctrica. El sistema simula sensores físicos
|
||||||
|
|
||||||
|
utilizando los sensores del teléfono móvil.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
La batería del dispositivo representa la batería de respaldo del sistema,
|
||||||
|
|
||||||
|
mientras que la conexión a una fuente eléctrica simula un panel solar activo.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Estado de energía</h2>
|
||||||
|
|
||||||
|
<p class="data">Batería del sistema: <span id="bat">--</span>%</p>
|
||||||
|
|
||||||
|
<p class="data">Fuente de energía: <span id="energia">--</span></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SENSORES -->
|
||||||
|
|
||||||
|
<div id="sensores" class="section">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Sensor Ambiental</h2>
|
||||||
|
|
||||||
|
<p class="data">Temperatura: <span id="temp">--</span> °C</p>
|
||||||
|
|
||||||
|
<p class="data">Humedad: <span id="hum">--</span> %</p>
|
||||||
|
|
||||||
|
<p class="data">Presión: <span id="pres">--</span> hPa</p>
|
||||||
|
|
||||||
|
<button class="action" onclick="clima()">Actualizar clima</button>
|
||||||
|
|
||||||
|
<p id="errorClima" style="color:#b00000;font-size:0.85em;"></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Sensor de Vibración</h2>
|
||||||
|
|
||||||
|
<p class="data">Vibración: <span id="vib">0.00</span></p>
|
||||||
|
|
||||||
|
<p class="data">Estado: <span id="estado">Estable</span></p>
|
||||||
|
|
||||||
|
<button class="action" id="btnVib" onclick="toggleVibracion()">Activar sensor</button>
|
||||||
|
|
||||||
|
<div class="alerta" id="alerta">
|
||||||
|
|
||||||
|
🚨 Vibración peligrosa detectada<br><br>
|
||||||
|
|
||||||
|
<button class="action" onclick="alertar()">Alertar usuarios cercanos</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Registros</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<th>Hora</th>
|
||||||
|
|
||||||
|
<th>Temp</th>
|
||||||
|
|
||||||
|
<th>Hum</th>
|
||||||
|
|
||||||
|
<th>Pres</th>
|
||||||
|
|
||||||
|
<th>Evento</th>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody id="tabla"></tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button class="action" onclick="guardar()">Guardar registro</button>
|
||||||
|
|
||||||
|
<button class="action" style="background:#555" onclick="borrarUltimo()">Borrar último</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GRAFICAS -->
|
||||||
|
|
||||||
|
<div id="graficas" class="section">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Humedad</h2>
|
||||||
|
|
||||||
|
<canvas id="gHum"></canvas>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Temperatura</h2>
|
||||||
|
|
||||||
|
<canvas id="gTemp"></canvas>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<h2>Presión</h2>
|
||||||
|
|
||||||
|
<canvas id="gPres"></canvas>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio id="sonido" loop>
|
||||||
|
|
||||||
|
<source src="https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg">
|
||||||
|
|
||||||
|
</audio>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const apiKey="729294e1fe04354f62449218961044b8";
|
||||||
|
|
||||||
|
let sensorActivo=false;
|
||||||
|
|
||||||
|
let alertaActiva=false;
|
||||||
|
|
||||||
|
let vibracion=0;
|
||||||
|
|
||||||
|
let charts={};
|
||||||
|
|
||||||
|
// Navegación
|
||||||
|
|
||||||
|
function mostrar(id,btn){
|
||||||
|
|
||||||
|
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
|
||||||
|
|
||||||
|
document.querySelectorAll('.nav button').forEach(b=>b.classList.remove('active'));
|
||||||
|
|
||||||
|
document.getElementById(id).classList.add('active');
|
||||||
|
|
||||||
|
btn.classList.add('active');
|
||||||
|
|
||||||
|
actualizarGraficas();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batería y panel solar
|
||||||
|
|
||||||
|
if(navigator.getBattery){
|
||||||
|
|
||||||
|
navigator.getBattery().then(b=>{
|
||||||
|
|
||||||
|
function actualizar(){
|
||||||
|
|
||||||
|
bat.innerText=Math.round(b.level*100);
|
||||||
|
|
||||||
|
energia.innerText=b.charging?"Panel solar activo":"Batería de respaldo";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
actualizar();
|
||||||
|
|
||||||
|
b.addEventListener("levelchange",actualizar);
|
||||||
|
|
||||||
|
b.addEventListener("chargingchange",actualizar);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clima
|
||||||
|
|
||||||
|
async function clima(){
|
||||||
|
|
||||||
|
errorClima.innerText="";
|
||||||
|
|
||||||
|
try{
|
||||||
|
|
||||||
|
const r=await fetch(`https://api.openweathermap.org/data/2.5/weather?lat=25.6866&lon=-100.3161&appid=${apiKey}&units=metric`);
|
||||||
|
|
||||||
|
const d=await r.json();
|
||||||
|
|
||||||
|
temp.innerText=Math.round(d.main.temp);
|
||||||
|
|
||||||
|
hum.innerText=d.main.humidity;
|
||||||
|
|
||||||
|
pres.innerText=d.main.pressure;
|
||||||
|
|
||||||
|
}catch{
|
||||||
|
|
||||||
|
errorClima.innerText="No se pudo acceder al clima";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vibración
|
||||||
|
|
||||||
|
function toggleVibracion(){
|
||||||
|
|
||||||
|
if(!sensorActivo){
|
||||||
|
|
||||||
|
window.addEventListener("devicemotion",leerVibracion);
|
||||||
|
|
||||||
|
btnVib.innerText="Desactivar sensor";
|
||||||
|
|
||||||
|
}else{
|
||||||
|
|
||||||
|
window.removeEventListener("devicemotion",leerVibracion);
|
||||||
|
|
||||||
|
btnVib.innerText="Activar sensor";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sensorActivo=!sensorActivo;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function leerVibracion(e){
|
||||||
|
|
||||||
|
let x=e.accelerationIncludingGravity?.x||0;
|
||||||
|
|
||||||
|
let y=e.accelerationIncludingGravity?.y||0;
|
||||||
|
|
||||||
|
let z=e.accelerationIncludingGravity?.z||0;
|
||||||
|
|
||||||
|
vibracion=Math.sqrt(x*x+y*y+z*z).toFixed(2);
|
||||||
|
|
||||||
|
vib.innerText=vibracion;
|
||||||
|
|
||||||
|
if(vibracion>12 && !alertaActiva){
|
||||||
|
|
||||||
|
alertaActiva=true;
|
||||||
|
|
||||||
|
estado.innerText="Inestable";
|
||||||
|
|
||||||
|
alerta.style.display="block";
|
||||||
|
|
||||||
|
sonido.play();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function alertar(){
|
||||||
|
|
||||||
|
alerta.style.display="none";
|
||||||
|
|
||||||
|
sonido.pause();
|
||||||
|
|
||||||
|
sonido.currentTime=0;
|
||||||
|
|
||||||
|
estado.innerText="Alerta atendida";
|
||||||
|
|
||||||
|
alertaActiva=false;
|
||||||
|
|
||||||
|
alert("Usuarios cercanos alertados");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registros
|
||||||
|
|
||||||
|
function guardar(){
|
||||||
|
|
||||||
|
const r={
|
||||||
|
|
||||||
|
h:new Date().toLocaleTimeString(),
|
||||||
|
|
||||||
|
t:temp.innerText,
|
||||||
|
|
||||||
|
hu:hum.innerText,
|
||||||
|
|
||||||
|
p:pres.innerText,
|
||||||
|
|
||||||
|
e:estado.innerText
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
let d=JSON.parse(localStorage.getItem("reg"))||[];
|
||||||
|
|
||||||
|
d.push(r);
|
||||||
|
|
||||||
|
localStorage.setItem("reg",JSON.stringify(d));
|
||||||
|
|
||||||
|
cargarTabla();
|
||||||
|
|
||||||
|
actualizarGraficas();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function cargarTabla(){
|
||||||
|
|
||||||
|
tabla.innerHTML="";
|
||||||
|
|
||||||
|
(JSON.parse(localStorage.getItem("reg"))||[]).forEach(r=>{
|
||||||
|
|
||||||
|
tabla.innerHTML+=`
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td>${r.h}</td>
|
||||||
|
|
||||||
|
<td>${r.t}</td>
|
||||||
|
|
||||||
|
<td>${r.hu}</td>
|
||||||
|
|
||||||
|
<td>${r.p}</td>
|
||||||
|
|
||||||
|
<td>${r.e}</td>
|
||||||
|
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
cargarTabla();
|
||||||
|
|
||||||
|
function borrarUltimo(){
|
||||||
|
|
||||||
|
let d=JSON.parse(localStorage.getItem("reg"))||[];
|
||||||
|
|
||||||
|
d.pop();
|
||||||
|
|
||||||
|
localStorage.setItem("reg",JSON.stringify(d));
|
||||||
|
|
||||||
|
cargarTabla();
|
||||||
|
|
||||||
|
actualizarGraficas();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gráficas automáticas
|
||||||
|
|
||||||
|
function graf(id,label,data,color){
|
||||||
|
|
||||||
|
if(charts[id]) charts[id].destroy();
|
||||||
|
|
||||||
|
charts[id]=new Chart(document.getElementById(id),{
|
||||||
|
|
||||||
|
type:"line",
|
||||||
|
|
||||||
|
data:{
|
||||||
|
|
||||||
|
labels:data.map((_,i)=>i+1),
|
||||||
|
|
||||||
|
datasets:[{
|
||||||
|
|
||||||
|
label:label,
|
||||||
|
|
||||||
|
data:data,
|
||||||
|
|
||||||
|
borderColor:color,
|
||||||
|
|
||||||
|
backgroundColor:color+"33",
|
||||||
|
|
||||||
|
fill:true,
|
||||||
|
|
||||||
|
tension:0.3
|
||||||
|
|
||||||
|
}]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function actualizarGraficas(){
|
||||||
|
|
||||||
|
const d=JSON.parse(localStorage.getItem("reg"))||[];
|
||||||
|
|
||||||
|
graf("gHum","Humedad (%)",d.map(x=>x.hu),"#1976d2");
|
||||||
|
|
||||||
|
graf("gTemp","Temperatura (°C)",d.map(x=>x.t),"#d32f2f");
|
||||||
|
|
||||||
|
graf("gPres","Presión (hPa)",d.map(x=>x.p),"#388e3c");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
253
_Sort/_/17_CameraShortCut/index_NEW.html
Normal file
226
_Sort/_/17_CameraShortCut/index_OLD.html
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="background-color:#F0F0F0;">
|
||||||
|
<script>
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
console.log(currentUrl);
|
||||||
|
let ver = "1.06"
|
||||||
|
let Data ='{"PlantName":"Ramos Arizpe",'+
|
||||||
|
'"ProjName":"RESS Battery Tray",'+
|
||||||
|
'"PlantIP": "120.",'+
|
||||||
|
'"PlantBuild": [{"PlantID": "MOD1"},{"PlantID": "MOD2"}],'+
|
||||||
|
'"Zones":[{"ZoneID": "NA"},{"ZoneID": "NB"},{"ZoneID": "NC"},{"ZoneID": "NF"},{"ZoneID": "NN"},{"ZoneID": "NZ"}],' +
|
||||||
|
'"Cams": [{"PlantID": "B01", "zone": "NA", "ip": "141.240.33" , "id": "NA015-T01", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.50" , "id": "NA020-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.51" , "id": "NA010-CAM01", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.56" , "id": "NA040-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.57" , "id": "NA040-R01CAM02", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.58" , "id": "NA040-R01CAM03", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.60" , "id": "NA040-CAM01", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.94" , "id": "NA105-T01BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.82" , "id": "NF010-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.83" , "id": "NF010-CAM01", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.192" , "id": "NF030-CAM01", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.193" , "id": "NF030-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.194" , "id": "NF030-CAM02", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.202" , "id": "NF030-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.163" , "id": "NN010-CAM01", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.164" , "id": "NN010-R02CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.183" , "id": "NN030-CAM01", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.184" , "id": "NN030-CAM02", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.185" , "id": "NN040-R02CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.217" , "id": "NN070-R01CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.218" , "id": "NN070-R02CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.219" , "id": "NN070-R03CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.220" , "id": "NN070-R04CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.221" , "id": "NN070-CAM01", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.236" , "id": "NN080-R01CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.237" , "id": "NN080-R02CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.238" , "id": "NN080-R03CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.239" , "id": "NN080-R04CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.240.240" , "id": "NN080-CAM01", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NA", "ip": "141.239.101" , "id": "NA190-BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.67" , "id": "NB030-BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.76" , "id": "NB075-T03BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.153" , "id": "NB042-R01CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.157" , "id": "NB042-R02CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.161" , "id": "NB042-R03CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.167" , "id": "NB052-R01CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.168" , "id": "NB042-CAM01", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.175" , "id": "NB052-R02CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.179" , "id": "NB052-CAM01", "type": "RVCV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.183" , "id": "NB090-R02CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.184" , "id": "NB090-CAM01", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.101" , "id": "NB110-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.102" , "id": "NB110-CAM01", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.104" , "id": "NB110-R01CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.108" , "id": "NB090-CAM02", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.147" , "id": "NB110-CAM02", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NB", "ip": "141.241.138" , "id": "NB145-T03BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NC", "ip": "141.238.37" , "id": "NC030-R01CAM01", "type": "InspectionV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NC", "ip": "141.238.38" , "id": "NC030-CAM02", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NC", "ip": "141.238.41" , "id": "NC030-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NC", "ip": "141.238.45" , "id": "NC030-CAM01", "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NC", "ip": "141.238.49" , "id": "NC025-T04BC1", "type": "BarCodeV91"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NZ", "ip": "141.237.35" , "id": "NZ003-BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NZ", "ip": "141.237.36" , "id": "NZ005-BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NZ", "ip": "141.237.44" , "id": "NZ011-BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NZ", "ip": "141.237.45" , "id": "NZ013-BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NZ", "ip": "141.237.57" , "id": "NZ020-T05BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B01", "zone": "NZ", "ip": "141.237.58" , "id": "NZ020-T06BC1", "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.43" , "id": "NA040-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.44" , "id": "NA040-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.57" , "id": "NA020-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.58" , "id": "NA040-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.59" , "id": "NA040-CAM02" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.60" , "id": "NA010-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.61" , "id": "NA015-BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.79" , "id": "NA105-BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.173", "id": "NF020-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.174", "id": "NF040-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.175", "id": "NF040-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.176", "id": "NF010-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.177", "id": "NF040-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.131.178", "id": "NF040-CAM02" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.133.46" , "id": "NA212-BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.45" , "id": "NB050-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.49" , "id": "NB050-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.53" , "id": "NB050-R03CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.55" , "id": "NB050-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.57" , "id": "NB030-BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.65" , "id": "NB070-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.69" , "id": "NB070-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.134.71" , "id": "NB070-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.58" , "id": "NB130-T03BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.89" , "id": "NB150-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.90" , "id": "NB150-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.91" , "id": "NB150-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.92" , "id": "NB170-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.93" , "id": "NB170-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.94" , "id": "NB170-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.95" , "id": "NB170-CAM02" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.135.118", "id": "NB205-T03BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.60" , "id": "NN010-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.61" , "id": "NN040-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.62" , "id": "NN010-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.63" , "id": "NN030-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.64" , "id": "NN030-CAM02" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.65" , "id": "NN030-CAM03" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.80" , "id": "NN080-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.81" , "id": "NN080-R04CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.91" , "id": "NN080-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.92" , "id": "NN080-R03CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.93" , "id": "NN080-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.109", "id": "NN090-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.110", "id": "NN090-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.111", "id": "NN090-R03CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.112", "id": "NN090-R04CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.113", "id": "NN090-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.128", "id": "NN100-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.129", "id": "NN100-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.130", "id": "NN100-R03CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.131", "id": "NN100-R04CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.132.132", "id": "NN100-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.145", "id": "NC030-BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.53" , "id": "NC050-R01CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.54" , "id": "NC050-CAM01" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.58" , "id": "NC050-R02CAM01", "type": "Inspection"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.59" , "id": "NC050-CAM02" , "type": "RVC"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.123", "id": "NZ003-T05BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.124", "id": "NZ005-T05BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.131", "id": "NZ011-T06BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.132", "id": "NZ015-T05BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.138", "id": "NZ020-R01BC1" , "type": "BarCode"},' +
|
||||||
|
'{"PlantID": "B02", "zone": "NF", "ip": "139.136.139", "id": "NZ020-R02BC1" , "type": "BarCode"}' +
|
||||||
|
']}';
|
||||||
|
|
||||||
|
const Plant = JSON.parse(Data);
|
||||||
|
document.write('<center><h2>'+Plant.PlantName+'</h2></center>');
|
||||||
|
document.write('<center>'+Plant.ProjName+'</center>');
|
||||||
|
document.write('<center><br></center>');
|
||||||
|
//
|
||||||
|
//alert(searchParams);
|
||||||
|
if (searchParams == 0){
|
||||||
|
document.write('<div class="btn-group">');
|
||||||
|
document.write('<title>' + Plant.ProjName +" (Home)"+'</title>');
|
||||||
|
for (let i = 0; i < Plant.PlantBuild.length; i++) {
|
||||||
|
document.write('<center><button onclick="PlantBuildfunc(\''+[i+1]+'\', event)"oncontextmenu="PlantBuildfunc(\''+[i+1]+'\', event)">'+Plant.PlantBuild[i].PlantID+'</button></center>');
|
||||||
|
}
|
||||||
|
document.write('</div>');
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if (searchParams.has('mod')){
|
||||||
|
parmdata = searchParams.get('mod');
|
||||||
|
document.write('<title>' + Plant.ProjName +" ("+parmdata +")"+'</title>');
|
||||||
|
document.write('<div class="btn-group">');
|
||||||
|
for (let i = 0; i < Plant.Cams.length; i++) {
|
||||||
|
if (Plant.Cams[i].PlantID == parmdata){
|
||||||
|
document.write('<center><button title="140.'+Plant.Cams[i].ip+'" onclick="CamClick(\''+[i]+'\', event)"oncontextmenu="CamClick(\''+[i]+'\', event)">'+Plant.Cams[i].id+Plant.Cams[i].PlantID+'</button></center>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.write('</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
function CamClick(vip,e) {
|
||||||
|
vtype = Plant.Cams[vip].type
|
||||||
|
if (e.type == 'click') {
|
||||||
|
if (vtype == "BarCode") {window.open('http://'+Plant.PlantIP+Plant.Cams[vip].ip+'/DesignAssistant/GMDAX_7_0_CODELABEL_G_V1_0/default.htm');}
|
||||||
|
if (vtype == "BarCodeV91") {window.open('http://'+Plant.PlantIP+Plant.Cams[vip].ip+'/DesignAssistant/GMDAX_9_1_CODELABEL_G_V1_0/default.htm');}
|
||||||
|
if (vtype == "Inspection") {window.open('http://'+Plant.PlantIP+Plant.Cams[vip].ip+'/DesignAssistant/GMDAX_7_0_BLOB_G_V1_0/default.htm');}
|
||||||
|
if (vtype == "InspectionV91") {window.open('http://'+Plant.PlantIP+Plant.Cams[vip].ip+'/DesignAssistant/GMDAX_9_1_BLOB_G_V1_0/default.htm');}
|
||||||
|
if (vtype == "RVC") {window.open('http://'+Plant.PlantIP+Plant.Cams[vip].ip+'/DesignAssistant/GMDAX_7_0_RVC_G_V2_0/default.htm');}
|
||||||
|
if (vtype == "RVCV91") {window.open('http://'+Plant.PlantIP+Plant.Cams[vip].ip+'/DesignAssistant/GMDAX_9_1_RVC_G_V1_0/default.htm');}
|
||||||
|
if (vtype = "") {alert("Camera type not programmed");}
|
||||||
|
}
|
||||||
|
if (e.type == 'contextmenu') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
temp = '\\' + '\\' +Plant.PlantIP+Plant.Cams[vip].ip;
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(temp);
|
||||||
|
} catch (e) {
|
||||||
|
var TempText = document.createElement("input");
|
||||||
|
TempText.value = temp;
|
||||||
|
document.body.appendChild(TempText);
|
||||||
|
TempText.select();
|
||||||
|
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(TempText);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function PlantBuildfunc(vip,e) {
|
||||||
|
vtype = Plant.Cams[vip].type
|
||||||
|
if (e.type == 'click') {window.open(currentUrl+'?mod=B0'+vip);}
|
||||||
|
if (e.type == 'contextmenu') {e.preventDefault();}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/* test.html?Mod=B02&zone=NA&cell=NA010 */
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
for (const param of searchParams) {
|
||||||
|
console.log(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#F0F0F0 : light grey (Backgroud)
|
||||||
|
#00D8ED : Aqua (Button Hover color)
|
||||||
|
#0073D6 : GM Blue (Button Normal color)
|
||||||
|
|
||||||
|
-->
|
||||||
19
_Sort/ble/V001.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import asyncio
|
||||||
|
from bleak import BleakScanner, BleakClient
|
||||||
|
|
||||||
|
SERVICE_UUID = "7e2a0001-1111-2222-3333-444455556666"
|
||||||
|
CHAR_UUID = "7e2a0002-1111-2222-3333-444455556666"
|
||||||
|
|
||||||
|
def handle_notify(sender, data):
|
||||||
|
text = data.decode(errors="ignore")
|
||||||
|
print("RX:", text)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("Connecting to", "10:51:DB:1B:E7:1E")
|
||||||
|
|
||||||
|
async with BleakClient("10:51:DB:1B:E7:1E") as client:
|
||||||
|
print("Connected")
|
||||||
|
await client.start_notify(CHAR_UUID, handle_notify)
|
||||||
|
await asyncio.sleep(120) # run for 1 minute
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
87
_Sort/ble/V002.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import pygame
|
||||||
|
import math
|
||||||
|
|
||||||
|
pygame.init()
|
||||||
|
pygame.joystick.init()
|
||||||
|
|
||||||
|
if pygame.joystick.get_count() == 0:
|
||||||
|
raise SystemExit("No controller detected. Plug in DualSense via USB first (recommended).")
|
||||||
|
|
||||||
|
js = pygame.joystick.Joystick(0)
|
||||||
|
js.init()
|
||||||
|
|
||||||
|
W, H = 900, 500
|
||||||
|
screen = pygame.display.set_mode((W, H))
|
||||||
|
pygame.display.set_caption(f"Controller Visualizer: {js.get_name()}")
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
font = pygame.font.SysFont(None, 24)
|
||||||
|
|
||||||
|
def clamp01(x): return max(0.0, min(1.0, x))
|
||||||
|
|
||||||
|
def draw_text(x, y, s):
|
||||||
|
screen.blit(font.render(s, True, (230, 230, 230)), (x, y))
|
||||||
|
|
||||||
|
def draw_stick(cx, cy, r, x, y, label):
|
||||||
|
# x,y expected in [-1,1]
|
||||||
|
pygame.draw.circle(screen, (80,80,80), (cx, cy), r, 2)
|
||||||
|
px = int(cx + x * (r - 6))
|
||||||
|
py = int(cy + y * (r - 6))
|
||||||
|
pygame.draw.circle(screen, (200,200,200), (px, py), 6)
|
||||||
|
draw_text(cx - r, cy + r + 8, f"{label}: x={x:+.2f} y={y:+.2f}")
|
||||||
|
|
||||||
|
def draw_bar(x, y, w, h, v, label):
|
||||||
|
# v in [0,1]
|
||||||
|
pygame.draw.rect(screen, (80,80,80), (x,y,w,h), 2)
|
||||||
|
fill = int(w * clamp01(v))
|
||||||
|
pygame.draw.rect(screen, (200,200,200), (x,y,fill,h))
|
||||||
|
draw_text(x, y - 22, f"{label}: {v:.2f}")
|
||||||
|
|
||||||
|
def draw_button(x, y, on, label):
|
||||||
|
col = (70,200,90) if on else (100,100,100)
|
||||||
|
pygame.draw.circle(screen, col, (x, y), 14)
|
||||||
|
draw_text(x + 22, y - 10, label)
|
||||||
|
|
||||||
|
# Axis indices vary by OS/driver. We’ll read a bunch and print them.
|
||||||
|
print("Axes:", js.get_numaxes(), "Buttons:", js.get_numbuttons(), "Hats:", js.get_numhats())
|
||||||
|
|
||||||
|
running = True
|
||||||
|
while running:
|
||||||
|
for e in pygame.event.get():
|
||||||
|
if e.type == pygame.QUIT:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
pygame.event.pump()
|
||||||
|
screen.fill((20, 20, 24))
|
||||||
|
|
||||||
|
# Common mappings (may differ):
|
||||||
|
lx = js.get_axis(0)
|
||||||
|
ly = js.get_axis(1)
|
||||||
|
rx = js.get_axis(2) if js.get_numaxes() > 2 else 0.0
|
||||||
|
ry = js.get_axis(3) if js.get_numaxes() > 3 else 0.0
|
||||||
|
|
||||||
|
# Triggers sometimes appear as axes, sometimes buttons; try axes 4/5 if present:
|
||||||
|
lt = (js.get_axis(4) + 1) / 2 if js.get_numaxes() > 4 else 0.0
|
||||||
|
rt = (js.get_axis(5) + 1) / 2 if js.get_numaxes() > 5 else 0.0
|
||||||
|
|
||||||
|
draw_stick(200, 220, 90, lx, ly, "Left stick")
|
||||||
|
draw_stick(450, 220, 90, rx, ry, "Right stick")
|
||||||
|
draw_bar(650, 150, 200, 24, lt, "L2")
|
||||||
|
draw_bar(650, 220, 200, 24, rt, "R2")
|
||||||
|
|
||||||
|
# Buttons (indices vary). We'll show first 12 if present:
|
||||||
|
for i in range(min(12, js.get_numbuttons())):
|
||||||
|
on = bool(js.get_button(i))
|
||||||
|
draw_button(650 + (i % 6) * 40, 300 + (i // 6) * 50, on, str(i))
|
||||||
|
|
||||||
|
# Hat (D-pad)
|
||||||
|
if js.get_numhats() > 0:
|
||||||
|
hx, hy = js.get_hat(0)
|
||||||
|
draw_text(650, 400, f"D-pad (hat0): {hx},{hy}")
|
||||||
|
|
||||||
|
draw_text(20, 20, f"Device: {js.get_name()}")
|
||||||
|
draw_text(20, 46, "Tip: If axes look wrong, mapping differs on your OS. USB is easiest.")
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
clock.tick(60)
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
142
_Sort/ble/V003.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import asyncio
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.animation import FuncAnimation
|
||||||
|
from bleak import BleakClient
|
||||||
|
|
||||||
|
# ====== HARD-CODED DEVICE ======
|
||||||
|
ADDRESS = "10:51:DB:1B:E7:1E"
|
||||||
|
|
||||||
|
# ====== ADXL345 CHARACTERISTIC UUID ======
|
||||||
|
CHAR_UUID = "7e2a1002-1111-2222-3333-444455556666"
|
||||||
|
|
||||||
|
# Plot refresh interval (ms)
|
||||||
|
PLOT_INTERVAL_MS = 50
|
||||||
|
|
||||||
|
latest = {
|
||||||
|
"ax": 0.0,
|
||||||
|
"ay": 0.0,
|
||||||
|
"az": 1.0,
|
||||||
|
"ts": 0.0,
|
||||||
|
"raw": "",
|
||||||
|
"count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_csv_triplet(s: str):
|
||||||
|
s = s.strip().strip('"')
|
||||||
|
parts = s.split(",")
|
||||||
|
if len(parts) != 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(parts[0]), float(parts[1]), float(parts[2])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def roll_pitch_from_accel(ax, ay, az):
|
||||||
|
roll = math.degrees(math.atan2(ay, az))
|
||||||
|
pitch = math.degrees(math.atan2(-ax, math.sqrt(ay * ay + az * az)))
|
||||||
|
return roll, pitch
|
||||||
|
|
||||||
|
def notify_handler(_sender: int, data: bytearray):
|
||||||
|
s = data.decode(errors="ignore").strip()
|
||||||
|
latest["raw"] = s
|
||||||
|
|
||||||
|
v = parse_csv_triplet(s)
|
||||||
|
if v is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
ax, ay, az = v
|
||||||
|
latest["ax"], latest["ay"], latest["az"] = ax, ay, az
|
||||||
|
latest["ts"] = time.time()
|
||||||
|
latest["count"] += 1
|
||||||
|
|
||||||
|
# Debug print every 5 packets (avoid flooding)
|
||||||
|
if latest["count"] % 5 == 0:
|
||||||
|
print(f"RX[{latest['count']}]: {ax:+.4f}, {ay:+.4f}, {az:+.4f}")
|
||||||
|
|
||||||
|
async def ble_loop():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"Connecting to {ADDRESS} ...")
|
||||||
|
async with BleakClient(ADDRESS, timeout=15.0) as client:
|
||||||
|
if not client.is_connected:
|
||||||
|
raise RuntimeError("Connect failed (not connected).")
|
||||||
|
|
||||||
|
print("Connected.")
|
||||||
|
print("Subscribing to notifications...")
|
||||||
|
await client.start_notify(CHAR_UUID, notify_handler)
|
||||||
|
print("Notify enabled. Streaming...\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BLE] Error/disconnect: {e}")
|
||||||
|
print("Reconnecting in 2 seconds...\n")
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
def start_ble_thread():
|
||||||
|
t = threading.Thread(target=lambda: asyncio.run(ble_loop()), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.")
|
||||||
|
start_ble_thread()
|
||||||
|
|
||||||
|
# --- Matplotlib setup ---
|
||||||
|
fig = plt.figure()
|
||||||
|
ax3 = fig.add_subplot(111, projection="3d")
|
||||||
|
ax3.set_title("ADXL345 live acceleration vector (g)")
|
||||||
|
|
||||||
|
lim = 2.0
|
||||||
|
ax3.set_xlim(-lim, lim)
|
||||||
|
ax3.set_ylim(-lim, lim)
|
||||||
|
ax3.set_zlim(-lim, lim)
|
||||||
|
ax3.set_xlabel("X (g)")
|
||||||
|
ax3.set_ylabel("Y (g)")
|
||||||
|
ax3.set_zlabel("Z (g)")
|
||||||
|
|
||||||
|
# Axes lines
|
||||||
|
ax3.plot([-lim, lim], [0, 0], [0, 0])
|
||||||
|
ax3.plot([0, 0], [-lim, lim], [0, 0])
|
||||||
|
ax3.plot([0, 0], [0, 0], [-lim, lim])
|
||||||
|
|
||||||
|
# Initial arrow + overlay
|
||||||
|
q = ax3.quiver(0, 0, 0, 0, 0, 1, length=1.0, normalize=False)
|
||||||
|
overlay = ax3.text2D(0.02, 0.95, "Waiting for data...", transform=ax3.transAxes)
|
||||||
|
|
||||||
|
def update(_frame):
|
||||||
|
nonlocal q
|
||||||
|
|
||||||
|
# Remove old arrow
|
||||||
|
try:
|
||||||
|
q.remove()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
vx, vy, vz = latest["ax"], latest["ay"], latest["az"]
|
||||||
|
roll, pitch = roll_pitch_from_accel(vx, vy, vz)
|
||||||
|
age_ms = (time.time() - latest["ts"]) * 1000.0 if latest["ts"] else float("inf")
|
||||||
|
|
||||||
|
# Draw new arrow
|
||||||
|
q = ax3.quiver(0, 0, 0, vx, vy, vz, length=1.0, normalize=False)
|
||||||
|
|
||||||
|
overlay.set_text(
|
||||||
|
f"packets: {latest['count']}\n"
|
||||||
|
f"ax={vx:+.4f} ay={vy:+.4f} az={vz:+.4f} (g)\n"
|
||||||
|
f"roll={roll:+.1f}° pitch={pitch:+.1f}° age={age_ms:.0f} ms\n"
|
||||||
|
f"raw: {latest['raw']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return q, overlay
|
||||||
|
|
||||||
|
# KEY FIX: keep this object alive
|
||||||
|
ani = FuncAnimation(fig, update, interval=PLOT_INTERVAL_MS, blit=False)
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
165
_Sort/ble/V004.py
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
# V005.py (VERSION: QT-ORDER-FIX-1)
|
||||||
|
print("=== V005 VERSION: QT-ORDER-FIX-1 ===")
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ✅ MUST create QApplication BEFORE any QWidget exists
|
||||||
|
from PyQt5 import QtWidgets, QtCore
|
||||||
|
app = QtWidgets.QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Now it is safe to import pyqtgraph / OpenGL stuff
|
||||||
|
import asyncio
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import pyqtgraph.opengl as gl
|
||||||
|
from bleak import BleakClient
|
||||||
|
|
||||||
|
# ====== HARD-CODED DEVICE ======
|
||||||
|
ADDRESS = "10:51:DB:1B:E7:1E"
|
||||||
|
CHAR_UUID = "7e2a1002-1111-2222-3333-444455556666"
|
||||||
|
|
||||||
|
latest = {"ax": 0.0, "ay": 0.0, "az": 1.0, "ts": 0.0, "count": 0, "raw": ""}
|
||||||
|
|
||||||
|
def parse_csv_triplet(s: str):
|
||||||
|
s = s.strip().strip('"')
|
||||||
|
parts = s.split(",")
|
||||||
|
if len(parts) != 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(parts[0]), float(parts[1]), float(parts[2])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def notify_handler(_sender: int, data: bytearray):
|
||||||
|
s = data.decode(errors="ignore").strip()
|
||||||
|
latest["raw"] = s
|
||||||
|
v = parse_csv_triplet(s)
|
||||||
|
if v is None:
|
||||||
|
return
|
||||||
|
latest["ax"], latest["ay"], latest["az"] = v
|
||||||
|
latest["ts"] = time.time()
|
||||||
|
latest["count"] += 1
|
||||||
|
|
||||||
|
def roll_pitch_from_accel(ax, ay, az):
|
||||||
|
roll = math.degrees(math.atan2(ay, az))
|
||||||
|
pitch = math.degrees(math.atan2(-ax, math.sqrt(ay * ay + az * az)))
|
||||||
|
return roll, pitch
|
||||||
|
|
||||||
|
async def ble_loop():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"Connecting to {ADDRESS} ...")
|
||||||
|
async with BleakClient(ADDRESS, timeout=15.0) as client:
|
||||||
|
if not client.is_connected:
|
||||||
|
raise RuntimeError("Connect failed.")
|
||||||
|
print("Connected. Enabling notifications...")
|
||||||
|
await client.start_notify(CHAR_UUID, notify_handler)
|
||||||
|
print("Notify enabled. Streaming...\n")
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BLE] Error/disconnect: {e}")
|
||||||
|
print("Reconnecting in 2 seconds...\n")
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
def start_ble_thread():
|
||||||
|
t = threading.Thread(target=lambda: asyncio.run(ble_loop()), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def make_cuboid(size=(2.0, 1.2, 0.1)):
|
||||||
|
L, W, T = size
|
||||||
|
x, y, z = L/2, W/2, T/2
|
||||||
|
verts = np.array([
|
||||||
|
[-x,-y,-z], [ x,-y,-z], [ x, y,-z], [-x, y,-z],
|
||||||
|
[-x,-y, z], [ x,-y, z], [ x, y, z], [-x, y, z],
|
||||||
|
], dtype=float)
|
||||||
|
faces = np.array([
|
||||||
|
[0,1,2],[0,2,3],
|
||||||
|
[4,6,5],[4,7,6],
|
||||||
|
[0,4,5],[0,5,1],
|
||||||
|
[1,5,6],[1,6,2],
|
||||||
|
[2,6,7],[2,7,3],
|
||||||
|
[3,7,4],[3,4,0],
|
||||||
|
], dtype=int)
|
||||||
|
return verts, faces
|
||||||
|
|
||||||
|
class Viewer(QtWidgets.QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("ADXL345 – 3D PCB Tilt (BLE)")
|
||||||
|
|
||||||
|
self.view = gl.GLViewWidget()
|
||||||
|
self.view.setCameraPosition(distance=6, elevation=25, azimuth=40)
|
||||||
|
self.setCentralWidget(self.view)
|
||||||
|
|
||||||
|
grid = gl.GLGridItem()
|
||||||
|
grid.setSize(10, 10)
|
||||||
|
grid.setSpacing(1, 1)
|
||||||
|
self.view.addItem(grid)
|
||||||
|
|
||||||
|
axis = gl.GLAxisItem()
|
||||||
|
axis.setSize(2, 2, 2)
|
||||||
|
self.view.addItem(axis)
|
||||||
|
|
||||||
|
verts, faces = make_cuboid()
|
||||||
|
colors = np.ones((faces.shape[0], 4), dtype=float)
|
||||||
|
colors[:,0]=0.25; colors[:,1]=0.7; colors[:,2]=0.35; colors[:,3]=0.9
|
||||||
|
|
||||||
|
meshdata = gl.MeshData(vertexes=verts, faces=faces)
|
||||||
|
self.mesh = gl.GLMeshItem(meshdata=meshdata, faceColors=colors, drawEdges=True, edgeColor=(0,0,0,1))
|
||||||
|
self.view.addItem(self.mesh)
|
||||||
|
|
||||||
|
self.vec = gl.GLLinePlotItem(pos=np.array([[0,0,0],[0,0,1]]), width=3, antialias=True)
|
||||||
|
self.view.addItem(self.vec)
|
||||||
|
|
||||||
|
self.label = QtWidgets.QLabel(self)
|
||||||
|
self.label.setStyleSheet("color:white; background:rgba(0,0,0,140); padding:6px;")
|
||||||
|
self.label.move(10, 10)
|
||||||
|
self.label.resize(440, 90)
|
||||||
|
|
||||||
|
self.roll = 0.0
|
||||||
|
self.pitch = 0.0
|
||||||
|
|
||||||
|
self.timer = QtCore.QTimer()
|
||||||
|
self.timer.timeout.connect(self.update_scene)
|
||||||
|
self.timer.start(20)
|
||||||
|
|
||||||
|
def update_scene(self):
|
||||||
|
axg, ayg, azg = latest["ax"], latest["ay"], latest["az"]
|
||||||
|
roll, pitch = roll_pitch_from_accel(axg, ayg, azg)
|
||||||
|
|
||||||
|
alpha = 0.15
|
||||||
|
self.roll = (1-alpha)*self.roll + alpha*roll
|
||||||
|
self.pitch = (1-alpha)*self.pitch + alpha*pitch
|
||||||
|
|
||||||
|
self.mesh.resetTransform()
|
||||||
|
self.mesh.rotate(self.pitch, 0, 1, 0)
|
||||||
|
self.mesh.rotate(self.roll, 1, 0, 0)
|
||||||
|
|
||||||
|
self.vec.setData(pos=np.array([[0,0,0],[axg, ayg, azg]], dtype=float))
|
||||||
|
|
||||||
|
age = (time.time()-latest["ts"])*1000 if latest["ts"] else 0
|
||||||
|
self.label.setText(
|
||||||
|
f"Packets: {latest['count']} Age: {age:.0f} ms\n"
|
||||||
|
f"ax={axg:+.3f}g ay={ayg:+.3f}g az={azg:+.3f}g\n"
|
||||||
|
f"roll={self.roll:+.1f}° pitch={self.pitch:+.1f}°\n"
|
||||||
|
f"raw: {latest['raw']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.")
|
||||||
|
start_ble_thread()
|
||||||
|
|
||||||
|
win = Viewer()
|
||||||
|
win.resize(900, 650)
|
||||||
|
win.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
225
_Sort/ble/V005.py
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
# V005.py (VERSION: 3D-PCB+GRAPH-1)
|
||||||
|
print("=== V005 VERSION: 3D-PCB+GRAPH-1 ===")
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from bleak import BleakClient
|
||||||
|
|
||||||
|
from PyQt5 import QtWidgets, QtCore
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import pyqtgraph.opengl as gl
|
||||||
|
|
||||||
|
# ====== HARD-CODED DEVICE ======
|
||||||
|
ADDRESS = "10:51:DB:1B:E7:1E"
|
||||||
|
CHAR_UUID = "7e2a1002-1111-2222-3333-444455556666"
|
||||||
|
|
||||||
|
# ====== GRAPH SETTINGS ======
|
||||||
|
HISTORY_SECONDS = 10.0 # how many seconds of history to show
|
||||||
|
GRAPH_UPDATE_MS = 50 # graph refresh rate
|
||||||
|
MAX_G = 4.0 # y-axis range for graph
|
||||||
|
|
||||||
|
# Shared latest values (written by BLE notify, read by UI)
|
||||||
|
latest = {"ax": 0.0, "ay": 0.0, "az": 1.0, "ts": 0.0, "count": 0, "raw": ""}
|
||||||
|
|
||||||
|
# Ring buffer for plotting
|
||||||
|
t_buf = []
|
||||||
|
ax_buf = []
|
||||||
|
ay_buf = []
|
||||||
|
az_buf = []
|
||||||
|
|
||||||
|
def parse_csv_triplet(s: str):
|
||||||
|
s = s.strip().strip('"')
|
||||||
|
parts = s.split(",")
|
||||||
|
if len(parts) != 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(parts[0]), float(parts[1]), float(parts[2])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def notify_handler(_sender: int, data: bytearray):
|
||||||
|
s = data.decode(errors="ignore").strip()
|
||||||
|
latest["raw"] = s
|
||||||
|
v = parse_csv_triplet(s)
|
||||||
|
if v is None:
|
||||||
|
return
|
||||||
|
latest["ax"], latest["ay"], latest["az"] = v
|
||||||
|
latest["ts"] = time.time()
|
||||||
|
latest["count"] += 1
|
||||||
|
|
||||||
|
def roll_pitch_from_accel(ax, ay, az):
|
||||||
|
roll = math.degrees(math.atan2(ay, az))
|
||||||
|
pitch = math.degrees(math.atan2(-ax, math.sqrt(ay * ay + az * az)))
|
||||||
|
return roll, pitch
|
||||||
|
|
||||||
|
async def ble_loop():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"Connecting to {ADDRESS} ...")
|
||||||
|
async with BleakClient(ADDRESS, timeout=15.0) as client:
|
||||||
|
if not client.is_connected:
|
||||||
|
raise RuntimeError("Connect failed.")
|
||||||
|
print("Connected. Enabling notifications...")
|
||||||
|
await client.start_notify(CHAR_UUID, notify_handler)
|
||||||
|
print("Notify enabled. Streaming...\n")
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BLE] Error/disconnect: {e}")
|
||||||
|
print("Reconnecting in 2 seconds...\n")
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
def start_ble_thread():
|
||||||
|
t = threading.Thread(target=lambda: asyncio.run(ble_loop()), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def make_cuboid(size=(2.0, 1.2, 0.1)):
|
||||||
|
L, W, T = size
|
||||||
|
x, y, z = L/2, W/2, T/2
|
||||||
|
verts = np.array([
|
||||||
|
[-x,-y,-z], [ x,-y,-z], [ x, y,-z], [-x, y,-z],
|
||||||
|
[-x,-y, z], [ x,-y, z], [ x, y, z], [-x, y, z],
|
||||||
|
], dtype=float)
|
||||||
|
faces = np.array([
|
||||||
|
[0,1,2],[0,2,3],
|
||||||
|
[4,6,5],[4,7,6],
|
||||||
|
[0,4,5],[0,5,1],
|
||||||
|
[1,5,6],[1,6,2],
|
||||||
|
[2,6,7],[2,7,3],
|
||||||
|
[3,7,4],[3,4,0],
|
||||||
|
], dtype=int)
|
||||||
|
return verts, faces
|
||||||
|
|
||||||
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("ADXL345 – 3D PCB + Live Accel Graph (BLE)")
|
||||||
|
|
||||||
|
# ---- Layout container ----
|
||||||
|
central = QtWidgets.QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
vbox = QtWidgets.QVBoxLayout(central)
|
||||||
|
vbox.setContentsMargins(6, 6, 6, 6)
|
||||||
|
vbox.setSpacing(6)
|
||||||
|
|
||||||
|
# ---- 3D view ----
|
||||||
|
self.view3d = gl.GLViewWidget()
|
||||||
|
self.view3d.setMinimumHeight(430)
|
||||||
|
self.view3d.setCameraPosition(distance=6, elevation=25, azimuth=40)
|
||||||
|
vbox.addWidget(self.view3d, stretch=3)
|
||||||
|
|
||||||
|
grid = gl.GLGridItem()
|
||||||
|
grid.setSize(10, 10)
|
||||||
|
grid.setSpacing(1, 1)
|
||||||
|
self.view3d.addItem(grid)
|
||||||
|
|
||||||
|
axis = gl.GLAxisItem()
|
||||||
|
axis.setSize(2, 2, 2)
|
||||||
|
self.view3d.addItem(axis)
|
||||||
|
|
||||||
|
verts, faces = make_cuboid()
|
||||||
|
colors = np.ones((faces.shape[0], 4), dtype=float)
|
||||||
|
colors[:,0]=0.25; colors[:,1]=0.7; colors[:,2]=0.35; colors[:,3]=0.9
|
||||||
|
|
||||||
|
meshdata = gl.MeshData(vertexes=verts, faces=faces)
|
||||||
|
self.mesh = gl.GLMeshItem(meshdata=meshdata, faceColors=colors, drawEdges=True, edgeColor=(0,0,0,1))
|
||||||
|
self.view3d.addItem(self.mesh)
|
||||||
|
|
||||||
|
self.vec = gl.GLLinePlotItem(pos=np.array([[0,0,0],[0,0,1]]), width=3, antialias=True)
|
||||||
|
self.view3d.addItem(self.vec)
|
||||||
|
|
||||||
|
# Small overlay label (top-left of window)
|
||||||
|
self.label = QtWidgets.QLabel()
|
||||||
|
self.label.setStyleSheet("color:white; background:rgba(0,0,0,140); padding:6px;")
|
||||||
|
vbox.addWidget(self.label)
|
||||||
|
|
||||||
|
# ---- Graph ----
|
||||||
|
self.plot = pg.PlotWidget()
|
||||||
|
self.plot.setMinimumHeight(200)
|
||||||
|
self.plot.showGrid(x=True, y=True, alpha=0.25)
|
||||||
|
self.plot.setLabel("left", "Acceleration", units="g")
|
||||||
|
self.plot.setLabel("bottom", "Time", units="s")
|
||||||
|
self.plot.setYRange(-MAX_G, MAX_G)
|
||||||
|
self.plot.addLegend(offset=(10, 10))
|
||||||
|
vbox.addWidget(self.plot, stretch=1)
|
||||||
|
|
||||||
|
# Curves
|
||||||
|
self.curve_ax = self.plot.plot([], [], name="ax", pen=pg.mkPen('r', width=2))
|
||||||
|
self.curve_ay = self.plot.plot([], [], name="ay", pen=pg.mkPen('g', width=2))
|
||||||
|
self.curve_az = self.plot.plot([], [], name="az", pen=pg.mkPen('b', width=2))
|
||||||
|
|
||||||
|
# ---- State ----
|
||||||
|
self.roll = 0.0
|
||||||
|
self.pitch = 0.0
|
||||||
|
self.t0 = time.time()
|
||||||
|
|
||||||
|
# ---- Timers ----
|
||||||
|
self.timer = QtCore.QTimer()
|
||||||
|
self.timer.timeout.connect(self.update_ui)
|
||||||
|
self.timer.start(GRAPH_UPDATE_MS)
|
||||||
|
|
||||||
|
def update_ui(self):
|
||||||
|
axg, ayg, azg = latest["ax"], latest["ay"], latest["az"]
|
||||||
|
|
||||||
|
# Add to history buffers
|
||||||
|
t = time.time() - self.t0
|
||||||
|
t_buf.append(t)
|
||||||
|
ax_buf.append(axg)
|
||||||
|
ay_buf.append(ayg)
|
||||||
|
az_buf.append(azg)
|
||||||
|
|
||||||
|
# Trim buffers to last HISTORY_SECONDS
|
||||||
|
while t_buf and (t - t_buf[0]) > HISTORY_SECONDS:
|
||||||
|
t_buf.pop(0)
|
||||||
|
ax_buf.pop(0)
|
||||||
|
ay_buf.pop(0)
|
||||||
|
az_buf.pop(0)
|
||||||
|
|
||||||
|
# Update plot curves
|
||||||
|
self.curve_ax.setData(t_buf, ax_buf)
|
||||||
|
self.curve_ay.setData(t_buf, ay_buf)
|
||||||
|
self.curve_az.setData(t_buf, az_buf)
|
||||||
|
if t_buf:
|
||||||
|
self.plot.setXRange(max(0, t_buf[-1] - HISTORY_SECONDS), t_buf[-1])
|
||||||
|
|
||||||
|
# Update 3D block tilt (smoothed)
|
||||||
|
roll, pitch = roll_pitch_from_accel(axg, ayg, azg)
|
||||||
|
alpha = 0.15
|
||||||
|
self.roll = (1 - alpha) * self.roll + alpha * roll
|
||||||
|
self.pitch = (1 - alpha) * self.pitch + alpha * pitch
|
||||||
|
|
||||||
|
self.mesh.resetTransform()
|
||||||
|
self.mesh.rotate(self.pitch, 0, 1, 0)
|
||||||
|
self.mesh.rotate(self.roll, 1, 0, 0)
|
||||||
|
|
||||||
|
# Update accel vector
|
||||||
|
self.vec.setData(pos=np.array([[0, 0, 0], [axg, ayg, azg]], dtype=float))
|
||||||
|
|
||||||
|
# Update label
|
||||||
|
age = (time.time() - latest["ts"]) * 1000 if latest["ts"] else 0
|
||||||
|
self.label.setText(
|
||||||
|
f"Packets: {latest['count']} Age: {age:.0f} ms\n"
|
||||||
|
f"ax={axg:+.3f}g ay={ayg:+.3f}g az={azg:+.3f}g\n"
|
||||||
|
f"roll={self.roll:+.1f}° pitch={self.pitch:+.1f}°"
|
||||||
|
)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.")
|
||||||
|
start_ble_thread()
|
||||||
|
|
||||||
|
app = QtWidgets.QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
|
win = MainWindow()
|
||||||
|
win.resize(1000, 750)
|
||||||
|
win.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
331
_Sort/ble/V006.py
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
# V005.py (VERSION: 3D-PCB+GRAPH+SHAKEBAR-1)
|
||||||
|
print("=== V006 VERSION: 3D-PCB+GRAPH+SHAKEBAR-1 ===")
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from bleak import BleakClient
|
||||||
|
|
||||||
|
from PyQt5 import QtWidgets, QtCore
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import pyqtgraph.opengl as gl
|
||||||
|
|
||||||
|
# ====== HARD-CODED DEVICE ======
|
||||||
|
ADDRESS = "10:51:DB:1B:E7:1E"
|
||||||
|
CHAR_UUID = "7e2a1002-1111-2222-3333-444455556666"
|
||||||
|
|
||||||
|
# ====== GRAPH SETTINGS ======
|
||||||
|
HISTORY_SECONDS = 10.0
|
||||||
|
UI_UPDATE_MS = 50
|
||||||
|
MAX_G = 4.0
|
||||||
|
|
||||||
|
# ====== SHAKE METRICS SETTINGS ======
|
||||||
|
# magnitude |a| in g (gravity ~1.0g when stationary)
|
||||||
|
WARN_MAG_G = 1.30 # mild motion
|
||||||
|
SHAKE_MAG_G = 1.70 # strong shake
|
||||||
|
|
||||||
|
# Use delta from 1g for percent mapping:
|
||||||
|
# delta_g = abs(|a| - 1.0)
|
||||||
|
DELTA_G_FULL_SCALE = 1.0 # delta=1.0g => 100%
|
||||||
|
|
||||||
|
# Shared latest values (written by BLE notify, read by UI)
|
||||||
|
latest = {"ax": 0.0, "ay": 0.0, "az": 1.0, "ts": 0.0, "count": 0, "raw": ""}
|
||||||
|
|
||||||
|
# Ring buffers for plotting
|
||||||
|
t_buf = []
|
||||||
|
ax_buf = []
|
||||||
|
ay_buf = []
|
||||||
|
az_buf = []
|
||||||
|
mag_buf = []
|
||||||
|
|
||||||
|
# ---------------- BLE ----------------
|
||||||
|
|
||||||
|
def parse_csv_triplet(s: str):
|
||||||
|
s = s.strip().strip('"')
|
||||||
|
parts = s.split(",")
|
||||||
|
if len(parts) != 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(parts[0]), float(parts[1]), float(parts[2])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def notify_handler(_sender: int, data: bytearray):
|
||||||
|
s = data.decode(errors="ignore").strip()
|
||||||
|
latest["raw"] = s
|
||||||
|
v = parse_csv_triplet(s)
|
||||||
|
if v is None:
|
||||||
|
return
|
||||||
|
latest["ax"], latest["ay"], latest["az"] = v
|
||||||
|
latest["ts"] = time.time()
|
||||||
|
latest["count"] += 1
|
||||||
|
|
||||||
|
def roll_pitch_from_accel(ax, ay, az):
|
||||||
|
roll = math.degrees(math.atan2(ay, az))
|
||||||
|
pitch = math.degrees(math.atan2(-ax, math.sqrt(ay * ay + az * az)))
|
||||||
|
return roll, pitch
|
||||||
|
|
||||||
|
async def ble_loop():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"Connecting to {ADDRESS} ...")
|
||||||
|
async with BleakClient(ADDRESS, timeout=15.0) as client:
|
||||||
|
if not client.is_connected:
|
||||||
|
raise RuntimeError("Connect failed.")
|
||||||
|
print("Connected. Enabling notifications...")
|
||||||
|
await client.start_notify(CHAR_UUID, notify_handler)
|
||||||
|
print("Notify enabled. Streaming...\n")
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BLE] Error/disconnect: {e}")
|
||||||
|
print("Reconnecting in 2 seconds...\n")
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
def start_ble_thread():
|
||||||
|
t = threading.Thread(target=lambda: asyncio.run(ble_loop()), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
# ---------------- 3D helpers ----------------
|
||||||
|
|
||||||
|
def make_cuboid(size=(2.0, 1.2, 0.1)):
|
||||||
|
L, W, T = size
|
||||||
|
x, y, z = L/2, W/2, T/2
|
||||||
|
verts = np.array([
|
||||||
|
[-x,-y,-z], [ x,-y,-z], [ x, y,-z], [-x, y,-z],
|
||||||
|
[-x,-y, z], [ x,-y, z], [ x, y, z], [-x, y, z],
|
||||||
|
], dtype=float)
|
||||||
|
faces = np.array([
|
||||||
|
[0,1,2],[0,2,3],
|
||||||
|
[4,6,5],[4,7,6],
|
||||||
|
[0,4,5],[0,5,1],
|
||||||
|
[1,5,6],[1,6,2],
|
||||||
|
[2,6,7],[2,7,3],
|
||||||
|
[3,7,4],[3,4,0],
|
||||||
|
], dtype=int)
|
||||||
|
return verts, faces
|
||||||
|
|
||||||
|
# ---------------- UI ----------------
|
||||||
|
|
||||||
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("ADXL345 – 3D PCB + Live Graph + Shake Meter (BLE)")
|
||||||
|
|
||||||
|
central = QtWidgets.QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
outer = QtWidgets.QVBoxLayout(central)
|
||||||
|
outer.setContentsMargins(6, 6, 6, 6)
|
||||||
|
outer.setSpacing(6)
|
||||||
|
|
||||||
|
# ---- 3D view ----
|
||||||
|
self.view3d = gl.GLViewWidget()
|
||||||
|
self.view3d.setMinimumHeight(420)
|
||||||
|
self.view3d.setCameraPosition(distance=6, elevation=25, azimuth=40)
|
||||||
|
outer.addWidget(self.view3d, stretch=3)
|
||||||
|
|
||||||
|
grid = gl.GLGridItem()
|
||||||
|
grid.setSize(10, 10)
|
||||||
|
grid.setSpacing(1, 1)
|
||||||
|
self.view3d.addItem(grid)
|
||||||
|
|
||||||
|
axis = gl.GLAxisItem()
|
||||||
|
axis.setSize(2, 2, 2)
|
||||||
|
self.view3d.addItem(axis)
|
||||||
|
|
||||||
|
verts, faces = make_cuboid()
|
||||||
|
colors = np.ones((faces.shape[0], 4), dtype=float)
|
||||||
|
colors[:,0]=0.25; colors[:,1]=0.7; colors[:,2]=0.35; colors[:,3]=0.9
|
||||||
|
|
||||||
|
meshdata = gl.MeshData(vertexes=verts, faces=faces)
|
||||||
|
self.mesh = gl.GLMeshItem(meshdata=meshdata, faceColors=colors,
|
||||||
|
drawEdges=True, edgeColor=(0,0,0,1))
|
||||||
|
self.view3d.addItem(self.mesh)
|
||||||
|
|
||||||
|
self.vec = gl.GLLinePlotItem(pos=np.array([[0,0,0],[0,0,1]]),
|
||||||
|
width=3, antialias=True)
|
||||||
|
self.view3d.addItem(self.vec)
|
||||||
|
|
||||||
|
# ---- Status label ----
|
||||||
|
self.status = QtWidgets.QLabel()
|
||||||
|
self.status.setStyleSheet("color:white; background:rgba(0,0,0,140); padding:6px;")
|
||||||
|
outer.addWidget(self.status)
|
||||||
|
|
||||||
|
# ---- Bottom area: graph + shake meter (right) ----
|
||||||
|
bottom = QtWidgets.QHBoxLayout()
|
||||||
|
bottom.setSpacing(10)
|
||||||
|
outer.addLayout(bottom, stretch=2)
|
||||||
|
|
||||||
|
# Graph
|
||||||
|
self.plot = pg.PlotWidget()
|
||||||
|
self.plot.setMinimumHeight(230)
|
||||||
|
self.plot.showGrid(x=True, y=True, alpha=0.25)
|
||||||
|
self.plot.setLabel("left", "Acceleration", units="g")
|
||||||
|
self.plot.setLabel("bottom", "Time", units="s")
|
||||||
|
self.plot.setYRange(-MAX_G, MAX_G)
|
||||||
|
self.plot.addLegend(offset=(10, 10))
|
||||||
|
|
||||||
|
bottom.addWidget(self.plot, stretch=5)
|
||||||
|
|
||||||
|
# Curves
|
||||||
|
self.curve_ax = self.plot.plot([], [], name="ax", pen=pg.mkPen('r', width=2))
|
||||||
|
self.curve_ay = self.plot.plot([], [], name="ay", pen=pg.mkPen('g', width=2))
|
||||||
|
self.curve_az = self.plot.plot([], [], name="az", pen=pg.mkPen('b', width=2))
|
||||||
|
self.curve_mag = self.plot.plot([], [], name="|a|", pen=pg.mkPen('y', width=2))
|
||||||
|
|
||||||
|
# Threshold lines (horizontal) for magnitude
|
||||||
|
self.warn_line = pg.InfiniteLine(pos=+WARN_MAG_G, angle=0, pen=pg.mkPen((255,165,0), width=2, style=QtCore.Qt.DashLine))
|
||||||
|
self.shake_line = pg.InfiniteLine(pos=+SHAKE_MAG_G, angle=0, pen=pg.mkPen((255, 60, 60), width=2, style=QtCore.Qt.DashLine))
|
||||||
|
self.plot.addItem(self.warn_line)
|
||||||
|
self.plot.addItem(self.shake_line)
|
||||||
|
|
||||||
|
# Shake meter panel (right)
|
||||||
|
panel = QtWidgets.QVBoxLayout()
|
||||||
|
panel.setSpacing(6)
|
||||||
|
bottom.addLayout(panel, stretch=1)
|
||||||
|
|
||||||
|
self.meter_title = QtWidgets.QLabel("Shake magnitude")
|
||||||
|
self.meter_title.setStyleSheet("font-weight: 600;")
|
||||||
|
panel.addWidget(self.meter_title)
|
||||||
|
|
||||||
|
self.meter_bar = QtWidgets.QProgressBar()
|
||||||
|
self.meter_bar.setRange(0, 100)
|
||||||
|
self.meter_bar.setValue(0)
|
||||||
|
self.meter_bar.setFormat("%p%")
|
||||||
|
self.meter_bar.setTextVisible(True)
|
||||||
|
self.meter_bar.setMinimumWidth(220)
|
||||||
|
self.meter_bar.setMinimumHeight(28)
|
||||||
|
panel.addWidget(self.meter_bar)
|
||||||
|
|
||||||
|
self.meter_readout = QtWidgets.QLabel("|a| = 0.000 g\nΔ=0.000 g")
|
||||||
|
panel.addWidget(self.meter_readout)
|
||||||
|
|
||||||
|
self.indicators = QtWidgets.QLabel("CALM")
|
||||||
|
self.indicators.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
self.indicators.setStyleSheet(
|
||||||
|
"font-weight: 700; font-size: 18px; padding: 10px; "
|
||||||
|
"border: 2px solid #555; border-radius: 10px;"
|
||||||
|
)
|
||||||
|
panel.addWidget(self.indicators)
|
||||||
|
|
||||||
|
panel.addStretch(1)
|
||||||
|
|
||||||
|
# ---- State ----
|
||||||
|
self.roll = 0.0
|
||||||
|
self.pitch = 0.0
|
||||||
|
self.t0 = time.time()
|
||||||
|
|
||||||
|
# ---- Timer ----
|
||||||
|
self.timer = QtCore.QTimer()
|
||||||
|
self.timer.timeout.connect(self.update_ui)
|
||||||
|
self.timer.start(UI_UPDATE_MS)
|
||||||
|
|
||||||
|
def set_indicator_state(self, state: str):
|
||||||
|
# Simple color changes via stylesheet
|
||||||
|
if state == "CALM":
|
||||||
|
self.indicators.setText("CALM")
|
||||||
|
self.indicators.setStyleSheet(
|
||||||
|
"font-weight: 700; font-size: 18px; padding: 10px;"
|
||||||
|
"border: 2px solid #555; border-radius: 10px;"
|
||||||
|
"background: rgba(80,80,80,120); color: white;"
|
||||||
|
)
|
||||||
|
elif state == "WARN":
|
||||||
|
self.indicators.setText("WARN")
|
||||||
|
self.indicators.setStyleSheet(
|
||||||
|
"font-weight: 700; font-size: 18px; padding: 10px;"
|
||||||
|
"border: 2px solid #a86a00; border-radius: 10px;"
|
||||||
|
"background: rgba(255,165,0,130); color: black;"
|
||||||
|
)
|
||||||
|
else: # SHAKE
|
||||||
|
self.indicators.setText("SHAKE!")
|
||||||
|
self.indicators.setStyleSheet(
|
||||||
|
"font-weight: 800; font-size: 20px; padding: 10px;"
|
||||||
|
"border: 2px solid #b00000; border-radius: 10px;"
|
||||||
|
"background: rgba(255,60,60,180); color: white;"
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_ui(self):
|
||||||
|
axg, ayg, azg = latest["ax"], latest["ay"], latest["az"]
|
||||||
|
|
||||||
|
# Magnitude (g)
|
||||||
|
mag = math.sqrt(axg*axg + ayg*ayg + azg*azg)
|
||||||
|
delta = abs(mag - 1.0)
|
||||||
|
|
||||||
|
# Map delta to 0..100%
|
||||||
|
pct = int(max(0.0, min(1.0, delta / max(1e-6, DELTA_G_FULL_SCALE))) * 100.0)
|
||||||
|
self.meter_bar.setValue(pct)
|
||||||
|
self.meter_readout.setText(f"|a| = {mag:.3f} g\nΔ = {delta:.3f} g")
|
||||||
|
|
||||||
|
# Threshold indicators
|
||||||
|
if mag >= SHAKE_MAG_G:
|
||||||
|
self.set_indicator_state("SHAKE")
|
||||||
|
elif mag >= WARN_MAG_G:
|
||||||
|
self.set_indicator_state("WARN")
|
||||||
|
else:
|
||||||
|
self.set_indicator_state("CALM")
|
||||||
|
|
||||||
|
# Add to history buffers
|
||||||
|
t = time.time() - self.t0
|
||||||
|
t_buf.append(t)
|
||||||
|
ax_buf.append(axg)
|
||||||
|
ay_buf.append(ayg)
|
||||||
|
az_buf.append(azg)
|
||||||
|
mag_buf.append(mag)
|
||||||
|
|
||||||
|
# Trim buffers
|
||||||
|
while t_buf and (t - t_buf[0]) > HISTORY_SECONDS:
|
||||||
|
t_buf.pop(0)
|
||||||
|
ax_buf.pop(0)
|
||||||
|
ay_buf.pop(0)
|
||||||
|
az_buf.pop(0)
|
||||||
|
mag_buf.pop(0)
|
||||||
|
|
||||||
|
# Update plot
|
||||||
|
self.curve_ax.setData(t_buf, ax_buf)
|
||||||
|
self.curve_ay.setData(t_buf, ay_buf)
|
||||||
|
self.curve_az.setData(t_buf, az_buf)
|
||||||
|
self.curve_mag.setData(t_buf, mag_buf)
|
||||||
|
|
||||||
|
if t_buf:
|
||||||
|
self.plot.setXRange(max(0, t_buf[-1] - HISTORY_SECONDS), t_buf[-1])
|
||||||
|
|
||||||
|
# Update 3D tilt (smoothed)
|
||||||
|
roll, pitch = roll_pitch_from_accel(axg, ayg, azg)
|
||||||
|
alpha = 0.15
|
||||||
|
self.roll = (1 - alpha) * self.roll + alpha * roll
|
||||||
|
self.pitch = (1 - alpha) * self.pitch + alpha * pitch
|
||||||
|
|
||||||
|
self.mesh.resetTransform()
|
||||||
|
self.mesh.rotate(self.pitch, 0, 1, 0)
|
||||||
|
self.mesh.rotate(self.roll, 1, 0, 0)
|
||||||
|
|
||||||
|
# Vector line
|
||||||
|
self.vec.setData(pos=np.array([[0, 0, 0], [axg, ayg, azg]], dtype=float))
|
||||||
|
|
||||||
|
# Top status text
|
||||||
|
age = (time.time() - latest["ts"]) * 1000 if latest["ts"] else 0
|
||||||
|
self.status.setText(
|
||||||
|
f"Packets: {latest['count']} Age: {age:.0f} ms\n"
|
||||||
|
f"ax={axg:+.3f}g ay={ayg:+.3f}g az={azg:+.3f}g |a|={mag:.3f}g\n"
|
||||||
|
f"roll={self.roll:+.1f}° pitch={self.pitch:+.1f}°"
|
||||||
|
)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.")
|
||||||
|
start_ble_thread()
|
||||||
|
|
||||||
|
app = QtWidgets.QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
|
win = MainWindow()
|
||||||
|
win.resize(1100, 820)
|
||||||
|
win.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
295
_Sort/ble/V007.py
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
# bme680_ui.py (VERSION: BME680-GRAPH+BAR-1)
|
||||||
|
print("=== BME680 UI VERSION: BME680-GRAPH+BAR-1 ===")
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bleak import BleakClient
|
||||||
|
from PyQt5 import QtWidgets, QtCore
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
# ====== HARD-CODED DEVICE + UUIDs ======
|
||||||
|
ADDRESS = "98:88:E0:76:30:56" # change if your BME680 device uses a different MAC
|
||||||
|
CHAR_UUID = "7e2a0002-1111-2222-3333-444455556666" # change if different from ADXL project
|
||||||
|
|
||||||
|
# ====== UI SETTINGS ======
|
||||||
|
HISTORY_SECONDS = 120.0 # 2 minutes of history (BME changes slower than accel)
|
||||||
|
UI_UPDATE_MS = 250 # 4 Hz UI refresh is plenty for env data
|
||||||
|
|
||||||
|
# “Air change” percent meter tuning (based on gas resistance change)
|
||||||
|
# We measure delta from a slowly-updated baseline.
|
||||||
|
BASELINE_ALPHA = 0.01 # slower = steadier baseline
|
||||||
|
DELTA_FULL_SCALE = 20000 # ohms delta => 100% (tune this)
|
||||||
|
|
||||||
|
# Thresholds for indicator states (percent)
|
||||||
|
WARN_PCT = 30
|
||||||
|
ALERT_PCT = 60
|
||||||
|
|
||||||
|
latest = {
|
||||||
|
"t": None, # °C
|
||||||
|
"h": None, # %
|
||||||
|
"gas": None, # ohms
|
||||||
|
"p": None, # hPa
|
||||||
|
"alt": None, # m
|
||||||
|
"ts": 0.0,
|
||||||
|
"count": 0,
|
||||||
|
"raw": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Buffers
|
||||||
|
t_buf = []
|
||||||
|
temp_buf = []
|
||||||
|
hum_buf = []
|
||||||
|
press_buf = []
|
||||||
|
gas_buf = []
|
||||||
|
alt_buf = []
|
||||||
|
|
||||||
|
gas_baseline = None # running baseline for “change” meter
|
||||||
|
|
||||||
|
|
||||||
|
def parse_csv_5(s: str):
|
||||||
|
s = s.strip().strip('"')
|
||||||
|
parts = s.split(",")
|
||||||
|
if len(parts) != 5:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
# temp, hum, gas_ohms, pressure_hpa, altitude_m
|
||||||
|
return float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def notify_handler(_sender: int, data: bytearray):
|
||||||
|
s = data.decode(errors="ignore").strip()
|
||||||
|
latest["raw"] = s
|
||||||
|
v = parse_csv_5(s)
|
||||||
|
if v is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
latest["t"], latest["h"], latest["gas"], latest["p"], latest["alt"] = v
|
||||||
|
latest["ts"] = time.time()
|
||||||
|
latest["count"] += 1
|
||||||
|
|
||||||
|
|
||||||
|
async def ble_loop():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"Connecting to {ADDRESS} ...")
|
||||||
|
async with BleakClient(ADDRESS, timeout=15.0) as client:
|
||||||
|
if not client.is_connected:
|
||||||
|
raise RuntimeError("Connect failed.")
|
||||||
|
|
||||||
|
print("Connected. Enabling notifications...")
|
||||||
|
await client.start_notify(CHAR_UUID, notify_handler)
|
||||||
|
print("Notify enabled. Streaming...\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BLE] Error/disconnect: {e}")
|
||||||
|
print("Reconnecting in 2 seconds...\n")
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def start_ble_thread():
|
||||||
|
t = threading.Thread(target=lambda: asyncio.run(ble_loop()), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("BME680 – Live Graph + Air-Change Meter (BLE)")
|
||||||
|
|
||||||
|
central = QtWidgets.QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
|
outer = QtWidgets.QVBoxLayout(central)
|
||||||
|
outer.setContentsMargins(8, 8, 8, 8)
|
||||||
|
outer.setSpacing(8)
|
||||||
|
|
||||||
|
self.status = QtWidgets.QLabel("Waiting for data...")
|
||||||
|
self.status.setStyleSheet("color:white; background:rgba(0,0,0,140); padding:8px;")
|
||||||
|
outer.addWidget(self.status)
|
||||||
|
|
||||||
|
# Bottom area: plots + meter on right
|
||||||
|
row = QtWidgets.QHBoxLayout()
|
||||||
|
row.setSpacing(10)
|
||||||
|
outer.addLayout(row)
|
||||||
|
|
||||||
|
# One plot with multiple y-scales is messy; we’ll use 2 plots stacked.
|
||||||
|
plots_col = QtWidgets.QVBoxLayout()
|
||||||
|
plots_col.setSpacing(8)
|
||||||
|
row.addLayout(plots_col, stretch=5)
|
||||||
|
|
||||||
|
# Plot 1: Temp + Humidity
|
||||||
|
self.plot1 = pg.PlotWidget()
|
||||||
|
self.plot1.showGrid(x=True, y=True, alpha=0.25)
|
||||||
|
self.plot1.setLabel("left", "Temp (°C) / Humidity (%)")
|
||||||
|
self.plot1.setLabel("bottom", "Time (s)")
|
||||||
|
self.plot1.addLegend(offset=(10, 10))
|
||||||
|
plots_col.addWidget(self.plot1)
|
||||||
|
|
||||||
|
self.curve_temp = self.plot1.plot([], [], name="Temp °C", pen=pg.mkPen('r', width=2))
|
||||||
|
self.curve_hum = self.plot1.plot([], [], name="Humidity %", pen=pg.mkPen('g', width=2))
|
||||||
|
|
||||||
|
# Plot 2: Pressure + Gas (gas is big so it’ll dominate visually; that’s ok)
|
||||||
|
self.plot2 = pg.PlotWidget()
|
||||||
|
self.plot2.showGrid(x=True, y=True, alpha=0.25)
|
||||||
|
self.plot2.setLabel("left", "Pressure (hPa) / Gas (Ω)")
|
||||||
|
self.plot2.setLabel("bottom", "Time (s)")
|
||||||
|
self.plot2.addLegend(offset=(10, 10))
|
||||||
|
plots_col.addWidget(self.plot2)
|
||||||
|
|
||||||
|
self.curve_press = self.plot2.plot([], [], name="Pressure hPa", pen=pg.mkPen('c', width=2))
|
||||||
|
self.curve_gas = self.plot2.plot([], [], name="Gas Ω", pen=pg.mkPen('y', width=2))
|
||||||
|
|
||||||
|
# Meter panel on right
|
||||||
|
panel = QtWidgets.QVBoxLayout()
|
||||||
|
panel.setSpacing(8)
|
||||||
|
row.addLayout(panel, stretch=1)
|
||||||
|
|
||||||
|
title = QtWidgets.QLabel("Air change (gas Δ)")
|
||||||
|
title.setStyleSheet("font-weight: 700;")
|
||||||
|
panel.addWidget(title)
|
||||||
|
|
||||||
|
self.bar = QtWidgets.QProgressBar()
|
||||||
|
self.bar.setRange(0, 100)
|
||||||
|
self.bar.setValue(0)
|
||||||
|
self.bar.setMinimumWidth(240)
|
||||||
|
self.bar.setMinimumHeight(28)
|
||||||
|
self.bar.setFormat("%p%")
|
||||||
|
panel.addWidget(self.bar)
|
||||||
|
|
||||||
|
self.readout = QtWidgets.QLabel("gas: --- Ω\nbaseline: --- Ω\nΔ: --- Ω")
|
||||||
|
panel.addWidget(self.readout)
|
||||||
|
|
||||||
|
self.ind = QtWidgets.QLabel("CALM")
|
||||||
|
self.ind.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
self.ind.setStyleSheet(
|
||||||
|
"font-weight: 800; font-size: 20px; padding: 10px;"
|
||||||
|
"border: 2px solid #555; border-radius: 10px;"
|
||||||
|
"background: rgba(80,80,80,120); color: white;"
|
||||||
|
)
|
||||||
|
panel.addWidget(self.ind)
|
||||||
|
panel.addStretch(1)
|
||||||
|
|
||||||
|
self.t0 = time.time()
|
||||||
|
|
||||||
|
self.timer = QtCore.QTimer()
|
||||||
|
self.timer.timeout.connect(self.update_ui)
|
||||||
|
self.timer.start(UI_UPDATE_MS)
|
||||||
|
|
||||||
|
def set_state(self, state: str):
|
||||||
|
if state == "CALM":
|
||||||
|
self.ind.setText("CALM")
|
||||||
|
self.ind.setStyleSheet(
|
||||||
|
"font-weight: 800; font-size: 20px; padding: 10px;"
|
||||||
|
"border: 2px solid #555; border-radius: 10px;"
|
||||||
|
"background: rgba(80,80,80,120); color: white;"
|
||||||
|
)
|
||||||
|
elif state == "WARN":
|
||||||
|
self.ind.setText("CHANGE")
|
||||||
|
self.ind.setStyleSheet(
|
||||||
|
"font-weight: 800; font-size: 20px; padding: 10px;"
|
||||||
|
"border: 2px solid #a86a00; border-radius: 10px;"
|
||||||
|
"background: rgba(255,165,0,140); color: black;"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.ind.setText("BIG CHANGE")
|
||||||
|
self.ind.setStyleSheet(
|
||||||
|
"font-weight: 900; font-size: 20px; padding: 10px;"
|
||||||
|
"border: 2px solid #b00000; border-radius: 10px;"
|
||||||
|
"background: rgba(255,60,60,180); color: white;"
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_ui(self):
|
||||||
|
global gas_baseline
|
||||||
|
|
||||||
|
if latest["t"] is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
t = now - self.t0
|
||||||
|
|
||||||
|
temp = latest["t"]
|
||||||
|
hum = latest["h"]
|
||||||
|
gas = latest["gas"]
|
||||||
|
pres = latest["p"]
|
||||||
|
alt = latest["alt"]
|
||||||
|
|
||||||
|
# Update baseline slowly
|
||||||
|
if gas_baseline is None:
|
||||||
|
gas_baseline = gas
|
||||||
|
else:
|
||||||
|
gas_baseline = (1.0 - BASELINE_ALPHA) * gas_baseline + BASELINE_ALPHA * gas
|
||||||
|
|
||||||
|
delta = abs(gas - gas_baseline)
|
||||||
|
pct = int(max(0.0, min(1.0, delta / max(1e-6, DELTA_FULL_SCALE))) * 100.0)
|
||||||
|
self.bar.setValue(pct)
|
||||||
|
|
||||||
|
if pct >= ALERT_PCT:
|
||||||
|
self.set_state("ALERT")
|
||||||
|
elif pct >= WARN_PCT:
|
||||||
|
self.set_state("WARN")
|
||||||
|
else:
|
||||||
|
self.set_state("CALM")
|
||||||
|
|
||||||
|
self.readout.setText(
|
||||||
|
f"gas: {gas:,.0f} Ω\nbaseline: {gas_baseline:,.0f} Ω\nΔ: {delta:,.0f} Ω"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Buffers
|
||||||
|
t_buf.append(t)
|
||||||
|
temp_buf.append(temp)
|
||||||
|
hum_buf.append(hum)
|
||||||
|
press_buf.append(pres)
|
||||||
|
gas_buf.append(gas)
|
||||||
|
|
||||||
|
# Trim
|
||||||
|
while t_buf and (t - t_buf[0]) > HISTORY_SECONDS:
|
||||||
|
t_buf.pop(0)
|
||||||
|
temp_buf.pop(0)
|
||||||
|
hum_buf.pop(0)
|
||||||
|
press_buf.pop(0)
|
||||||
|
gas_buf.pop(0)
|
||||||
|
|
||||||
|
# Update plots
|
||||||
|
self.curve_temp.setData(t_buf, temp_buf)
|
||||||
|
self.curve_hum.setData(t_buf, hum_buf)
|
||||||
|
self.curve_press.setData(t_buf, press_buf)
|
||||||
|
self.curve_gas.setData(t_buf, gas_buf)
|
||||||
|
|
||||||
|
if t_buf:
|
||||||
|
xmin = max(0, t_buf[-1] - HISTORY_SECONDS)
|
||||||
|
xmax = t_buf[-1]
|
||||||
|
self.plot1.setXRange(xmin, xmax)
|
||||||
|
self.plot2.setXRange(xmin, xmax)
|
||||||
|
|
||||||
|
age_ms = (now - latest["ts"]) * 1000.0 if latest["ts"] else 0.0
|
||||||
|
self.status.setText(
|
||||||
|
f"Packets: {latest['count']} Age: {age_ms:.0f} ms\n"
|
||||||
|
f"T={temp:.2f} °C RH={hum:.1f}% P={pres:.1f} hPa Gas={gas:,.0f} Ω Alt={alt:.1f} m"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.")
|
||||||
|
start_ble_thread()
|
||||||
|
|
||||||
|
app = QtWidgets.QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
|
win = MainWindow()
|
||||||
|
win.resize(1200, 720)
|
||||||
|
win.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||