Added old project files in _Sort folders

This commit is contained in:
Jarrad Brown 2026-02-27 14:43:57 -06:00
parent 342e88b23d
commit 3bd002dead
42 changed files with 10768 additions and 0 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load diff

View 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

View 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

Binary file not shown.

106
_Sort/BLE_Acc/BLE_Acc.ino Normal file
View 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
View 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);
}
}

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

View 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
View 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
View 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 &nbsp; ay=<span id="ay"></span> g &nbsp; 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>

View 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() {}

View 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 (dont 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'
);
}

View 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
View 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")

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

View file

View 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(&#39;inicio&#39;,this)"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Start</font></font></button>
<button onclick="mostrar(&#39;sensores&#39;,this)"><font dir="auto" style="vertical-align: inherit;"><font dir="auto" style="vertical-align: inherit;">Sensors</font></font></button>
<button onclick="mostrar(&#39;graficas&#39;,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: &#39;Google Sans&#39;, 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="{&quot;mode&quot;:&quot;full&quot;,&quot;isActive&quot;:true,&quot;isUserDisabled&quot;:false}"></div></template></grammarly-desktop-integration></html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

File diff suppressed because one or more lines are too long

View 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="{&quot;mode&quot;:&quot;full&quot;,&quot;isActive&quot;:true,&quot;isUserDisabled&quot;:false}"></div></template></grammarly-desktop-integration></html>

613
_Sort/WebSite/index.html Normal file
View 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>

File diff suppressed because one or more lines are too long

View 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
View 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
View 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. Well 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
View 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
View 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
View 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
View 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
View 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; well 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 itll dominate visually; thats 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()

Binary file not shown.