Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,9 @@ node_modules/
__pycache__/
*.pyc


# Firmware related:
firmware/panorama/.cache/

# Runtime directory
rundir/
2 changes: 2 additions & 0 deletions client/include/client/argparser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
};
35 changes: 35 additions & 0 deletions client/include/client/esp32_scanner.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <string>
#include <atomic>
#include <thread>
#include <functional>

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<void(bool)> cb);

private:
void run();
bool probe(const std::string& host, int port);

std::atomic<bool> running_{false};
std::atomic<bool> available_{false};
std::thread thread_;
std::function<void(bool)> callback_;
};
7 changes: 7 additions & 0 deletions client/include/client/graph_panel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,12 @@ class GraphPanel : public wxPanel {

std::set<std::string> 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();
};
27 changes: 22 additions & 5 deletions client/include/client/mainframe.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@
#include "client/graph_panel.hpp"
#include <set>


class MessageModel;
class DataBuffer;
//class GraphPanel;
class TcpClient;
class Esp32Scanner;
class SensorDataManager;
//class SensorDataFrame; // Add this

class MainFrame : public wxFrame {
public:
Expand All @@ -37,11 +36,17 @@ 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,
ID_ESP32_AUTOSTART,
ID_ESP32_CONNECT,
ID_ESP32_DISMISS
};

MainFrame(const wxString& title, std::shared_ptr<MessageModel> model,
std::shared_ptr<DataBuffer> dataBuffer,
TcpClient* tcpClient,
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxSize(1200, 800));

Expand All @@ -65,9 +70,16 @@ class MainFrame : public wxFrame {
void OnViewConsole(wxCommandEvent& event);
void OnViewFullscreen(wxCommandEvent& event);
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<MessageModel> model_;
std::shared_ptr<DataBuffer> dataBuffer_;
TcpClient* tcpClient_;
wxTextCtrl* messageDisplay_;
wxPanel* consolePanel_;
GraphPanel* graphPanel_;
Expand All @@ -79,7 +91,12 @@ class MainFrame : public wxFrame {

void OnUpdateTimer(wxTimerEvent& event);


// Auto detecting ESP32s
std::unique_ptr<Esp32Scanner> esp32Scanner_;
wxPanel* esp32Banner_ = nullptr;
wxBoxSizer* mainSizer_ = nullptr;
std::atomic<bool> esp32BannerPending_{false};
bool esp32BannerVisible_ = false;
};

#endif // __MAINFRAME__
2 changes: 2 additions & 0 deletions client/include/client/tcp_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class TcpClient {

void start();
void stop();
void reconnectWith(const std::string& host, int port);
void sendCommand(const std::string& cmd);

private:
void run();
Expand Down
21 changes: 21 additions & 0 deletions client/include/common/panorama_colours.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <wx/colour.h>

// 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);
8 changes: 7 additions & 1 deletion client/src/argparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -27,6 +29,10 @@ bool ArgParser::isNoGuiMode() const {
return noGuiMode_;
}

bool ArgParser::isNoEspMode() const {
return noEspMode_;
}

std::string ArgParser::getRuntimeDirectory() const {
return runtimeDir_;
}
126 changes: 126 additions & 0 deletions client/src/esp32_scanner.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#include "client/esp32_scanner.hpp"
#include <chrono>

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#define CLOSE_SOCKET closesocket
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <cerrno>
#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<void(bool)> 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
}
Loading
Loading