From 6ce76071987d1894451349bb4d9eca4ef21dba82 Mon Sep 17 00:00:00 2001 From: 0r0chic0 Date: Sat, 7 Mar 2026 13:46:34 -0800 Subject: [PATCH 1/8] Server-client test for sensor data transfer --- .../index/main.cpp.621DB008C2D6724D.idx | Bin 0 -> 24602 bytes .../index/test.cpp.4729B44F633614F3.idx | Bin 0 -> 29104 bytes firmware/panorama/.clangd | 9 ++ firmware/panorama/src/main.cpp | 106 --------------- firmware/panorama/src/test.cpp | 127 ++++++++++++++++++ firmware/panorama/src/test_server.py | 54 ++++++++ 6 files changed, 190 insertions(+), 106 deletions(-) create mode 100644 firmware/panorama/.cache/clangd/index/main.cpp.621DB008C2D6724D.idx create mode 100644 firmware/panorama/.cache/clangd/index/test.cpp.4729B44F633614F3.idx create mode 100644 firmware/panorama/.clangd delete mode 100644 firmware/panorama/src/main.cpp create mode 100644 firmware/panorama/src/test.cpp create mode 100644 firmware/panorama/src/test_server.py diff --git a/firmware/panorama/.cache/clangd/index/main.cpp.621DB008C2D6724D.idx b/firmware/panorama/.cache/clangd/index/main.cpp.621DB008C2D6724D.idx new file mode 100644 index 0000000000000000000000000000000000000000..1d973c8e0cf5c728d824dc4e727d183de627503d GIT binary patch literal 24602 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 a2akeO$zzX!we{!0&2RcW_#42#pzvP@v&s1Y literal 0 HcmV?d00001 diff --git a/firmware/panorama/.cache/clangd/index/test.cpp.4729B44F633614F3.idx b/firmware/panorama/.cache/clangd/index/test.cpp.4729B44F633614F3.idx new file mode 100644 index 0000000000000000000000000000000000000000..d40dc5a727617fa217c41fc84eda2764298e846d GIT binary patch literal 29104 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 literal 0 HcmV?d00001 diff --git a/firmware/panorama/.clangd b/firmware/panorama/.clangd new file mode 100644 index 00000000..e3c57092 --- /dev/null +++ b/firmware/panorama/.clangd @@ -0,0 +1,9 @@ +# Strip ESP32/toolchain flags that clang (x86 host) doesn't support when used for IDE diagnostics. +# Use headers from the compiler in compile_commands.json so stdlib.h and other toolchain headers are found. +CompileFlags: + BuiltinHeaders: QueryDriver + Remove: + - -mlongcalls + - -mlong-calls + - -fstrict-volatile-bitfields + - -fno-tree-switch-conversion diff --git a/firmware/panorama/src/main.cpp b/firmware/panorama/src/main.cpp deleted file mode 100644 index c227be3c..00000000 --- a/firmware/panorama/src/main.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include -#include - -// ===== Wi-Fi Access Point (ESP32 hosts itself) ===== -const char* AP_SSID = "ESP32-JSON"; -const char* AP_PASS = "esp32json"; // must be 8+ chars - -// ===== TCP server ===== -const uint16_t SERVER_PORT = 9000; -WiFiServer server(SERVER_PORT); - -// ===== State ===== -WiFiClient client; // single client for simplicity -unsigned long lastSendMs = 0; // pacing JSON sends -const uint32_t SEND_PERIOD_MS = 1000; - -// make a tiny JSON line without ArduinoJson -String makeJson() { - // dummy data — tweak as you like - static int seq = 0; - float temp = 20.0 + sin(millis() / 1000.0) * 2.5; - int humidity = 40 + (millis()/1000) % 20; - - // ISO-ish time in ms since boot for now - String s = "{"; - s += "\"seq\":" + String(seq++) + ","; - s += "\"ts_ms\":" + String(millis()) + ","; - s += "\"temp_c\":" + String(temp, 2) + ","; - s += "\"humidity\":" + String(humidity) + ","; - s += "\"status\":\"ok\""; - s += "}\n"; // newline-delimited JSON (NDJSON) - return s; -} - -void setup() { - Serial.begin(115200); - delay(200); - - // Start AP (self-hosted) - Serial.println("[WiFi] Starting AP…"); - bool ok = WiFi.softAP(AP_SSID, AP_PASS); - if (!ok) { - Serial.println("[WiFi] AP start failed!"); - } - IPAddress ip = WiFi.softAPIP(); // usually 192.168.4.1 - Serial.print("[WiFi] AP SSID: "); Serial.println(AP_SSID); - Serial.print("[WiFi] AP PASS: "); Serial.println(AP_PASS); - Serial.print("[WiFi] AP IP: "); Serial.println(ip); - - // Start TCP server - server.begin(); - server.setNoDelay(true); - Serial.print("[TCP] Listening on port "); Serial.println(SERVER_PORT); -} - -void handleNewClient() { - WiFiClient incoming = server.available(); - if (!incoming) return; - - // If we already have a client, drop the older one - if (client && client.connected()) { - client.stop(); - } - - client = incoming; - client.setTimeout(50); - Serial.print("[TCP] Client connected from "); - Serial.println(client.remoteIP()); - - // optional greeting / one-shot JSON blob - client.print("{\"hello\":\"welcome\",\"port\":"); - client.print(SERVER_PORT); - client.print(",\"hint\":\"I will stream one JSON per second. Each line is a JSON object.\"}\n"); -} - -void streamJsonIfTime() { - if (!client || !client.connected()) return; - - // read & ignore any input (you could add commands here) - while (client.available()) { - (void)client.read(); // drain input - } - - unsigned long now = millis(); - if (now - lastSendMs >= SEND_PERIOD_MS) { - lastSendMs = now; - String line = makeJson(); - client.print(line); // send one JSON line - Serial.print("[TX] "); // mirror to serial - Serial.print(line); - } -} - -void loop() { - // accept new client connections - handleNewClient(); - - // send JSON periodically if a client is connected - streamJsonIfTime(); - - // clean up if disconnected - if (client && !client.connected()) { - Serial.println("[TCP] Client disconnected"); - client.stop(); - } -} diff --git a/firmware/panorama/src/test.cpp b/firmware/panorama/src/test.cpp new file mode 100644 index 00000000..68dd8184 --- /dev/null +++ b/firmware/panorama/src/test.cpp @@ -0,0 +1,127 @@ +#include +#include + +// Wi-Fi Access Point credentials +const char* SSID = "ESP32-Interface"; +const char* PASS = "12345678"; +const uint16_t PORT = 9000; + +// HC-SR04 pins (adjust to match your wiring) +const int TRIG_PIN = 5; +const int ECHO_PIN = 18; + +WiFiServer server(PORT); +WiFiClient client; + +bool sendEnabled = false; +unsigned long sampleInterval = 1000; // ms +unsigned long lastSend = 0; +unsigned long startTime = 0; + +uint32_t seq = 0; +const uint16_t SENSOR_ID = 1; +const char* SENSOR_NAME = "ultrasonic_distance_cm"; + +// Return distance in cm, or -1.0 if no echo / timeout +float readUltrasonicCm() { + // ensure trigger is low + digitalWrite(TRIG_PIN, LOW); + delayMicroseconds(2); + + // 10 µs HIGH pulse to trigger measurement + digitalWrite(TRIG_PIN, HIGH); + delayMicroseconds(10); + digitalWrite(TRIG_PIN, LOW); + + // measure echo time (timeout 30 ms ≈ 5 m) + unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000); + if (duration == 0) { + return -1.0f; // no echo + } + + // speed of sound ≈ 0.0343 cm/µs, divide by 2 (out and back) + float distanceCm = (duration * 0.0343f) / 2.0f; + return distanceCm; +} + +void setup() { + Serial.begin(115200); + + // ultrasonic sensor pins + pinMode(TRIG_PIN, OUTPUT); + pinMode(ECHO_PIN, INPUT); + digitalWrite(TRIG_PIN, LOW); + + WiFi.mode(WIFI_AP); + WiFi.softAP(SSID, PASS); + IPAddress ip = WiFi.softAPIP(); + Serial.printf("AP started: %s (%s)\n", SSID, ip.toString().c_str()); + + server.begin(); + server.setNoDelay(true); +} + +void handleCommand(String cmd) { + cmd.trim(); + cmd.toUpperCase(); + + if (cmd.startsWith("START")) { + int spaceIdx = cmd.indexOf(' '); + if (spaceIdx > 0) { + float freq = cmd.substring(spaceIdx + 1).toFloat(); + if (freq > 0) sampleInterval = 1000.0 / freq; + } + startTime = millis(); + seq = 0; // reset sequence on start + sendEnabled = true; + Serial.printf("Signal ON, freq=%.1f Hz\n", 1000.0 / sampleInterval); + + } else if (cmd.startsWith("STOP")) { + sendEnabled = false; + Serial.println("Signal OFF"); + + } else { + Serial.printf("Unknown cmd: %s\n", cmd.c_str()); + } +} + +void loop() { + // accept new client + WiFiClient newClient = server.available(); + if (newClient) { + if (client && client.connected()) client.stop(); + client = newClient; + client.print(F("{\"type\":\"status\",\"msg\":\"connected\"}\n")); + Serial.println("Backend connected"); + } + + // read commands + if (client && client.connected() && client.available()) { + String cmd = client.readStringUntil('\n'); + handleCommand(cmd); + } + + // send JSON data packets + if (sendEnabled && client && client.connected()) { + unsigned long now = millis(); + if (now - lastSend >= sampleInterval) { + lastSend = now; + + float distanceCm = readUltrasonicCm(); + unsigned long timestamp = now - startTime; + + String json = + "{" + "\"sensor\":\"" + String(SENSOR_NAME) + "\"," + "\"sensor_id\":" + String(SENSOR_ID) + "," + "\"seq\":" + String(seq++) + "," + "\"timestamp_ms\":" + String(timestamp) + "," + "\"value\":" + String(distanceCm, 2) + + "}\n"; + + client.print(json); + Serial.print("Sent: "); + Serial.print(json); + } + } +} \ No newline at end of file diff --git a/firmware/panorama/src/test_server.py b/firmware/panorama/src/test_server.py new file mode 100644 index 00000000..20267c2b --- /dev/null +++ b/firmware/panorama/src/test_server.py @@ -0,0 +1,54 @@ +import socket +import time +import json + +ESP_IP = "192.168.4.1" # ESP32 softAP IP (printed in Serial Monitor) +PORT = 9000 +RUN_SECONDS = 30 # how long to receive data + + +def main(): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((ESP_IP, PORT)) + print(f"Connected to {ESP_IP}:{PORT}") + + # read optional status line from ESP32 + try: + status = s.recv(1024).decode().strip() + if status: + print("Status from ESP32:", status) + except OSError: + pass + + # start JSON streaming at 5 Hz + s.sendall(b"START 5\n") + start = time.time() + + try: + while time.time() - start < RUN_SECONDS: + data = s.recv(1024) + if not data: + print("Connection closed by ESP32") + break + + # ESP32 sends one JSON object per line; ignore debug text (e.g. "Sensor detected at GPIO 5") + for line in data.decode().splitlines(): + line = line.strip() + if not line or not line.startswith("{"): + continue # skip non-JSON lines + try: + msg = json.loads(line) + print("JSON:", msg) + except json.JSONDecodeError: + print("Bad JSON:", line) + finally: + # tell ESP32 to stop streaming + try: + s.sendall(b"STOP\n") + except OSError: + pass + print("Sent STOP") + + +if __name__ == "__main__": + main() \ No newline at end of file From 9fab878c7d1812ffa20bc9bf1693dac867836403 Mon Sep 17 00:00:00 2001 From: 0r0chic0 Date: Sat, 14 Mar 2026 15:31:28 -0700 Subject: [PATCH 2/8] Message control for server --- firmware/panorama/src/test_server.py | 74 ++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/firmware/panorama/src/test_server.py b/firmware/panorama/src/test_server.py index 20267c2b..e77bfa14 100644 --- a/firmware/panorama/src/test_server.py +++ b/firmware/panorama/src/test_server.py @@ -1,54 +1,98 @@ import socket -import time +import sys import json +import threading ESP_IP = "192.168.4.1" # ESP32 softAP IP (printed in Serial Monitor) PORT = 9000 -RUN_SECONDS = 30 # how long to receive data + +# Commands the ESP32 understands (sent as one line, newline-terminated): +# START [freq] - start streaming at freq Hz (default 5); e.g. "START 10" +# STOP - stop streaming +# Type QUIT or EXIT to close the connection and exit. + + +def read_commands(sock, stop_event): + """Read lines from stdin and send them as commands to the ESP32.""" + try: + while not stop_event.is_set(): + line = sys.stdin.readline() + if not line: + break + line = line.strip() + if not line: + continue + if line.upper() in ("QUIT", "EXIT", "Q"): + stop_event.set() + break + # Send command to ESP32 (one line, newline-terminated) + try: + sock.sendall((line + "\n").encode()) + print(f"[sent] {line}") + except OSError as e: + print(f"[error] send failed: {e}", file=sys.stderr) + stop_event.set() + break + except (KeyboardInterrupt, EOFError): + stop_event.set() def main(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((ESP_IP, PORT)) print(f"Connected to {ESP_IP}:{PORT}") + print("Send commands: START [freq], STOP. Type QUIT to exit.\n") - # read optional status line from ESP32 + # Read optional status line from ESP32 + s.settimeout(0.5) try: status = s.recv(1024).decode().strip() if status: print("Status from ESP32:", status) + except socket.timeout: + pass except OSError: pass + s.settimeout(None) - # start JSON streaming at 5 Hz - s.sendall(b"START 5\n") - start = time.time() + stop_event = threading.Event() + cmd_thread = threading.Thread(target=read_commands, args=(s, stop_event), daemon=True) + cmd_thread.start() try: - while time.time() - start < RUN_SECONDS: - data = s.recv(1024) + buffer = "" + while not stop_event.is_set(): + try: + data = s.recv(1024) + except (OSError, socket.timeout): + continue if not data: print("Connection closed by ESP32") break - # ESP32 sends one JSON object per line; ignore debug text (e.g. "Sensor detected at GPIO 5") - for line in data.decode().splitlines(): + buffer += data.decode(errors="replace") + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) line = line.strip() - if not line or not line.startswith("{"): - continue # skip non-JSON lines + if not line: + continue + if not line.startswith("{"): + # Status or debug line + print("ESP32:", line) + continue try: msg = json.loads(line) print("JSON:", msg) except json.JSONDecodeError: print("Bad JSON:", line) finally: - # tell ESP32 to stop streaming + stop_event.set() try: s.sendall(b"STOP\n") except OSError: pass - print("Sent STOP") + print("Sent STOP; exiting.") if __name__ == "__main__": - main() \ No newline at end of file + main() From 0be9cbd7d91fafd3275c5c37c327c675520fe367 Mon Sep 17 00:00:00 2001 From: Alex Zhou Date: Sat, 14 Mar 2026 15:42:48 -0700 Subject: [PATCH 3/8] Removed firmware cache files and updated gitignore --- .gitignore | 6 +++++- .../clangd/index/main.cpp.621DB008C2D6724D.idx | Bin 24602 -> 0 bytes .../clangd/index/test.cpp.4729B44F633614F3.idx | Bin 29104 -> 0 bytes 3 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 firmware/panorama/.cache/clangd/index/main.cpp.621DB008C2D6724D.idx delete mode 100644 firmware/panorama/.cache/clangd/index/test.cpp.4729B44F633614F3.idx diff --git a/.gitignore b/.gitignore index 9aa39412..903c3c3a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,8 @@ desktop.ini node_modules/ __pycache__/ -*.pyc \ No newline at end of file +*.pyc + + +# Firmware related: +firmware/panorama/.cache/ \ No newline at end of file diff --git a/firmware/panorama/.cache/clangd/index/main.cpp.621DB008C2D6724D.idx b/firmware/panorama/.cache/clangd/index/main.cpp.621DB008C2D6724D.idx deleted file mode 100644 index 1d973c8e0cf5c728d824dc4e727d183de627503d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24602 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 a2akeO$zzX!we{!0&2RcW_#42#pzvP@v&s1Y diff --git a/firmware/panorama/.cache/clangd/index/test.cpp.4729B44F633614F3.idx b/firmware/panorama/.cache/clangd/index/test.cpp.4729B44F633614F3.idx deleted file mode 100644 index d40dc5a727617fa217c41fc84eda2764298e846d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29104 zcmd5^d3+RAwys-r0$DnN5CSn^0x}3x|-pzH_T{I~5f2$FpwWr@`36Lq;$(mi2K zlq%Y%PGvkbmCZG6zJ|t%f@x(1g?%(v*<3xBt8Hm2vL{wG0(-@TS|g%Mh9L*@EQY)q zsWWeZCPo!$f-MK@H?F33ku9yMvbojQ>T7J^L@CHe71Q?HqRM7pWkYMl1j&dL72=$# z8=Gq(gQ~VUTz&1L+WLwDNF*}BU#_XXvbCyHXan((26@|sqV*lzPX>R4BTCTdW zq0Tpln+voLm=@JGS4`+E&M(d{^@y@=c4M3cRkby(^(}1(VgnO0Cx4oPhdzSbq2uc5lWt)?I1VqfcAuBoxbcQ$Zwa~j&Xmd3W` z>RL=$$y{@7Wle1}x47BY3V-SwtLL{c9$)ozf14rchTpfKvBjSsQFt?`>0w#(M|h!w zZzQ0K!^m!@}kcwLOjss?h#Dy68n2*bx)cFUlIX%81B*SrwG1C^-Ppo3o#fL^xCD1&Il#P1H8XCRIlFa>`^*MhPl;a1`gDA8bC zib0XUd6ViZy=G7u1kKy)N~j;IqNxpTJGLf zCMdF`4qAm1AqLg@dxe0ZEzCmbm_kT1Ox$rwqOQpz-EO?s$MlYHo%c9GK+|ar$AqvZ zboi?m;HNV)0-`-Z<0F2UgIIqlC=tU^8eYP@($Ewq9a3zm&B>~oA{@)7^$Ij||Rfg@)RxrODcD5Tn22SC-C7oXB!n_%x zh=#}T+!{@z2dS#`u&_l@MC*=7urlc|AS^37k6^VqK=N4g42P|wsL&(8dZf<7Y*vM_ z9jptfH0G7i(EFh2bv_uCj+f?mQQ!ogQlCI?a*St5qRh9eFg%w9N~Xd|W{6U?Nu6}& zdRUL`V!$w?f!H9=S;rE`qB0hWqpdb2)!-d2S_r0xbcz)+P1sYSwH$y=#6qMt?Iwr@ zrG<~yh?u;WIp%W8WdO3OfWElj2F!8mjb z!-hnqWQGuu@L4TN#e*T44F1Yc_)dvaWVM49Znt4dwCd1^fmtJlNHn0xFlpDq5#Hf7 zRnCs<{7}%8VTp%Q^>s-BC{TxF(w!)E$y83*fZxPt^u%a5LPwv+qQ!Q&Xw0i)hY~ps zitG{L5EJYHgYz-Sxhg6?_V_ZWopkhnJPAaB#)$yX{c#1*OgLgjX;^+dA%ej{)+K`^a^uwSjeC^U3zyqPT<9e5Mf#i2gmDNx@1+L<5+_Ky$%WggOgUG>BBWokJ>ev^llx&75~btQLAVxcz|B#kLw53fTMWk8L+&w* z&Y+VA1A_ssJxF6P7}O;RmiYovd`&t2O6}qm#?(!XM*nmy6yCsrBYUE3;scNlwzCrh zTWZHDpdo?P*kdcn6ox775x6KGfUX|ywl}4DxsBxtMO<9=N(%9gjx56M?yQIPq zb+`oTk47Loio}PVMvU+}?0*jy90ePJ(WIe?ax{!@==HwANXbSy-{AX@u_L2!Mo-b& zX{feKj=M{^w;FM>Ggpqq@*AodajTEK<4B6kcUo!4o{zi}PAkr7m!w{`g>!0wSDk zHY0x3`csIz3gQ5qba62_S~t7ZPJ@FrKnN(~ygs!E+>SA5%o-wao*D;3a=uJ(CdJg? zRH?^`fYkfyV1H!c;S~x}w%+qeR3WY9LD}r43JQ_XR0F$ECmUbH48d@l#sP{|x&RlV zNi904N!>bQP4C8}ZVDU@U_+y9h8Pah7?r^ZPsgXsaSRGJfw7$TjSwY~THgX#K`J*} zOu-7u7;{Awln8h-UaI9Ia7l;)UQ~%s<{WfNQ5Cum8l|ytVrb58jKG!%h4OSsyv`|b zXAI#@h!PxP z7vMCo=n*AB;bA2=w#nAV5>+88l<0wdA&()M9t#8_d)jm9WeG-A0e}88KlT60inHv2 zEj@h3BE}7ub5)OZy$8oo#OS@5!fG!BL4p1RQoD57lsq^Hx7rTh!7c3|L;>bOE(;fu zg3@Y1iu9HtNCdJoK+Y@ShKR@Hp%C~BNYG6ag3sbNSilB=F4+`9APtGS0>9~!rbC!q zPXKO;cHqz847JeV!BZRn*FZW*(PaS?q`@&2h%W01$YxNM6cM=FA)Ex`2`ie~4l!L6 z9I6I3*P--UR2L*1G`pgIcM$e2wWZ{LjYL#xaM45L~15 z#O&S^Yi0pi_1hCXk`nYl6!m;*wRB9jHCmU7gB2V8wyJ7RBTUenwSZ7N)7Oxs_?DZD?t1=6p5et)X&3Ez{Bh z39Zc#C>?VP(>s}1cnSu`9-i*?daI{)0&q=B=?>uBDvR!NRDHv7ZrLd zV*$(aL%&$=U=^_l{XAd*ooAoB6@w$V$xuIq;Kk+`8qqZD|LxPETbnwB(qTFbnd)Pr`BEn>(Id zxMyL3n>lLbF)Dr3m>w2C3C9E5_C@Q@f4s!a{Ah8wQe7Y^eiDuy_uO>b;tO6Vbu-^v z9Es_PBYRl#gU(opS+&E z5+z)Tl9?ogn9JD>JdK)=Gs%IZu{ao&%bKd~+{{{wBPlrv z^oO5>_{VI+#2C4yA8ru%Z4qBvtcr0*%D929bhJIxuyA!+PIg@sSHDGToAg#~kbH|OfhgB!3UXsbaGhFj$ zZrhQBfGw3tO(ykv&*X|nuh@)I5a`<+ZH^=Sr0ub%^lx#%7S0Y&A#9u1J%sK&aC$lm z>4j`z5-~s^i`b$#`L{X3m<8SyIYS_QdCKzPVPufHLdJ*U~@x@|I}>un}j5HDGsPpaEkd448UdN5Rp{ zhA*u@R4_np#PQ);u)90Rtu(DPu67{Hr<8k0i6%@ua%S@*pO@g0F7qraAfR1)r@i?@ z-KJ8U-kIH*OVWw+#4_7mCS;qMk(x!UePf1x(>sR_l_D3^+KAtjBNJRfR>Jm@{z#G2`d|Ud(=1 zv%h8-eC}nv!-ytP!L+WW$Jt975jh|MfcW590$^hi=B5Ok6gQOV}lG zlnY-Qwe^Erp9&0Mb%$nLaQ-f~F*pF*nz4CPhu%C!#%xU~O<OAXlWUh?~chP1_gU z@bZtc3(|AgoHU|yAamJVJ2{>mZzqe{;xwX{f>*b%-~F$DuEZX=D6=S==x6$s)mn{v*>GK^)%jG6yashc4q6L zMQaBr0ztzTf(IPr^OUc4?tJwxcU_D?D%?Ahs-O4B6(8?7{NSb7)a$%;C8TybQVtKF zcF{AJVz}~(@+zWXAX}!kOe1Xfc04ojrH`K2jHShmoxr`mCD-+tnS43URv06kPQ?1^ zraN~1ap4PF2Pg&cCAbya$3cKHgPmb-XfxTFc5+GflDNOx`_jT6cNT8B6W3-5TarkM z0Awj!YA4G^l#L?4ev+=aW*@}%-~k`82jO8p2@aIA-vmN@xgt+Y9FZS{Rv z4<)H3@!{kjDoaK)4`)1p^txStDm<+t>t`VD~wZ#7MmP z1G7F>pFlDJ(k?lMu#I7Wv^#Sk?On#+8xGa`&r8{jtXB6M9<@k%6P=tNMmedG+;=A`G0CJh$I-1J;KIX->7oh(c)wD-Bi>BXao zNWZ)1xifl3Uhx`+t{YvKM{4Bg2C+K-=`-HIin2QmGHAyQ!9G+68RlfpnLs`Vvc9ao z&PJ|0W#wd2gKwSI+u0Al+gSC%0C2PG+Lx}r z&h-V(wsdHzn|!{ey>##MJF5SU)5{CXr<3#pldmd$>!HI(aC*tml6dXwEyfsq|MG8f zXP29uJC3*}ASZez77+66F=yWU_~qNbbAjfUCN51UN(0h9Z2{6g^#Ia7n|M0(){IM@ zO8g#IANE(oe{ma9i!)oE{T|EaP4dPq@W`)kUs|5~`cZ7>%d(fn+ZvF8(SbZ71CXIf zq4@klmlPh04eJudWc;!LCUWH8`o8xe;ETnqNqeduUdL`=*Rw)cR61e4djuUpU!x&k zqOZ_lbO8MueSkhjZ=)~Jhvv81+oHv1-;C};ccXjIz34u4KYAWLi=IO_ zqV4D=6l|B~u2eee{(;^=C1(Z1veK84HlLgKSHz#QG&}qddJsK;PWdl1?lJTfTF_7* zS+x(nhW?IjMa4N|$9qrB&C8#3`s68R{OZi9p3^2ynD#gH5-KPv+>IVZRn>DEUqDR@ zn_HIo_n;@ypV5DyU1-HKNLGK3u0*e(tI$?-HM$o45#54rL${+l(N6R*dK^80{(@da zucH4%zNgWW9q119ClvY}5^g|y(d%e=`RvLW6|-he|8;FmbQ8K5U5>V(OVFk0GIRyn wj8=D@ckTt~x4$`mE$Tt*(0X(c+K4uwPSemYMBQtyL1VK18}#}w#D0VR56^a;MF0Q* From 1cf27b63079a832c0fbe1eddb05b337881a4c6ac Mon Sep 17 00:00:00 2001 From: Alex Zhou Date: Sat, 21 Mar 2026 13:49:27 -0700 Subject: [PATCH 4/8] Add ESP32 AP TCP connection --- client/include/client/mainframe.hpp | 3 +++ client/include/client/tcp_client.hpp | 1 + client/src/json_reader.cpp | 20 ++++++++------------ client/src/main.cpp | 12 ++++++++++-- client/src/mainframe.cpp | 16 ++++++++++++++-- client/src/settings_dialog.cpp | 2 +- client/src/tcp_client.cpp | 12 +++++++----- firmware/panorama/src/test.cpp | 27 +++++++++++++++++++++++++-- 8 files changed, 69 insertions(+), 24 deletions(-) diff --git a/client/include/client/mainframe.hpp b/client/include/client/mainframe.hpp index d1998de8..65af6e97 100644 --- a/client/include/client/mainframe.hpp +++ b/client/include/client/mainframe.hpp @@ -19,6 +19,7 @@ class MessageModel; class DataBuffer; +class TcpClient; //class GraphPanel; class SensorDataManager; //class SensorDataFrame; // Add this @@ -40,6 +41,7 @@ class MainFrame : public wxFrame { MainFrame(const wxString& title, std::shared_ptr model, std::shared_ptr dataBuffer, + TcpClient* tcpClient, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(1200, 800)); @@ -66,6 +68,7 @@ class MainFrame : public wxFrame { std::shared_ptr model_; std::shared_ptr dataBuffer_; + TcpClient* tcpClient_; wxTextCtrl* messageDisplay_; wxPanel* consolePanel_; diff --git a/client/include/client/tcp_client.hpp b/client/include/client/tcp_client.hpp index 26b72557..981d76c4 100644 --- a/client/include/client/tcp_client.hpp +++ b/client/include/client/tcp_client.hpp @@ -17,6 +17,7 @@ class TcpClient { void start(); void stop(); + void reconnectWith(const std::string& host, int port); private: void run(); diff --git a/client/src/json_reader.cpp b/client/src/json_reader.cpp index 40fb9672..19b12e55 100644 --- a/client/src/json_reader.cpp +++ b/client/src/json_reader.cpp @@ -58,19 +58,15 @@ buffer_data_t JsonReader::exportToBuffer(std::string json) { return ret; } - std::string sensorTypeString ( - doc["sensor"].GetString(), - doc["sensor"].GetStringLength() - ); - std::string sensorUnitString ( - doc["unit"].GetString(), - doc["unit"].GetStringLength() - ); - double sensorValue = doc["value"].GetDouble(); + if (!doc.HasMember("sensor") || !doc["sensor"].IsString() || + !doc.HasMember("unit") || !doc["unit"].IsString() || + !doc.HasMember("value") || !doc["value"].IsNumber()) { + return ret; + } - ret.datatype = sensorTypeString.c_str(); - ret.data = sensorValue; - ret.dataunit = sensorUnitString.c_str(); + ret.datatype = std::string(doc["sensor"].GetString(), doc["sensor"].GetStringLength()); + ret.data = doc["value"].GetDouble(); + ret.dataunit = std::string(doc["unit"].GetString(), doc["unit"].GetStringLength()); ret.timestamp = std::time(&ret.timestamp); return ret; diff --git a/client/src/main.cpp b/client/src/main.cpp index 4025d35f..c429d35c 100644 --- a/client/src/main.cpp +++ b/client/src/main.cpp @@ -135,7 +135,15 @@ class PanoramaClient : public wxApp { dataBuffer_ = std::make_shared(runtimeDir + "/data"); // --- Create and start TCP client on separate thread --- - tcpClient_ = std::make_unique("127.0.0.1", 3000, model_, dataBuffer_, dataLogger_); + std::string tcpHost; + int tcpPort; + bool autoReconnect; + int reconnectDelay; + if (!config.getTcpSettings(tcpHost, tcpPort, autoReconnect, reconnectDelay)) { + tcpHost = "127.0.0.1"; + tcpPort = 3000; + } + tcpClient_ = std::make_unique(tcpHost, tcpPort, model_, dataBuffer_, dataLogger_); tcpClient_->start(); // For running without a gui @@ -150,7 +158,7 @@ class PanoramaClient : public wxApp { } // --- Create view --- - MainFrame* w = new MainFrame("Panorama Client", model_, dataBuffer_); + MainFrame* w = new MainFrame("Panorama Client", model_, dataBuffer_, tcpClient_.get()); w->Show(); diff --git a/client/src/mainframe.cpp b/client/src/mainframe.cpp index c636fe7a..878dfd81 100644 --- a/client/src/mainframe.cpp +++ b/client/src/mainframe.cpp @@ -6,14 +6,17 @@ #include "client/sensor_manager.hpp" #include "client/sensor.hpp" #include "client/settings_dialog.hpp" +#include "client/tcp_client.hpp" +#include "client/config_manager.hpp" #include #include #include MainFrame::MainFrame(const wxString& title, std::shared_ptr model, std::shared_ptr dataBuffer, + TcpClient* tcpClient, const wxPoint& pos, const wxSize& size) - : wxFrame(nullptr, wxID_ANY, title, pos, size), model_(model), dataBuffer_(dataBuffer) { + : wxFrame(nullptr, wxID_ANY, title, pos, size), model_(model), dataBuffer_(dataBuffer), tcpClient_(tcpClient) { CreateMenuBar(); @@ -233,5 +236,14 @@ void MainFrame::OnViewFullscreen(wxCommandEvent& event) { void MainFrame::OnSettingsOpen(wxCommandEvent& event) { SettingsDialog dialog(this); - dialog.ShowModal(); + if (dialog.ShowModal() == wxID_OK && tcpClient_) { + ConfigManager& config = ConfigManager::getInstance(); + std::string host; + int port; + bool autoReconnect; + int reconnectDelay; + if (config.getTcpSettings(host, port, autoReconnect, reconnectDelay)) { + tcpClient_->reconnectWith(host, port); + } + } } diff --git a/client/src/settings_dialog.cpp b/client/src/settings_dialog.cpp index f7893917..2b2e37f0 100644 --- a/client/src/settings_dialog.cpp +++ b/client/src/settings_dialog.cpp @@ -5,7 +5,7 @@ #include SettingsDialog::SettingsDialog(wxWindow* parent) - : wxDialog(parent, wxID_ANY, "Settings", wxDefaultPosition, wxSize(500, 300)) { + : wxDialog(parent, wxID_ANY, "Settings", wxDefaultPosition, wxSize(500, 400)) { wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); diff --git a/client/src/tcp_client.cpp b/client/src/tcp_client.cpp index df5d45c4..ed854db9 100644 --- a/client/src/tcp_client.cpp +++ b/client/src/tcp_client.cpp @@ -92,11 +92,6 @@ void TcpClient::run() { logger_->logJsonData(received); } - // print json - //pinfo("Received JSON: ", received); - reader.exportToBuffer(received); - - // Parse JSON and write to DataBuffer buffer_data_t parsedData = reader.exportToBuffer(received); dataBuffer_->writeData(parsedData); @@ -106,6 +101,13 @@ void TcpClient::run() { } } +void TcpClient::reconnectWith(const std::string& host, int port) { + host_ = host; + port_ = port; + // Force a disconnectso the run loop reconnects with new settings + cleanup(); +} + void TcpClient::reconnect() { cleanup(); diff --git a/firmware/panorama/src/test.cpp b/firmware/panorama/src/test.cpp index 68dd8184..e55cfccb 100644 --- a/firmware/panorama/src/test.cpp +++ b/firmware/panorama/src/test.cpp @@ -6,9 +6,13 @@ const char* SSID = "ESP32-Interface"; const char* PASS = "12345678"; const uint16_t PORT = 9000; +// Onboard LED pin +const int LED_PIN = 2; + // HC-SR04 pins (adjust to match your wiring) const int TRIG_PIN = 5; const int ECHO_PIN = 18; +const unsigned long BLINK_DELAY_MS = 250; WiFiServer server(PORT); WiFiClient client; @@ -17,6 +21,8 @@ bool sendEnabled = false; unsigned long sampleInterval = 1000; // ms unsigned long lastSend = 0; unsigned long startTime = 0; +unsigned long lastBlink = 0; +bool ledState = false; uint32_t seq = 0; const uint16_t SENSOR_ID = 1; @@ -47,6 +53,10 @@ float readUltrasonicCm() { void setup() { Serial.begin(115200); + // onboard LED + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + // ultrasonic sensor pins pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT); @@ -95,6 +105,18 @@ void loop() { Serial.println("Backend connected"); } + // update onboard LED: solid when connected, blink when disconnected + if (client && client.connected()) { + digitalWrite(LED_PIN, HIGH); + } else { + unsigned long now = millis(); + if (now - lastBlink >= BLINK_DELAY_MS) { + lastBlink = now; + ledState = !ledState; + digitalWrite(LED_PIN, ledState ? HIGH : LOW); + } + } + // read commands if (client && client.connected() && client.available()) { String cmd = client.readStringUntil('\n'); @@ -113,10 +135,11 @@ void loop() { String json = "{" "\"sensor\":\"" + String(SENSOR_NAME) + "\"," + "\"unit\":\"cm\"," + "\"value\":" + String(distanceCm, 2) + "," "\"sensor_id\":" + String(SENSOR_ID) + "," "\"seq\":" + String(seq++) + "," - "\"timestamp_ms\":" + String(timestamp) + "," - "\"value\":" + String(distanceCm, 2) + + "\"timestamp_ms\":" + String(timestamp) + "}\n"; client.print(json); From 391ec998ab8961a84a701aac9c01c3792ec34486 Mon Sep 17 00:00:00 2001 From: Alex Zhou Date: Sat, 21 Mar 2026 14:40:05 -0700 Subject: [PATCH 5/8] Firware-Backend connection and transmission bringup --- client/include/client/mainframe.hpp | 6 ++- client/include/client/tcp_client.hpp | 1 + client/src/mainframe.cpp | 31 +++++++++++++--- client/src/tcp_client.cpp | 10 +++++ firmware/panorama/src/test.cpp | 55 +++++++++++++--------------- tools/pserver/pserver.py | 52 ++++++++++++++++++++++++-- tools/pserver/pstream_json.py | 8 ++-- 7 files changed, 120 insertions(+), 43 deletions(-) diff --git a/client/include/client/mainframe.hpp b/client/include/client/mainframe.hpp index 03d6bb8c..19be0170 100644 --- a/client/include/client/mainframe.hpp +++ b/client/include/client/mainframe.hpp @@ -36,7 +36,9 @@ class MainFrame : public wxFrame { ID_EDIT_PREFERENCES, ID_VIEW_CONSOLE, ID_VIEW_FULLSCREEN, - ID_SETTINGS_OPEN + ID_SETTINGS_OPEN, + ID_BTN_START, + ID_BTN_STOP }; MainFrame(const wxString& title, std::shared_ptr model, @@ -65,6 +67,8 @@ class MainFrame : public wxFrame { void OnViewConsole(wxCommandEvent& event); void OnViewFullscreen(wxCommandEvent& event); void OnSettingsOpen(wxCommandEvent& event); + void OnStartStream(wxCommandEvent& event); + void OnStopStream(wxCommandEvent& event); std::shared_ptr model_; std::shared_ptr dataBuffer_; diff --git a/client/include/client/tcp_client.hpp b/client/include/client/tcp_client.hpp index 981d76c4..e6b6a744 100644 --- a/client/include/client/tcp_client.hpp +++ b/client/include/client/tcp_client.hpp @@ -18,6 +18,7 @@ class TcpClient { void start(); void stop(); void reconnectWith(const std::string& host, int port); + void sendCommand(const std::string& cmd); private: void run(); diff --git a/client/src/mainframe.cpp b/client/src/mainframe.cpp index bb5b286f..48155f9a 100644 --- a/client/src/mainframe.cpp +++ b/client/src/mainframe.cpp @@ -24,6 +24,21 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, updateTimer_.Bind(wxEVT_TIMER, &MainFrame::OnUpdateTimer, this); updateTimer_.Start(10); // milliseconds between GUI refreshes + // Control bar with start and stop btns + wxPanel* controlBar = new wxPanel(this); + wxBoxSizer* controlSizer = new wxBoxSizer(wxHORIZONTAL); + wxButton* startBtn = new wxButton(controlBar, ID_BTN_START, wxString::FromUTF8("\u25B6 Start")); + startBtn->SetBackgroundColour(wxColour(76, 175, 80)); + startBtn->SetForegroundColour(*wxWHITE); + wxButton* stopBtn = new wxButton(controlBar, ID_BTN_STOP, wxString::FromUTF8("\u25A0 Stop")); + stopBtn->SetBackgroundColour(wxColour(244, 67, 54)); + stopBtn->SetForegroundColour(*wxWHITE); + controlSizer->Add(startBtn, 0, wxALL, 4); + controlSizer->Add(stopBtn, 0, wxALL, 4); + controlBar->SetSizer(controlSizer); + + Bind(wxEVT_BUTTON, &MainFrame::OnStartStream, this, ID_BTN_START); + Bind(wxEVT_BUTTON, &MainFrame::OnStopStream, this, ID_BTN_STOP); // Create splitter for layout wxSplitterWindow* mainSplitter = new wxSplitterWindow(this, wxID_ANY); @@ -79,6 +94,7 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, // Layout wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); + mainSizer->Add(controlBar, 0, wxEXPAND); mainSizer->Add(mainSplitter, 1, wxEXPAND); SetSizer(mainSizer); @@ -253,10 +269,15 @@ void MainFrame::OnSettingsOpen(wxCommandEvent& event) { } } -void MainFrame::OnUpdateTimer(wxTimerEvent&) { - if (updatePending_.exchange(false)) { - updateMessageDisplay(); - updateDataPanel(); +void MainFrame::OnStartStream(wxCommandEvent& event) { + if (tcpClient_) { + tcpClient_->sendCommand("START"); + } +} + +void MainFrame::OnStopStream(wxCommandEvent& event) { + if (tcpClient_) { + tcpClient_->sendCommand("STOP"); } } @@ -265,4 +286,4 @@ void MainFrame::OnUpdateTimer(wxTimerEvent&) { updateMessageDisplay(); updateDataPanel(); } -} +} \ No newline at end of file diff --git a/client/src/tcp_client.cpp b/client/src/tcp_client.cpp index ed854db9..039c39a6 100644 --- a/client/src/tcp_client.cpp +++ b/client/src/tcp_client.cpp @@ -101,6 +101,16 @@ void TcpClient::run() { } } +void TcpClient::sendCommand(const std::string& cmd) { + if (socket_ != INVALID_SOCKET) { + std::string msg = cmd + "\n"; + send(socket_, msg.c_str(), msg.size(), 0); + model_->addMessage("Sent command: " + cmd); + } else { + model_->addMessage("Error: Not connected, cannot send command"); + } +} + void TcpClient::reconnectWith(const std::string& host, int port) { host_ = host; port_ = port; diff --git a/firmware/panorama/src/test.cpp b/firmware/panorama/src/test.cpp index e55cfccb..e2d9e8b5 100644 --- a/firmware/panorama/src/test.cpp +++ b/firmware/panorama/src/test.cpp @@ -25,30 +25,23 @@ unsigned long lastBlink = 0; bool ledState = false; uint32_t seq = 0; -const uint16_t SENSOR_ID = 1; -const char* SENSOR_NAME = "ultrasonic_distance_cm"; -// Return distance in cm, or -1.0 if no echo / timeout -float readUltrasonicCm() { - // ensure trigger is low - digitalWrite(TRIG_PIN, LOW); - delayMicroseconds(2); - - // 10 µs HIGH pulse to trigger measurement - digitalWrite(TRIG_PIN, HIGH); - delayMicroseconds(10); - digitalWrite(TRIG_PIN, LOW); - - // measure echo time (timeout 30 ms ≈ 5 m) - unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000); - if (duration == 0) { - return -1.0f; // no echo - } - - // speed of sound ≈ 0.0343 cm/µs, divide by 2 (out and back) - float distanceCm = (duration * 0.0343f) / 2.0f; - return distanceCm; -} +struct SensorConfig { + const char* sensor; + const char* dataunit; + const char* datatype; + uint16_t sensorID; + float baseValue; + float variation; +}; + +const SensorConfig sensors[] = { + {"TM1000", "K", "temperature", 1, 25.5f, 0.5f}, + {"HD1000", "watercontent", "humidity", 2, 60.2f, 0.3f}, + {"PP1000", "hPa", "pressure", 3, 1013.25f, 0.1f}, + {"NM1000", "nm", "light", 4, 0.2f, 0.0f}, +}; +const int NUM_SENSORS = sizeof(sensors) / sizeof(sensors[0]); void setup() { Serial.begin(115200); @@ -123,23 +116,25 @@ void loop() { handleCommand(cmd); } - // send JSON data packets if (sendEnabled && client && client.connected()) { unsigned long now = millis(); if (now - lastSend >= sampleInterval) { lastSend = now; - float distanceCm = readUltrasonicCm(); + int idx = seq % NUM_SENSORS; + const SensorConfig& s = sensors[idx]; + float value = s.baseValue + (seq % 10) * s.variation; unsigned long timestamp = now - startTime; String json = "{" - "\"sensor\":\"" + String(SENSOR_NAME) + "\"," - "\"unit\":\"cm\"," - "\"value\":" + String(distanceCm, 2) + "," - "\"sensor_id\":" + String(SENSOR_ID) + "," + "\"sensor\":\"" + String(s.sensor) + "\"," + "\"dataunit\":\"" + String(s.dataunit) + "\"," + "\"data\":" + String(value, 2) + "," + "\"datatype\":\"" + String(s.datatype) + "\"," + "\"sensorID\":" + String(s.sensorID) + "," "\"seq\":" + String(seq++) + "," - "\"timestamp_ms\":" + String(timestamp) + + "\"timestamp\":" + String(timestamp) + "}\n"; client.print(json); diff --git a/tools/pserver/pserver.py b/tools/pserver/pserver.py index 3bf80d83..6eaafe77 100644 --- a/tools/pserver/pserver.py +++ b/tools/pserver/pserver.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import socket import sys +import threading from putils import * from pstreamer import PStreamer from pstream_json import PStreamJSON @@ -8,10 +9,42 @@ class PServer: def __init__(self): self.verbose = 0 + self._streaming = False + + def _read_commands(self, client_socket, stop_event): + """Note: This runs in a separate thread""" + buffer = "" + try: + while not stop_event.is_set(): + try: + data = client_socket.recv(1024) + except OSError: + break + if not data: + break + buffer += data.decode(errors="replace") + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.strip().upper() + if not line: + continue + if line.startswith("START"): + pinfo("PServer", "Received START command") + self._streaming = True + elif line.startswith("STOP"): + pinfo("PServer", "Received STOP command") + self._streaming = False + else: + pinfo("PServer", f"Unknown command: {line}") + except Exception as e: + pinfo("PServer", f"Command reader error: {e}") + finally: + stop_event.set() def send_stream(self, client_socket, client_address, streamer: PStreamer): """ Send data from the streamer to the connected client. + Waits for a START command before sending data. Args: client_socket: Socket connected to the client @@ -19,17 +52,29 @@ def send_stream(self, client_socket, client_address, streamer: PStreamer): streamer: PStreamer instance providing data """ print(f"[PServer] Client connected from {client_address}") + pinfo("PServer", "Waiting for START command...") + + stop_event = threading.Event() + cmd_thread = threading.Thread( + target=self._read_commands, + args=(client_socket, stop_event), + daemon=True + ) + cmd_thread.start() try: counter = 0 - while True: + while not stop_event.is_set(): + if not self._streaming: + stop_event.wait(timeout=0.1) + continue + data = streamer.get_data(timeout=2.0) if data is None: pwarning("PServer", "No data available from streamer") continue - + client_socket.sendall(data) - #pinfo("PServer", f"Sent: {data.decode('utf-8').strip()}") counter += 1 except (BrokenPipeError, ConnectionResetError) as e: @@ -37,6 +82,7 @@ def send_stream(self, client_socket, client_address, streamer: PStreamer): except KeyboardInterrupt: pinfo("PServer", "Connection interrupted") finally: + stop_event.set() client_socket.close() def main(): diff --git a/tools/pserver/pstream_json.py b/tools/pserver/pstream_json.py index 739fa897..774dca13 100644 --- a/tools/pserver/pstream_json.py +++ b/tools/pserver/pstream_json.py @@ -15,7 +15,7 @@ def __init__(self): "timestamp": 123032032, "dataunit": "K", "datatype": "temperature", - "sensor:": "TM1000", + "sensor": "TM1000", "sensorID": 1 }, { @@ -23,7 +23,7 @@ def __init__(self): "timestamp": 123032032, "dataunit": "watercontent", "datatype": "humidity", - "sensor:": "HD1000", + "sensor": "HD1000", "sensorID": 2 }, { @@ -31,7 +31,7 @@ def __init__(self): "timestamp": 123032032, "dataunit": "hPa", "datatype": "pressure", - "sensor:": "PP1000", + "sensor": "PP1000", "sensorID": 3 }, { @@ -39,7 +39,7 @@ def __init__(self): "timestamp": 123032032, "dataunit": "nm", "datatype": "light", - "sensor:": "NM1000", + "sensor": "NM1000", "sensorID": 4 } ] From 5f28a32447781743af443118cb16e861aa5265a2 Mon Sep 17 00:00:00 2001 From: Alex Zhou Date: Sat, 21 Mar 2026 15:19:18 -0700 Subject: [PATCH 6/8] Added ESP32 scanning ability --- client/include/client/esp32_scanner.hpp | 35 ++++++ client/include/client/mainframe.hpp | 20 +++- client/include/common/panorama_colours.hpp | 21 ++++ client/src/esp32_scanner.cpp | 126 +++++++++++++++++++++ client/src/graph_panel.cpp | 9 +- client/src/mainframe.cpp | 95 ++++++++++++++-- client/src/sensor_data_panel.cpp | 7 +- client/src/sensor_manager.cpp | 3 +- client/src/settings_dialog.cpp | 2 +- 9 files changed, 295 insertions(+), 23 deletions(-) create mode 100644 client/include/client/esp32_scanner.hpp create mode 100644 client/include/common/panorama_colours.hpp create mode 100644 client/src/esp32_scanner.cpp diff --git a/client/include/client/esp32_scanner.hpp b/client/include/client/esp32_scanner.hpp new file mode 100644 index 00000000..b7ed7046 --- /dev/null +++ b/client/include/client/esp32_scanner.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +class Esp32Scanner { +public: + static constexpr const char* ESP32_DEFAULT_HOST = "192.168.4.1"; + static constexpr int ESP32_DEFAULT_PORT = 9000; + static constexpr int SCAN_INTERVAL_MS = 5000; + static constexpr int CONNECT_TIMEOUT_MS = 500; + + Esp32Scanner(); + ~Esp32Scanner(); + + void start(); + void stop(); + + bool isAvailable() const { return available_.load(); } + + // Callback is fired on the scanner thread when the availability changes. + // The bool parameter is true when the ESP32 becomes reachable. + void setOnAvailabilityChanged(std::function cb); + +private: + void run(); + bool probe(const std::string& host, int port); + + std::atomic running_{false}; + std::atomic available_{false}; + std::thread thread_; + std::function callback_; +}; diff --git a/client/include/client/mainframe.hpp b/client/include/client/mainframe.hpp index 19be0170..3d7d1fce 100644 --- a/client/include/client/mainframe.hpp +++ b/client/include/client/mainframe.hpp @@ -16,13 +16,11 @@ #include "client/sensor.hpp" #include - class MessageModel; class DataBuffer; class TcpClient; -//class GraphPanel; +class Esp32Scanner; class SensorDataManager; -//class SensorDataFrame; // Add this class MainFrame : public wxFrame { public: @@ -38,7 +36,10 @@ class MainFrame : public wxFrame { ID_VIEW_FULLSCREEN, ID_SETTINGS_OPEN, ID_BTN_START, - ID_BTN_STOP + ID_BTN_STOP, + ID_ESP32_AUTOSTART, + ID_ESP32_CONNECT, + ID_ESP32_DISMISS }; MainFrame(const wxString& title, std::shared_ptr model, @@ -69,6 +70,10 @@ class MainFrame : public wxFrame { void OnSettingsOpen(wxCommandEvent& event); void OnStartStream(wxCommandEvent& event); void OnStopStream(wxCommandEvent& event); + void OnEsp32Autostart(wxCommandEvent& event); + void OnEsp32Connect(wxCommandEvent& event); + void OnEsp32Dismiss(wxCommandEvent& event); + void ShowEsp32Banner(bool show); std::shared_ptr model_; std::shared_ptr dataBuffer_; @@ -83,7 +88,12 @@ class MainFrame : public wxFrame { void OnUpdateTimer(wxTimerEvent& event); - + // Auto detecting ESP32s + std::unique_ptr esp32Scanner_; + wxPanel* esp32Banner_ = nullptr; + wxBoxSizer* mainSizer_ = nullptr; + std::atomic esp32BannerPending_{false}; + bool esp32BannerVisible_ = false; }; #endif // __MAINFRAME__ \ No newline at end of file diff --git a/client/include/common/panorama_colours.hpp b/client/include/common/panorama_colours.hpp new file mode 100644 index 00000000..db4de32d --- /dev/null +++ b/client/include/common/panorama_colours.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +// Panorama colour palette +const wxColour PCOLOUR_WHITE(255, 255, 255); +const wxColour PCOLOUR_BLACK(0, 0, 0); + +const wxColour PCOLOUR_DARK_GREY(40, 40, 40); +const wxColour PCOLOUR_GREY(90, 90, 90); +const wxColour PCOLOUR_MID_GREY(150, 150, 150); +const wxColour PCOLOUR_LIGHT_GREY(220, 220, 220); +const wxColour PCOLOUR_PANEL_GREY(240, 240, 240); + +const wxColour PCOLOUR_GREEN(76, 175, 80); +const wxColour PCOLOUR_RED(244, 67, 54); +const wxColour PCOLOUR_BLUE(33, 150, 243); + +const wxColour PCOLOUR_LIGHT_RED(255, 200, 200); +const wxColour PCOLOUR_LIGHT_YELLOW(255, 255, 200); +const wxColour PCOLOUR_LIGHT_GREEN(200, 255, 200); diff --git a/client/src/esp32_scanner.cpp b/client/src/esp32_scanner.cpp new file mode 100644 index 00000000..d50139f2 --- /dev/null +++ b/client/src/esp32_scanner.cpp @@ -0,0 +1,126 @@ +#include "client/esp32_scanner.hpp" +#include + +#ifdef _WIN32 + #include + #include + #define CLOSE_SOCKET closesocket +#else + #include + #include + #include + #include + #include + #include + #include + #define CLOSE_SOCKET close + #define INVALID_SOCKET -1 +#endif + +Esp32Scanner::Esp32Scanner() {} + +Esp32Scanner::~Esp32Scanner() { + stop(); +} + +void Esp32Scanner::start() { + if (running_.load()) return; + running_ = true; + thread_ = std::thread(&Esp32Scanner::run, this); +} + +void Esp32Scanner::stop() { + running_ = false; + if (thread_.joinable()) { + thread_.join(); + } +} + +void Esp32Scanner::setOnAvailabilityChanged(std::function cb) { + callback_ = std::move(cb); +} + +void Esp32Scanner::run() { + while (running_) { + bool reachable = probe(ESP32_DEFAULT_HOST, ESP32_DEFAULT_PORT); + bool prev = available_.exchange(reachable); + + if (reachable != prev && callback_) { + callback_(reachable); + } + + for (int i = 0; i < SCAN_INTERVAL_MS / 100 && running_; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); // prevent stop() blocking for too long + } + } +} + +bool Esp32Scanner::probe(const std::string& host, int port) { +#ifdef _WIN32 + SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock == INVALID_SOCKET) return false; + + // Set this as non-blocking + u_long mode = 1; + ioctlsocket(sock, FIONBIO, &mode); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = inet_addr(host.c_str()); + + connect(sock, (sockaddr*)&addr, sizeof(addr)); + + fd_set writefds; + FD_ZERO(&writefds); + FD_SET(sock, &writefds); + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = CONNECT_TIMEOUT_MS * 1000; + + int result = select(0, nullptr, &writefds, nullptr, &tv); + closesocket(sock); + return result > 0; +#else + int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock == INVALID_SOCKET) return false; + + // Set this as non-blocking + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + inet_pton(AF_INET, host.c_str(), &addr.sin_addr); + + int ret = connect(sock, (sockaddr*)&addr, sizeof(addr)); + if (ret == 0) { + // Connected immediately + CLOSE_SOCKET(sock); + return true; + } + + if (errno != EINPROGRESS) { + CLOSE_SOCKET(sock); + return false; + } + + // Wait for connection, with timeout + struct pollfd pfd; + pfd.fd = sock; + pfd.events = POLLOUT; + int pollResult = poll(&pfd, 1, CONNECT_TIMEOUT_MS); + + bool connected = false; + if (pollResult > 0 && (pfd.revents & POLLOUT)) { + int err = 0; + socklen_t len = sizeof(err); + getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &len); + connected = (err == 0); + } + + CLOSE_SOCKET(sock); + return connected; +#endif +} diff --git a/client/src/graph_panel.cpp b/client/src/graph_panel.cpp index d4b73d65..f494b7fa 100644 --- a/client/src/graph_panel.cpp +++ b/client/src/graph_panel.cpp @@ -1,4 +1,5 @@ #include "client/graph_panel.hpp" +#include "common/panorama_colours.hpp" #include #include @@ -10,7 +11,7 @@ wxEND_EVENT_TABLE() GraphPanel::GraphPanel(wxWindow* parent) : wxPanel(parent, wxID_ANY) { - SetBackgroundColour(wxColour(255, 255, 255)); + SetBackgroundColour(PCOLOUR_WHITE); SetBackgroundStyle(wxBG_STYLE_PAINT); // double buffering so smoother } @@ -36,7 +37,7 @@ void GraphPanel::OnSize(wxSizeEvent& event) { void GraphPanel::DrawBackground(wxDC& dc) { wxSize size = GetClientSize(); - dc.SetBrush(wxBrush(wxColour(220, 220, 220))); + dc.SetBrush(wxBrush(PCOLOUR_LIGHT_GREY)); dc.SetPen(*wxTRANSPARENT_PEN); @@ -53,7 +54,7 @@ void GraphPanel::DrawGrid(wxDC& dc) { int graphHeight = size.GetHeight() - topMargin - bottomMargin; int graphWidth = size.GetWidth() - leftMargin - 20; - dc.SetPen(wxPen(wxColour(90, 90, 90), 1)); + dc.SetPen(wxPen(PCOLOUR_GREY, 1)); // horizontal grid lines for (int i = 0; i <= 4; i++) { @@ -78,7 +79,7 @@ void GraphPanel::DrawAxes(wxDC& dc){ int graphHeight = size.GetHeight() - topMargin - bottomMargin; int graphWidth = size.GetWidth() - leftMargin - 20; - dc.SetPen(wxPen(wxColour(0, 0, 0), 2)); + dc.SetPen(wxPen(PCOLOUR_BLACK, 2)); // draw y-axis dc.DrawLine(leftMargin, topMargin, leftMargin, topMargin + graphHeight); diff --git a/client/src/mainframe.cpp b/client/src/mainframe.cpp index 48155f9a..8a24e9ed 100644 --- a/client/src/mainframe.cpp +++ b/client/src/mainframe.cpp @@ -7,8 +7,10 @@ #include "client/sensor.hpp" #include "client/settings_dialog.hpp" #include "common/panorama_utils.hpp" +#include "common/panorama_colours.hpp" #include "client/tcp_client.hpp" #include "client/config_manager.hpp" +#include "client/esp32_scanner.hpp" #include #include #include @@ -28,10 +30,10 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, wxPanel* controlBar = new wxPanel(this); wxBoxSizer* controlSizer = new wxBoxSizer(wxHORIZONTAL); wxButton* startBtn = new wxButton(controlBar, ID_BTN_START, wxString::FromUTF8("\u25B6 Start")); - startBtn->SetBackgroundColour(wxColour(76, 175, 80)); + startBtn->SetBackgroundColour(PCOLOUR_GREEN); startBtn->SetForegroundColour(*wxWHITE); wxButton* stopBtn = new wxButton(controlBar, ID_BTN_STOP, wxString::FromUTF8("\u25A0 Stop")); - stopBtn->SetBackgroundColour(wxColour(244, 67, 54)); + stopBtn->SetBackgroundColour(PCOLOUR_RED); stopBtn->SetForegroundColour(*wxWHITE); controlSizer->Add(startBtn, 0, wxALL, 4); controlSizer->Add(stopBtn, 0, wxALL, 4); @@ -40,6 +42,27 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, Bind(wxEVT_BUTTON, &MainFrame::OnStartStream, this, ID_BTN_START); Bind(wxEVT_BUTTON, &MainFrame::OnStopStream, this, ID_BTN_STOP); + // ESP32 notification banner (hidden by default) + esp32Banner_ = new wxPanel(this); + esp32Banner_->SetBackgroundColour(PCOLOUR_BLUE); + wxBoxSizer* bannerSizer = new wxBoxSizer(wxHORIZONTAL); + wxStaticText* bannerLabel = new wxStaticText(esp32Banner_, wxID_ANY, + wxString::FromUTF8(" ESP32 device detected on network (192.168.4.1:9000)")); + bannerLabel->SetForegroundColour(*wxWHITE); + wxButton* autostartBtn = new wxButton(esp32Banner_, ID_ESP32_AUTOSTART, "Connect && Autostart"); + wxButton* connectBtn = new wxButton(esp32Banner_, ID_ESP32_CONNECT, "Connect"); + wxButton* dismissBtn = new wxButton(esp32Banner_, ID_ESP32_DISMISS, "Dismiss"); + bannerSizer->Add(bannerLabel, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, 5); + bannerSizer->Add(autostartBtn, 0, wxALL, 3); + bannerSizer->Add(connectBtn, 0, wxALL, 3); + bannerSizer->Add(dismissBtn, 0, wxALL, 3); + esp32Banner_->SetSizer(bannerSizer); + esp32Banner_->Hide(); + + Bind(wxEVT_BUTTON, &MainFrame::OnEsp32Autostart, this, ID_ESP32_AUTOSTART); + Bind(wxEVT_BUTTON, &MainFrame::OnEsp32Connect, this, ID_ESP32_CONNECT); + Bind(wxEVT_BUTTON, &MainFrame::OnEsp32Dismiss, this, ID_ESP32_DISMISS); + // Create splitter for layout wxSplitterWindow* mainSplitter = new wxSplitterWindow(this, wxID_ANY); wxSplitterWindow* topSplitter = new wxSplitterWindow(mainSplitter, wxID_ANY); @@ -50,7 +73,7 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, // Data view area - Sensor Data Panel (rows added dynamically as sensors arrive) wxPanel* dataViewPanel = new wxPanel(rightSplitter); - dataViewPanel->SetBackgroundColour(wxColour(240, 240, 240)); + dataViewPanel->SetBackgroundColour(PCOLOUR_PANEL_GREY); sensorDataGrid = new SensorDataFrame(dataViewPanel, wxArrayString()); @@ -68,8 +91,8 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, messageDisplay_ = new wxTextCtrl(consolePanel_, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY | wxTE_WORDWRAP); - messageDisplay_->SetBackgroundColour(wxColour(40, 40, 40)); - messageDisplay_->SetForegroundColour(wxColour(255, 255, 255)); + messageDisplay_->SetBackgroundColour(PCOLOUR_DARK_GREY); + messageDisplay_->SetForegroundColour(PCOLOUR_WHITE); wxStaticText* consoleLabel = new wxStaticText(consolePanel_, wxID_ANY, "Console"); @@ -93,10 +116,11 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, mainSplitter->SetSashGravity(1.0); // keeps console constant at 150px // Layout - wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); - mainSizer->Add(controlBar, 0, wxEXPAND); - mainSizer->Add(mainSplitter, 1, wxEXPAND); - SetSizer(mainSizer); + mainSizer_ = new wxBoxSizer(wxVERTICAL); + mainSizer_->Add(controlBar, 0, wxEXPAND); + mainSizer_->Add(esp32Banner_, 0, wxEXPAND); + mainSizer_->Add(mainSplitter, 1, wxEXPAND); + SetSizer(mainSizer_); // Register as observer model_->addObserver(std::bind(&MainFrame::onModelUpdated, this)); @@ -105,6 +129,15 @@ MainFrame::MainFrame(const wxString& title, std::shared_ptr model, sensorManager_->SetOnSensorToggled(std::bind(&MainFrame::onSensorToggled, this)); CreateStatusBar(); + + // Start ESP32 auto-detection scanner + esp32Scanner_ = std::make_unique(); + esp32Scanner_->setOnAvailabilityChanged([this](bool available) { + if (available) { + esp32BannerPending_.store(true); + } + }); + esp32Scanner_->start(); } void MainFrame::onModelUpdated() { @@ -286,4 +319,48 @@ void MainFrame::OnUpdateTimer(wxTimerEvent&) { updateMessageDisplay(); updateDataPanel(); } + + // Makesure the esp is actually detected + if (esp32BannerPending_.exchange(false)) { + ShowEsp32Banner(true); + } +} + +void MainFrame::ShowEsp32Banner(bool show) { + if (esp32BannerVisible_ == show) return; + esp32BannerVisible_ = show; + esp32Banner_->Show(show); + mainSizer_->Layout(); +} + +void MainFrame::OnEsp32Autostart(wxCommandEvent& event) { + if (tcpClient_) { + tcpClient_->reconnectWith( + Esp32Scanner::ESP32_DEFAULT_HOST, + Esp32Scanner::ESP32_DEFAULT_PORT); + model_->addMessage("Connecting to ESP32 at 192.168.4.1:9000 (autostart)..."); + tcpClient_->sendCommand("START"); + } + ShowEsp32Banner(false); + if (esp32Scanner_) { + esp32Scanner_->stop(); + } +} + +void MainFrame::OnEsp32Connect(wxCommandEvent& event) { + if (tcpClient_) { + tcpClient_->reconnectWith( + Esp32Scanner::ESP32_DEFAULT_HOST, + Esp32Scanner::ESP32_DEFAULT_PORT); + model_->addMessage("Connecting to ESP32 at 192.168.4.1:9000..."); + } + ShowEsp32Banner(false); + + if (esp32Scanner_) { + esp32Scanner_->stop(); + } +} + +void MainFrame::OnEsp32Dismiss(wxCommandEvent& event) { + ShowEsp32Banner(false); } \ No newline at end of file diff --git a/client/src/sensor_data_panel.cpp b/client/src/sensor_data_panel.cpp index acc9250c..b78303c7 100644 --- a/client/src/sensor_data_panel.cpp +++ b/client/src/sensor_data_panel.cpp @@ -1,4 +1,5 @@ #include "client/sensor_data_panel.h" +#include "common/panorama_colours.hpp" #include SensorDataFrame::SensorDataFrame(wxWindow* parent, const wxArrayString& sensorNames) @@ -111,11 +112,11 @@ void SensorDataFrame::UpdateReading(const std::string& sensorName, double value, // Color if (value > 50.0) { - grid_->SetCellBackgroundColour(row, 1, wxColour(255, 200, 200)); // Light red + grid_->SetCellBackgroundColour(row, 1, PCOLOUR_LIGHT_RED); } else if (value > 25.0) { - grid_->SetCellBackgroundColour(row, 1, wxColour(255, 255, 200)); // Light yellow + grid_->SetCellBackgroundColour(row, 1, PCOLOUR_LIGHT_YELLOW); } else { - grid_->SetCellBackgroundColour(row, 1, wxColour(200, 255, 200)); // Light green + grid_->SetCellBackgroundColour(row, 1, PCOLOUR_LIGHT_GREEN); } grid_->ForceRefresh(); diff --git a/client/src/sensor_manager.cpp b/client/src/sensor_manager.cpp index 85ec05e6..b712aa13 100644 --- a/client/src/sensor_manager.cpp +++ b/client/src/sensor_manager.cpp @@ -1,11 +1,12 @@ #include #include #include "client/settings_dialog.hpp" +#include "common/panorama_colours.hpp" SensorManagerPanel::SensorManagerPanel(wxWindow* parent) : wxPanel(parent, wxID_ANY) { - SetBackgroundColour(wxColour(150, 150, 150)); + SetBackgroundColour(PCOLOUR_MID_GREY); sizer_ = new wxBoxSizer(wxVERTICAL); diff --git a/client/src/settings_dialog.cpp b/client/src/settings_dialog.cpp index 2b2e37f0..bb732643 100644 --- a/client/src/settings_dialog.cpp +++ b/client/src/settings_dialog.cpp @@ -118,7 +118,7 @@ void SettingsDialog::saveSettings() { void SettingsDialog::OnSave(wxCommandEvent& event) { saveSettings(); - wxMessageBox("Settings saved successfully.\n\nNote: Some settings may require an application restart to take effect.", + wxMessageBox("Settings saved successfully.\n\nTCP connection settings will take effect immediately.", "Settings Saved", wxOK | wxICON_INFORMATION); EndModal(wxID_OK); } From fc7f08527dafbddb4388f6cd33b1fe681cd70102 Mon Sep 17 00:00:00 2001 From: Alex Zhou Date: Sat, 21 Mar 2026 15:27:52 -0700 Subject: [PATCH 7/8] Added default to localhost when using -noesp --- client/include/client/argparser.hpp | 2 ++ client/src/argparser.cpp | 8 +++++++- client/src/main.cpp | 5 ++++- scripts/run.sh | 3 +++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/include/client/argparser.hpp b/client/include/client/argparser.hpp index 98b0b0fc..ff3eed31 100644 --- a/client/include/client/argparser.hpp +++ b/client/include/client/argparser.hpp @@ -8,10 +8,12 @@ class ArgParser { bool isTestMode() const; bool isNoGuiMode() const; + bool isNoEspMode() const; std::string getRuntimeDirectory() const; private: bool testMode_; bool noGuiMode_; + bool noEspMode_; std::string runtimeDir_; }; diff --git a/client/src/argparser.cpp b/client/src/argparser.cpp index aaeaaa98..d1a5ef2c 100644 --- a/client/src/argparser.cpp +++ b/client/src/argparser.cpp @@ -2,16 +2,18 @@ #include "wx/cmdline.h" ArgParser::ArgParser(int argc, char** argv) - : testMode_(false), noGuiMode_(false), runtimeDir_("") + : testMode_(false), noGuiMode_(false), noEspMode_(false), runtimeDir_("") { wxCmdLineParser parser(argc, argv); parser.AddSwitch("t", "test", "test mode"); parser.AddSwitch("n", "nogui", "run without GUI (console mode)"); + parser.AddSwitch("e", "noesp", "run without ESP32 (use localhost TCP)"); parser.AddOption("r", "runtime-dir", "runtime directory for data and config", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL); parser.Parse(false); // don't exit on errors testMode_ = parser.Found("test"); noGuiMode_ = parser.Found("nogui"); + noEspMode_ = parser.Found("noesp"); wxString runtimeDirWx; if (parser.Found("runtime-dir", &runtimeDirWx)) { @@ -27,6 +29,10 @@ bool ArgParser::isNoGuiMode() const { return noGuiMode_; } +bool ArgParser::isNoEspMode() const { + return noEspMode_; +} + std::string ArgParser::getRuntimeDirectory() const { return runtimeDir_; } \ No newline at end of file diff --git a/client/src/main.cpp b/client/src/main.cpp index ccb4fd7e..7f536bd6 100644 --- a/client/src/main.cpp +++ b/client/src/main.cpp @@ -140,7 +140,10 @@ class PanoramaClient : public wxApp { int tcpPort; bool autoReconnect; int reconnectDelay; - if (!config.getTcpSettings(tcpHost, tcpPort, autoReconnect, reconnectDelay)) { + if (parser.isNoEspMode()) { + tcpHost = "127.0.0.1"; + tcpPort = 3000; + } else if (!config.getTcpSettings(tcpHost, tcpPort, autoReconnect, reconnectDelay)) { tcpHost = "127.0.0.1"; tcpPort = 3000; } diff --git a/scripts/run.sh b/scripts/run.sh index 5b1235ec..e3b9a94a 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -75,6 +75,9 @@ fi # Build command with flags CMD="$BIN" +if [[ "$NOESP" == true ]]; then + CMD="$CMD --noesp" +fi if [[ "$NOGUI" == true ]]; then CMD="$CMD --nogui" fi From 7a770667a08cb97f5abf9d193e84a19cce291a33 Mon Sep 17 00:00:00 2001 From: Alex Zhou Date: Sat, 21 Mar 2026 15:42:29 -0700 Subject: [PATCH 8/8] Fixed merge conflict issues --- client/include/client/graph_panel.hpp | 7 +++ client/src/graph_panel.cpp | 65 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/client/include/client/graph_panel.hpp b/client/include/client/graph_panel.hpp index e29f9bcd..e49f20eb 100644 --- a/client/include/client/graph_panel.hpp +++ b/client/include/client/graph_panel.hpp @@ -22,5 +22,12 @@ class GraphPanel : public wxPanel { std::set visibleSensors_; + void OnPaint(wxPaintEvent& event); + void OnSize(wxSizeEvent& event); + void DrawBackground(wxDC& dc); + void DrawGrid(wxDC& dc); + void DrawAxes(wxDC& dc); void UpdateGraph(); + + wxDECLARE_EVENT_TABLE(); }; \ No newline at end of file diff --git a/client/src/graph_panel.cpp b/client/src/graph_panel.cpp index ead4cd3a..82153b91 100644 --- a/client/src/graph_panel.cpp +++ b/client/src/graph_panel.cpp @@ -1,5 +1,6 @@ #include "client/graph_panel.hpp" #include "common/panorama_colours.hpp" +#include #include #include @@ -64,6 +65,70 @@ void GraphPanel::DrawBackground(wxDC& dc) { dc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight()); } +// Function: to draw grid lines +void GraphPanel::DrawGrid(wxDC& dc) { + wxSize size = GetClientSize(); + + int leftMargin = 60; + int bottomMargin = 40; + int topMargin = 20; + int graphHeight = size.GetHeight() - topMargin - bottomMargin; + int graphWidth = size.GetWidth() - leftMargin - 20; + + dc.SetPen(wxPen(PCOLOUR_GREY, 1)); + + // horizontal grid lines + for (int i = 0; i <= 4; i++) { + int y = topMargin + (graphHeight * i / 4); + dc.DrawLine(leftMargin, y, leftMargin + graphWidth, y); + } + + // vertical grid lines + for (int i = 0; i <=6; i++) { + int x = leftMargin + (graphWidth * i / 6); + dc.DrawLine(x, topMargin, x, topMargin + graphHeight); + } +} + + +void GraphPanel::DrawAxes(wxDC& dc){ + wxSize size = GetClientSize(); + + int leftMargin = 60; + int bottomMargin = 40; + int topMargin = 20; + int graphHeight = size.GetHeight() - topMargin - bottomMargin; + int graphWidth = size.GetWidth() - leftMargin - 20; + + dc.SetPen(wxPen(PCOLOUR_BLACK, 2)); + + // draw y-axis + dc.DrawLine(leftMargin, topMargin, leftMargin, topMargin + graphHeight); + + // draw x-axis + dc.DrawLine(leftMargin, topMargin + graphHeight, leftMargin + graphWidth, topMargin + graphHeight); + + // + //x axis label + for (int i = 0; i < 6; i++) { + int x = leftMargin + (graphWidth * i / 6); + int value = i; + wxString label = wxString::Format("%d", value); + wxSize textSize = dc.GetTextExtent(label); + dc.DrawText(label, x - textSize.GetWidth() / 2, size.GetHeight() - bottomMargin + 5); + } + + //y axis label + for (int i = 0; i <= 4; i++) { + int y = (topMargin + graphHeight) - (graphHeight * i / 4); + int value = i * 25; + wxString label = wxString::Format("%d", value); + wxSize textSize = dc.GetTextExtent(label); + dc.DrawText(label, leftMargin - textSize.GetWidth() - 5, y - textSize.GetHeight() / 2); + } + +} + void GraphPanel::AddDataPoint(const std::string& sensorName, double value, double timestamp){ sensorData_[sensorName].push_back({timestamp, value});