Updated Enivroment Sensor Remote Control Code (Ardunio)
This commit is contained in:
parent
517af872b7
commit
5eece1a2b6
1 changed files with 645 additions and 4 deletions
|
|
@ -1,9 +1,650 @@
|
||||||
void setup() {
|
/*
|
||||||
// put your setup code here, to run once:
|
Heltec WiFi Kit32 V3 (ESP32-S3) BLE Remote for "FLL-Umbrella-Env"
|
||||||
|
|
||||||
|
What this remote does:
|
||||||
|
- Scans + connects over BLE to your sensor (server)
|
||||||
|
- Lets you switch MODE: LIVE / SIM
|
||||||
|
- In SIM mode, lets you edit: Temp (°C), RH (%), Pressure (hPa), IAQ (auto or manual)
|
||||||
|
- Writes the same binary packets your sensor expects:
|
||||||
|
CMD 0x01: [0]=0x01, [1]=mode(0/1)
|
||||||
|
CMD 0x02: [0]=0x02, then LE fields:
|
||||||
|
temp_x100 (int16), rh_x100 (uint16), press_x10 (uint16),
|
||||||
|
optional iaq (uint16)
|
||||||
|
|
||||||
|
Libraries to install (Arduino Library Manager):
|
||||||
|
- NimBLE-Arduino (by h2zero)
|
||||||
|
- U8g2 (by olikraus)
|
||||||
|
|
||||||
|
Wiring assumptions (CHANGE IF NEEDED):
|
||||||
|
- OLED I2C (0.98" SSD1306/SH1106):
|
||||||
|
SDA = GPIO17
|
||||||
|
SCL = GPIO18
|
||||||
|
RST = GPIO21 (optional; set to -1 if your display has no reset pin)
|
||||||
|
- Encoder:
|
||||||
|
A = GPIO5
|
||||||
|
B = GPIO6
|
||||||
|
BTN = GPIO7 (to GND, uses internal pullup)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- If your Heltec board’s OLED pins differ, change OLED_SDA/OLED_SCL/OLED_RST below.
|
||||||
|
- If your encoder pins differ, change ENC_A/ENC_B/ENC_BTN below.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <U8g2lib.h>
|
||||||
|
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
|
||||||
|
// ===================== BLE IDs (must match your sensor code) =====================
|
||||||
|
static const char* BLE_DEV_NAME_TARGET = "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";
|
||||||
|
|
||||||
|
// ===================== OLED (I2C) pins =====================
|
||||||
|
// Common on Heltec WiFi Kit32 V3: SDA=17, SCL=18, RST=21 (verify on your board)
|
||||||
|
static const int OLED_SDA = 17;
|
||||||
|
static const int OLED_SCL = 18;
|
||||||
|
static const int OLED_RST = 21; // set -1 if not used by your display
|
||||||
|
|
||||||
|
// If your display is SSD1306 128x64 I2C:
|
||||||
|
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ OLED_RST, /* clock=*/ OLED_SCL, /* data=*/ OLED_SDA);
|
||||||
|
|
||||||
|
// ===================== Encoder pins =====================
|
||||||
|
static const int ENC_A = 5;
|
||||||
|
static const int ENC_B = 6;
|
||||||
|
static const int ENC_BTN = 7;
|
||||||
|
|
||||||
|
// ===================== Simple encoder + button handling =====================
|
||||||
|
static volatile int32_t enc_delta = 0;
|
||||||
|
static volatile uint8_t enc_last = 0;
|
||||||
|
|
||||||
|
static uint32_t btn_down_ms = 0;
|
||||||
|
static bool btn_last = true; // pullup => idle HIGH
|
||||||
|
|
||||||
|
static inline bool readBtn() { return digitalRead(ENC_BTN) == LOW; } // pressed = LOW
|
||||||
|
|
||||||
|
IRAM_ATTR void isrEnc() {
|
||||||
|
// 2-bit Gray decoding on A/B
|
||||||
|
uint8_t a = (uint8_t)digitalRead(ENC_A);
|
||||||
|
uint8_t b = (uint8_t)digitalRead(ENC_B);
|
||||||
|
uint8_t s = (a << 1) | b;
|
||||||
|
|
||||||
|
// transition table (old->new) using 4-bit index
|
||||||
|
// produces +1/-1 per valid step, 0 otherwise
|
||||||
|
static const int8_t tbl[16] = {
|
||||||
|
0, -1, +1, 0,
|
||||||
|
+1, 0, 0, -1,
|
||||||
|
-1, 0, 0, +1,
|
||||||
|
0, +1, -1, 0
|
||||||
|
};
|
||||||
|
uint8_t idx = (enc_last << 2) | s;
|
||||||
|
enc_last = s;
|
||||||
|
enc_delta += tbl[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== App state =====================
|
||||||
|
enum Mode : uint8_t { MODE_LIVE = 0, MODE_SIM = 1 };
|
||||||
|
|
||||||
|
struct SimValues {
|
||||||
|
int16_t temp_x100 = 2500; // 25.00 C
|
||||||
|
uint16_t rh_x100 = 5000; // 50.00 %
|
||||||
|
uint16_t press_x10 = 10132; // 1013.2 hPa
|
||||||
|
uint16_t iaq = 25; // 0..500
|
||||||
|
bool iaq_manual = false; // false => omit IAQ field, sensor computes heuristic
|
||||||
|
};
|
||||||
|
|
||||||
|
static Mode mode = MODE_LIVE;
|
||||||
|
static SimValues sim;
|
||||||
|
|
||||||
|
// ===================== BLE client handles =====================
|
||||||
|
static NimBLEAdvertisedDevice* advFound = nullptr;
|
||||||
|
static NimBLEClient* client = nullptr;
|
||||||
|
static NimBLERemoteCharacteristic* chrCtrl = nullptr;
|
||||||
|
|
||||||
|
static bool bleConnected = false;
|
||||||
|
static bool bleBusy = false;
|
||||||
|
static int8_t bleRssi = 0;
|
||||||
|
|
||||||
|
// ===================== UI =====================
|
||||||
|
enum Screen {
|
||||||
|
SCR_HOME = 0,
|
||||||
|
SCR_MENU,
|
||||||
|
SCR_EDIT
|
||||||
|
};
|
||||||
|
|
||||||
|
static Screen screen = SCR_HOME;
|
||||||
|
|
||||||
|
static const char* menuItems[] = {
|
||||||
|
"Connect / Reconnect",
|
||||||
|
"Mode: LIVE/SIM",
|
||||||
|
"Temp (C)",
|
||||||
|
"RH (%)",
|
||||||
|
"Press (hPa)",
|
||||||
|
"IAQ: Auto/Manual",
|
||||||
|
"IAQ Value (0..500)",
|
||||||
|
"Send SIM now",
|
||||||
|
"Disconnect"
|
||||||
|
};
|
||||||
|
static const int MENU_COUNT = (int)(sizeof(menuItems) / sizeof(menuItems[0]));
|
||||||
|
static int menuIdx = 0;
|
||||||
|
|
||||||
|
// Edit state
|
||||||
|
enum EditField { EF_NONE, EF_MODE, EF_TEMP, EF_RH, EF_PRESS, EF_IAQ_MANUAL, EF_IAQ };
|
||||||
|
static EditField editField = EF_NONE;
|
||||||
|
static int32_t editValue = 0;
|
||||||
|
|
||||||
|
// ===================== Helpers =====================
|
||||||
|
static inline uint16_t clampU16(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;
|
||||||
|
}
|
||||||
|
static inline int16_t clampI16(int32_t v, int16_t lo, int16_t hi) {
|
||||||
|
if (v < (int32_t)lo) return lo;
|
||||||
|
if (v > (int32_t)hi) return hi;
|
||||||
|
return (int16_t)v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void oledClear() {
|
||||||
|
u8g2.clearBuffer();
|
||||||
|
u8g2.setFont(u8g2_font_6x12_tf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void oledDrawCentered(const char* s, int y) {
|
||||||
|
int w = u8g2.getStrWidth(s);
|
||||||
|
int x = (128 - w) / 2;
|
||||||
|
u8g2.drawStr(x < 0 ? 0 : x, y, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== BLE write packets =====================
|
||||||
|
static bool bleWrite(const uint8_t* data, size_t len) {
|
||||||
|
if (!bleConnected || !chrCtrl) return false;
|
||||||
|
// write without response preferred (your server supports WRITE | WRITE_NR)
|
||||||
|
return chrCtrl->writeValue(data, len, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sendMode(Mode m) {
|
||||||
|
uint8_t pkt[2] = { 0x01, (uint8_t)(m == MODE_SIM ? 1 : 0) };
|
||||||
|
return bleWrite(pkt, sizeof(pkt));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sendSim(const SimValues& v) {
|
||||||
|
uint8_t pkt[1 + 2 + 2 + 2 + 2]; // with IAQ (max)
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
pkt[len++] = 0x02;
|
||||||
|
|
||||||
|
auto putU16LE = [&](uint16_t x) {
|
||||||
|
pkt[len++] = (uint8_t)(x & 0xFF);
|
||||||
|
pkt[len++] = (uint8_t)((x >> 8) & 0xFF);
|
||||||
|
};
|
||||||
|
auto putI16LE = [&](int16_t x) { putU16LE((uint16_t)x); };
|
||||||
|
|
||||||
|
putI16LE(v.temp_x100);
|
||||||
|
putU16LE(v.rh_x100);
|
||||||
|
putU16LE(v.press_x10);
|
||||||
|
|
||||||
|
if (v.iaq_manual) {
|
||||||
|
putU16LE(v.iaq);
|
||||||
|
}
|
||||||
|
return bleWrite(pkt, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== BLE scanning/connection =====================
|
||||||
|
class AdvCB : public NimBLEAdvertisedDeviceCallbacks {
|
||||||
|
void onResult(NimBLEAdvertisedDevice* d) override {
|
||||||
|
// Match by name OR service UUID (either is fine)
|
||||||
|
if (d->haveName() && d->getName() == BLE_DEV_NAME_TARGET) {
|
||||||
|
advFound = d;
|
||||||
|
NimBLEDevice::getScan()->stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d->isAdvertisingService(NimBLEUUID(UUID_SVC_CTRL))) {
|
||||||
|
advFound = d;
|
||||||
|
NimBLEDevice::getScan()->stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void bleDisconnect() {
|
||||||
|
bleBusy = true;
|
||||||
|
if (client && client->isConnected()) client->disconnect();
|
||||||
|
chrCtrl = nullptr;
|
||||||
|
bleConnected = false;
|
||||||
|
bleBusy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool bleConnectToFound() {
|
||||||
|
if (!advFound) return false;
|
||||||
|
|
||||||
|
bleBusy = true;
|
||||||
|
|
||||||
|
if (!client) client = NimBLEDevice::createClient();
|
||||||
|
|
||||||
|
if (client->isConnected()) client->disconnect();
|
||||||
|
|
||||||
|
if (!client->connect(advFound)) {
|
||||||
|
bleBusy = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NimBLERemoteService* svc = client->getService(UUID_SVC_CTRL);
|
||||||
|
if (!svc) {
|
||||||
|
client->disconnect();
|
||||||
|
bleBusy = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrCtrl = svc->getCharacteristic(UUID_CHR_CTRL);
|
||||||
|
if (!chrCtrl) {
|
||||||
|
client->disconnect();
|
||||||
|
bleBusy = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bleConnected = true;
|
||||||
|
bleBusy = false;
|
||||||
|
|
||||||
|
// Immediately push current mode + (if SIM) sim values so remote and sensor stay aligned
|
||||||
|
sendMode(mode);
|
||||||
|
if (mode == MODE_SIM) sendSim(sim);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool bleScanAndConnect(uint32_t ms = 5000) {
|
||||||
|
bleBusy = true;
|
||||||
|
advFound = nullptr;
|
||||||
|
|
||||||
|
NimBLEScan* scan = NimBLEDevice::getScan();
|
||||||
|
scan->setAdvertisedDeviceCallbacks(new AdvCB(), true);
|
||||||
|
scan->setActiveScan(true);
|
||||||
|
scan->setInterval(45);
|
||||||
|
scan->setWindow(30);
|
||||||
|
|
||||||
|
scan->start(ms / 1000, false); // blocking in NimBLE-Arduino (seconds)
|
||||||
|
bleBusy = false;
|
||||||
|
|
||||||
|
if (!advFound) return false;
|
||||||
|
return bleConnectToFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== UI drawing =====================
|
||||||
|
static void drawHome() {
|
||||||
|
oledClear();
|
||||||
|
|
||||||
|
u8g2.setFont(u8g2_font_6x12_tf);
|
||||||
|
u8g2.drawStr(0, 12, "Env Sensor BLE Remote");
|
||||||
|
|
||||||
|
char line[32];
|
||||||
|
|
||||||
|
snprintf(line, sizeof(line), "BLE: %s", bleConnected ? "CONNECTED" : "DISCONNECTED");
|
||||||
|
u8g2.drawStr(0, 28, line);
|
||||||
|
|
||||||
|
snprintf(line, sizeof(line), "Mode: %s", (mode == MODE_SIM) ? "SIM" : "LIVE");
|
||||||
|
u8g2.drawStr(0, 40, line);
|
||||||
|
|
||||||
|
// display sim values in friendly units
|
||||||
|
float t = sim.temp_x100 / 100.0f;
|
||||||
|
float rh = sim.rh_x100 / 100.0f;
|
||||||
|
float p = sim.press_x10 / 10.0f;
|
||||||
|
|
||||||
|
snprintf(line, sizeof(line), "T %.2fC RH %.2f%%", t, rh);
|
||||||
|
u8g2.drawStr(0, 52, line);
|
||||||
|
|
||||||
|
snprintf(line, sizeof(line), "P %.1fhPa IAQ %s",
|
||||||
|
p, sim.iaq_manual ? String(sim.iaq).c_str() : "AUTO");
|
||||||
|
u8g2.drawStr(0, 64, line);
|
||||||
|
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawMenu() {
|
||||||
|
oledClear();
|
||||||
|
u8g2.drawStr(0, 12, "Menu (press=select)");
|
||||||
|
|
||||||
|
// Show 4 lines windowed
|
||||||
|
int top = menuIdx - 1;
|
||||||
|
if (top < 0) top = 0;
|
||||||
|
if (top > MENU_COUNT - 4) top = MENU_COUNT - 4;
|
||||||
|
if (top < 0) top = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int idx = top + i;
|
||||||
|
if (idx >= MENU_COUNT) break;
|
||||||
|
|
||||||
|
int y = 28 + i * 12;
|
||||||
|
if (idx == menuIdx) {
|
||||||
|
u8g2.drawBox(0, y - 10, 128, 12);
|
||||||
|
u8g2.setDrawColor(0);
|
||||||
|
u8g2.drawStr(2, y, menuItems[idx]);
|
||||||
|
u8g2.setDrawColor(1);
|
||||||
|
} else {
|
||||||
|
u8g2.drawStr(2, y, menuItems[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status footer
|
||||||
|
char s[32];
|
||||||
|
snprintf(s, sizeof(s), "%s | %s",
|
||||||
|
bleBusy ? "BUSY" : (bleConnected ? "BLE OK" : "NO BLE"),
|
||||||
|
(mode == MODE_SIM) ? "SIM" : "LIVE");
|
||||||
|
u8g2.drawStr(0, 64, s);
|
||||||
|
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawEdit() {
|
||||||
|
oledClear();
|
||||||
|
u8g2.drawStr(0, 12, "Edit (rot=chg)");
|
||||||
|
|
||||||
|
char line1[32], line2[32], line3[32];
|
||||||
|
|
||||||
|
switch (editField) {
|
||||||
|
case EF_MODE:
|
||||||
|
snprintf(line1, sizeof(line1), "Mode:");
|
||||||
|
snprintf(line2, sizeof(line2), "%s", (editValue ? "SIM" : "LIVE"));
|
||||||
|
break;
|
||||||
|
case EF_TEMP:
|
||||||
|
snprintf(line1, sizeof(line1), "Temp (C):");
|
||||||
|
snprintf(line2, sizeof(line2), "%.2f", editValue / 100.0f);
|
||||||
|
snprintf(line3, sizeof(line3), "Step: 0.25C");
|
||||||
|
break;
|
||||||
|
case EF_RH:
|
||||||
|
snprintf(line1, sizeof(line1), "RH (%):");
|
||||||
|
snprintf(line2, sizeof(line2), "%.2f", editValue / 100.0f);
|
||||||
|
snprintf(line3, sizeof(line3), "Step: 0.50%%");
|
||||||
|
break;
|
||||||
|
case EF_PRESS:
|
||||||
|
snprintf(line1, sizeof(line1), "Pressure (hPa):");
|
||||||
|
snprintf(line2, sizeof(line2), "%.1f", editValue / 10.0f);
|
||||||
|
snprintf(line3, sizeof(line3), "Step: 0.5hPa");
|
||||||
|
break;
|
||||||
|
case EF_IAQ_MANUAL:
|
||||||
|
snprintf(line1, sizeof(line1), "IAQ Mode:");
|
||||||
|
snprintf(line2, sizeof(line2), "%s", (editValue ? "MANUAL" : "AUTO"));
|
||||||
|
break;
|
||||||
|
case EF_IAQ:
|
||||||
|
snprintf(line1, sizeof(line1), "IAQ (0..500):");
|
||||||
|
snprintf(line2, sizeof(line2), "%ld", (long)editValue);
|
||||||
|
snprintf(line3, sizeof(line3), "Step: 5");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
snprintf(line1, sizeof(line1), "Unknown edit");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8g2.drawStr(0, 28, line1);
|
||||||
|
u8g2.setFont(u8g2_font_9x15_tf);
|
||||||
|
u8g2.drawStr(0, 48, line2);
|
||||||
|
u8g2.setFont(u8g2_font_6x12_tf);
|
||||||
|
if (line3[0]) u8g2.drawStr(0, 64, line3);
|
||||||
|
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Button events =====================
|
||||||
|
static bool btnShortPress = false;
|
||||||
|
static bool btnLongPress = false;
|
||||||
|
|
||||||
|
static void pollButton() {
|
||||||
|
bool pressed = readBtn();
|
||||||
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
btnShortPress = false;
|
||||||
|
btnLongPress = false;
|
||||||
|
|
||||||
|
if (pressed && btn_last == false) {
|
||||||
|
// still held
|
||||||
|
if (btn_down_ms && (now - btn_down_ms) > 600) {
|
||||||
|
// long press fire once
|
||||||
|
btnLongPress = true;
|
||||||
|
btn_down_ms = 0; // prevent repeat
|
||||||
|
}
|
||||||
|
} else if (pressed && btn_last == true) {
|
||||||
|
// just pressed
|
||||||
|
btn_down_ms = now;
|
||||||
|
} else if (!pressed && btn_last == false) {
|
||||||
|
// just released
|
||||||
|
if (btn_down_ms) {
|
||||||
|
// released before long threshold => short press
|
||||||
|
btnShortPress = true;
|
||||||
|
}
|
||||||
|
btn_down_ms = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn_last = pressed ? false : true; // track "idle high"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Encoder delta =====================
|
||||||
|
static int32_t consumeEncSteps() {
|
||||||
|
noInterrupts();
|
||||||
|
int32_t d = enc_delta;
|
||||||
|
enc_delta = 0;
|
||||||
|
interrupts();
|
||||||
|
// Many encoders generate 2/4 edges per detent; tune divisor if too sensitive.
|
||||||
|
// Keep it simple: divide by 2 to calm it down.
|
||||||
|
return d / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Menu actions =====================
|
||||||
|
static void enterEdit(EditField f, int32_t initial) {
|
||||||
|
editField = f;
|
||||||
|
editValue = initial;
|
||||||
|
screen = SCR_EDIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyEdit() {
|
||||||
|
// Apply editValue to the active field, clamp where needed.
|
||||||
|
switch (editField) {
|
||||||
|
case EF_MODE:
|
||||||
|
mode = (editValue ? MODE_SIM : MODE_LIVE);
|
||||||
|
if (bleConnected) sendMode(mode);
|
||||||
|
// If switching into SIM, push current sim immediately
|
||||||
|
if (bleConnected && mode == MODE_SIM) sendSim(sim);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EF_TEMP:
|
||||||
|
sim.temp_x100 = clampI16(editValue, (int16_t)-4000, (int16_t)8500); // -40.00..85.00
|
||||||
|
if (bleConnected && mode == MODE_SIM) sendSim(sim);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EF_RH:
|
||||||
|
sim.rh_x100 = clampU16(editValue, 0, 10000); // 0..100.00%
|
||||||
|
if (bleConnected && mode == MODE_SIM) sendSim(sim);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EF_PRESS:
|
||||||
|
sim.press_x10 = clampU16(editValue, 8000, 12000); // 800.0..1200.0 hPa
|
||||||
|
if (bleConnected && mode == MODE_SIM) sendSim(sim);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EF_IAQ_MANUAL:
|
||||||
|
sim.iaq_manual = (editValue != 0);
|
||||||
|
if (bleConnected && mode == MODE_SIM) sendSim(sim); // will include/omit IAQ field
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EF_IAQ:
|
||||||
|
sim.iaq = clampU16(editValue, 0, 500);
|
||||||
|
if (bleConnected && mode == MODE_SIM && sim.iaq_manual) sendSim(sim);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
screen = SCR_MENU;
|
||||||
|
editField = EF_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void doMenuSelect() {
|
||||||
|
switch (menuIdx) {
|
||||||
|
case 0: // Connect
|
||||||
|
bleScanAndConnect(6000);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // Mode
|
||||||
|
enterEdit(EF_MODE, (mode == MODE_SIM) ? 1 : 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // Temp
|
||||||
|
enterEdit(EF_TEMP, sim.temp_x100);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: // RH
|
||||||
|
enterEdit(EF_RH, sim.rh_x100);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4: // Pressure
|
||||||
|
enterEdit(EF_PRESS, sim.press_x10);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5: // IAQ Auto/Manual
|
||||||
|
enterEdit(EF_IAQ_MANUAL, sim.iaq_manual ? 1 : 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 6: // IAQ value
|
||||||
|
enterEdit(EF_IAQ, sim.iaq);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7: // Send SIM now
|
||||||
|
if (bleConnected) {
|
||||||
|
// force SIM mode on device if you're in SIM locally
|
||||||
|
sendMode(mode);
|
||||||
|
if (mode == MODE_SIM) sendSim(sim);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 8: // Disconnect
|
||||||
|
bleDisconnect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Setup / Loop =====================
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
// Encoder pins
|
||||||
|
pinMode(ENC_A, INPUT_PULLUP);
|
||||||
|
pinMode(ENC_B, INPUT_PULLUP);
|
||||||
|
pinMode(ENC_BTN, INPUT_PULLUP);
|
||||||
|
|
||||||
|
// Seed encoder state + attach interrupts
|
||||||
|
enc_last = ((uint8_t)digitalRead(ENC_A) << 1) | (uint8_t)digitalRead(ENC_B);
|
||||||
|
attachInterrupt(digitalPinToInterrupt(ENC_A), isrEnc, CHANGE);
|
||||||
|
attachInterrupt(digitalPinToInterrupt(ENC_B), isrEnc, CHANGE);
|
||||||
|
|
||||||
|
// OLED
|
||||||
|
u8g2.begin();
|
||||||
|
u8g2.setContrast(255);
|
||||||
|
|
||||||
|
// BLE init
|
||||||
|
NimBLEDevice::init("Env-Remote");
|
||||||
|
NimBLEDevice::setPower(ESP_PWR_LVL_P6);
|
||||||
|
|
||||||
|
screen = SCR_HOME;
|
||||||
|
|
||||||
|
oledClear();
|
||||||
|
oledDrawCentered("BLE Env Remote", 28);
|
||||||
|
oledDrawCentered("Press to open menu", 48);
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
delay(800);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// put your main code here, to run repeatedly:
|
pollButton();
|
||||||
|
int32_t steps = consumeEncSteps();
|
||||||
|
|
||||||
|
// (optional) RSSI if connected
|
||||||
|
if (bleConnected && client && client->isConnected()) {
|
||||||
|
int r = client->getRssi();
|
||||||
|
if (r != 0) bleRssi = (int8_t)r;
|
||||||
|
} else {
|
||||||
|
bleConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation logic
|
||||||
|
if (screen == SCR_HOME) {
|
||||||
|
if (btnShortPress) screen = SCR_MENU;
|
||||||
|
if (steps != 0) screen = SCR_MENU;
|
||||||
|
drawHome();
|
||||||
|
delay(50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen == SCR_MENU) {
|
||||||
|
if (steps != 0) {
|
||||||
|
menuIdx += (steps > 0) ? 1 : -1;
|
||||||
|
if (menuIdx < 0) menuIdx = 0;
|
||||||
|
if (menuIdx >= MENU_COUNT) menuIdx = MENU_COUNT - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnShortPress) {
|
||||||
|
doMenuSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnLongPress) {
|
||||||
|
screen = SCR_HOME;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawMenu();
|
||||||
|
delay(30);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen == SCR_EDIT) {
|
||||||
|
// Change edit value based on field
|
||||||
|
if (steps != 0) {
|
||||||
|
switch (editField) {
|
||||||
|
case EF_MODE:
|
||||||
|
case EF_IAQ_MANUAL:
|
||||||
|
editValue = (editValue == 0) ? 1 : 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EF_TEMP: {
|
||||||
|
// step 0.25C => 25 in x100
|
||||||
|
editValue += (steps > 0 ? 25 : -25);
|
||||||
|
editValue = clampI16(editValue, -4000, 8500);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case EF_RH: {
|
||||||
|
// step 0.50% => 50 in x100
|
||||||
|
editValue += (steps > 0 ? 50 : -50);
|
||||||
|
editValue = clampU16(editValue, 0, 10000);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case EF_PRESS: {
|
||||||
|
// step 0.5hPa => 5 in x10
|
||||||
|
editValue += (steps > 0 ? 5 : -5);
|
||||||
|
editValue = clampU16(editValue, 8000, 12000);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case EF_IAQ: {
|
||||||
|
// step 5
|
||||||
|
editValue += (steps > 0 ? 5 : -5);
|
||||||
|
editValue = clampU16(editValue, 0, 500);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnShortPress) {
|
||||||
|
applyEdit(); // accept
|
||||||
|
}
|
||||||
|
if (btnLongPress) {
|
||||||
|
// cancel
|
||||||
|
screen = SCR_MENU;
|
||||||
|
editField = EF_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEdit();
|
||||||
|
delay(30);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue