diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index a25c9ec5..a65b54e1 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -27,6 +27,7 @@ set(CLIENT_HEADERS include/client/message_model.hpp include/client/tcp_client.hpp include/client/json_reader.hpp + include/client/buffer_base.hpp include/client/DataBuffer.hpp include/client/sensor_manager.hpp include/client/sensor_data_panel.h diff --git a/client/include/client/DataBuffer.hpp b/client/include/client/DataBuffer.hpp index b4e1ded4..e05935fb 100644 --- a/client/include/client/DataBuffer.hpp +++ b/client/include/client/DataBuffer.hpp @@ -1,11 +1,13 @@ -#pragma once +#ifndef __DATABUFFER__ +#define __DATABUFFER__ #include #include #include #include "common/panorama_defines.hpp" +#include "client/buffer_base.hpp" #include -class DataBuffer { +class DataBuffer : public BufferBase { public: DataBuffer(); ~DataBuffer(); @@ -35,6 +37,7 @@ class DataBuffer { // Clear the buffer void clear(); + // Extract the next complete JSON object from buffer // Removes extracted portion from buffer std::string extractNextJson(); @@ -47,7 +50,7 @@ class DataBuffer { // Parse the next complete JSON object in the buffer. // Returns success/failure depending on whether parsing succeeded. // The parsed result can be returned as a variant, struct, or any user-defined type. - bool parseNextJson(/* ParsedData &out */); + std::string parseNextJson(/* ParsedData &out */); // Parse *all* complete JSON objects currently in the buffer. // Useful if the buffer contains multiple messages. @@ -57,6 +60,8 @@ class DataBuffer { std::string toStringAll(); + void exportBuffer(); + private: // Raw buffer storing incoming data std::list buffer_; @@ -78,3 +83,4 @@ class DataBuffer { // This keeps parsing logic isolated from buffer logic bool decodeJson(const std::string& jsonStr /*, ParsedData &out */); }; +#endif // __DATABUFFER__ \ No newline at end of file diff --git a/client/include/client/buffer_base.hpp b/client/include/client/buffer_base.hpp new file mode 100644 index 00000000..d50ea151 --- /dev/null +++ b/client/include/client/buffer_base.hpp @@ -0,0 +1,102 @@ +// #pragma once +#include +#include +#include + +/** + * @brief Base class for buffers with templated data type + * @tparam T The type of data stored in the buffer + * + * This template class provides thread-safe buffer operations + * (e.g., writes, reads ...) + * that can be customized by child classes + * to work with any data type. + */ +template +class BufferBase { +public: + BufferBase() = default; + virtual ~BufferBase() = default; + + /** + * @brief Write data to the buffer + * @param data The data to append to the buffer + */ + virtual void write(T data) { + std::lock_guard lock(mutex_); + buffer_.push_back(std::move(data)); + + } + + /** + * @brief Replace the entire buffer with new data + * @param data The data to set as the new buffer content + */ + virtual void setData(T data) { + std::lock_guard lock(mutex_); + buffer_.clear(); + buffer_.push_back(std::move(data)); + } + + /** + * @brief Read all data from the buffer without clearing it + * @return A copy of all data in the buffer + */ + virtual std::list readAll() const { + std::lock_guard lock(mutex_); + return buffer_; + } + + /** + * @brief Read and clear all data from the buffer + * @return All data that was in the buffer + */ + virtual std::list consume() { + std::lock_guard lock(mutex_); + std::list result = std::move(buffer_); + buffer_.clear(); + return result; + } + + // Returns the first element of the buffer and removes it from the buffer + virtual T extractNextBuffer() { + buffer_data_t ret = buffer_.front(); + buffer_.pop_front(); + return ret; + } + + virtual void popFront() { + buffer_.pop_front(); + } + + /** + * @brief Get the number of elements in the buffer + * @return The number of elements + */ + virtual size_t size() const { + std::lock_guard lock(mutex_); + return buffer_.size(); + } + + /** + * @brief Check if the buffer is empty + * @return true if the buffer is empty, false otherwise + */ + virtual bool empty() const { + std::lock_guard lock(mutex_); + return buffer_.empty(); + } + + /** + * @brief Clear all data from the buffer + */ + virtual void clear() { + std::lock_guard lock(mutex_); + buffer_.clear(); + } + +protected: + std::list buffer_; + mutable std::mutex mutex_; + int MAX_BUFFER_SIZE = 5; +}; diff --git a/client/include/client/mainframe.hpp b/client/include/client/mainframe.hpp index f18bc5b1..d8d5711a 100644 --- a/client/include/client/mainframe.hpp +++ b/client/include/client/mainframe.hpp @@ -1,4 +1,5 @@ -#pragma once +#ifndef __MAINFRAME__ +#define __MAINFRAME__ #include #include #include @@ -13,6 +14,7 @@ class MessageModel; +class DataBuffer; //class GraphPanel; class SensorDataManager; //class SensorDataFrame; // Add this @@ -20,6 +22,7 @@ class SensorDataManager; class MainFrame : public wxFrame { public: MainFrame(const wxString& title, std::shared_ptr model, + std::shared_ptr dataBuffer, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(1200, 800)); @@ -27,6 +30,9 @@ class MainFrame : public wxFrame { void updateMessageDisplay(); std::shared_ptr model_; + std::shared_ptr dataBuffer_; wxTextCtrl* messageDisplay_; }; + +#endif // __MAINFRAME__ \ No newline at end of file diff --git a/client/include/client/tcp_client.hpp b/client/include/client/tcp_client.hpp index 7ff26090..26b72557 100644 --- a/client/include/client/tcp_client.hpp +++ b/client/include/client/tcp_client.hpp @@ -1,15 +1,18 @@ -#pragma once +#ifndef __TCP_CLIENT__ +#define __TCP_CLIENT__ + #include #include #include #include class MessageModel; +class DataBuffer; class DataLogger; class TcpClient { public: - TcpClient(const std::string& host, int port, std::shared_ptr model, std::shared_ptr logger = nullptr); + TcpClient(const std::string& host, int port, std::shared_ptr model, std::shared_ptr dataBuffer, std::shared_ptr logger = nullptr); ~TcpClient(); void start(); @@ -24,12 +27,15 @@ class TcpClient { int port_; std::shared_ptr model_; std::shared_ptr logger_; + std::shared_ptr dataBuffer_; std::atomic running_; std::thread clientThread_; - + #ifdef _WIN32 unsigned long long socket_; // SOCKET type on Windows #else int socket_; #endif -}; \ No newline at end of file +}; + +#endif \ No newline at end of file diff --git a/client/include/common/panorama_defines.hpp b/client/include/common/panorama_defines.hpp index c333dc23..82fa6b44 100644 --- a/client/include/common/panorama_defines.hpp +++ b/client/include/common/panorama_defines.hpp @@ -1,6 +1,10 @@ +#pragma once +#include + typedef struct { - char a; - float a_data; - char b; - float b_data; + float data; // actual value + std::time_t timestamp; // date recorded + const char * dataunit; // e.g. "kPa", "mL" + const char * datatype; // e.g. "temperature", "sound" + } buffer_data_t; \ No newline at end of file diff --git a/client/include/common/panorama_utils.hpp b/client/include/common/panorama_utils.hpp index 59a1e38d..e681404a 100644 --- a/client/include/common/panorama_utils.hpp +++ b/client/include/common/panorama_utils.hpp @@ -10,8 +10,8 @@ // #endif -#ifndef PANORAMA_UTILS_HPP -#define PANORAMA_UTILS_HPP +#ifndef __PANORAMA_UTILS_HPP__ +#define __PANORAMA_UTILS_HPP__ #include @@ -22,4 +22,11 @@ void pinfo(const Args&... args) { std::cout << std::endl; } -#endif // PANORAMA_UTILS_HPP +template +void pdebug(const Args&... args) { + std::cout << "[DEBUG][Client]\t"; + (std::cout << ... << args); // fold expression over operator<< + std::cout << std::endl; +} + +#endif // __PANORAMA_UTILS_HPP__ diff --git a/client/src/DataBuffer.cpp b/client/src/DataBuffer.cpp index 0c5693ce..a5409ae5 100644 --- a/client/src/DataBuffer.cpp +++ b/client/src/DataBuffer.cpp @@ -1,4 +1,7 @@ + #include "client/DataBuffer.hpp" +#include +#include DataBuffer::DataBuffer() { // TODO: any initialization if needed @@ -8,33 +11,59 @@ DataBuffer::~DataBuffer() { } void DataBuffer::writeData(buffer_data_t jsonChunk) { - // Append the new chunk of raw JSON data to the buffer. This functions only job is to store raw + // Append the new chunk of raw JSON data to the buffer. This functions only job is to store raw // inbound data in a way that doesn't lose anything later the parser will call extractNextJson() // parseNextJson() - buffer_.push_back(jsonChunk); + // std::cout << "[DataBuffer] Writing data to buffer:" << std::endl; + // std::cout << "[DataBuffer] a: '" << jsonChunk.a << "' a_data: " << jsonChunk.a_data << std::endl; + // std::cout << "[DataBuffer] b: '" << jsonChunk.b << "' b_data: " << jsonChunk.b_data << std::endl; + // std::cout << "[DataBuffer] Buffer size before write: " << size() << std::endl; + + write(jsonChunk); + if (size() > MAX_BUFFER_SIZE) { + popFront(); + exportBuffer(); + } + // std::cout << "[DataBuffer] Buffer size after write: " << size() << std::endl; + // std::cout << buffer_.size(); + // std::cout << "[DataBuffer] : " << toStringAll() << std::endl; } void DataBuffer::setData(buffer_data_t jsonData) { // Overwrite the entire buffer with new raw JSON data - buffer_.clear(); - buffer_.push_front(jsonData); + BufferBase::setData(jsonData); } std::list DataBuffer::readAll() const { // TODO: return raw buffer as is - return buffer_; + return BufferBase::readAll(); } std::list DataBuffer::consume() { // TODO: copy the raw buffer then clear the buffer and return copied content - std::list temp = buffer_; + return BufferBase::consume(); } + +size_t DataBuffer::size() const { + return BufferBase::size(); +} + +void DataBuffer::clear() { + BufferBase::clear(); +} + +bool DataBuffer::hasCompleteJson() const { + // For now, just check if buffer is not empty + // In a more sophisticated implementation, this would check for complete JSON objects + return !buffer_.empty(); +} + std::string DataBuffer::extractNextJson() { // TODO: // 1. Use findJsonBoundary() to locate a complete JSON object // 2. Extract it from buffer_ // 3. Remove extracted substring from buffer_ - return {}; + return toString(extractNextBuffer()); } size_t DataBuffer::findJsonBoundary() const { @@ -58,13 +87,13 @@ bool DataBuffer::decodeJson(const std::string& jsonStr /*, ParsedData &out */) { return false; } -bool DataBuffer::parseNextJson(/* ParsedData &out */) { +std::string DataBuffer::parseNextJson(/* ParsedData &out */) { // TODO: // 1. Use extractNextJson() to get next complete object // 2. Validate using isValidJson() // 3. Decode using decodeJson() // 4. Return true if parsed successfully - return false; + return ""; } void DataBuffer::parseAll(/* std::vector &out */) { @@ -74,12 +103,23 @@ void DataBuffer::parseAll(/* std::vector &out */) { // parseNextJson(...) } + std::string DataBuffer::toString(const buffer_data_t& buffer_item) { //convert one struct of buffer_ into string + bool hasUnit = buffer_item.dataunit != nullptr && buffer_item.dataunit[0] != '\0'; + std::string temp = "{"; - temp = temp + "\"" + buffer_item.a + "\": " + std::to_string(buffer_item.a_data) + ", "; - temp = temp + "\"" + buffer_item.b + "\": " + std::to_string(buffer_item.b_data); + + temp = temp + "\"datatype\": \"" + buffer_item.datatype + "\", \"data\": " + std::to_string(buffer_item.data) + ", "; + + if (hasUnit) { + temp = temp + "\"dataunit\": \"" + buffer_item.dataunit + "\", "; + } + + temp = temp + "\"timestamp\": " + std::to_string(buffer_item.timestamp); + temp += "}"; + return temp; } @@ -87,9 +127,31 @@ std::string DataBuffer::toStringAll() { //print all buffer_ as string //buffer_ is an array of buffer_data_t std::string res = ""; - - for (buffer_data_t buffer_item : buffer_) { - res += toString(buffer_item) + ",\n"; + int c = 0; + for (buffer_data_t buffer_item : readAll()) { + if (c == size() - 1) { + res += toString(buffer_item) + "\n"; + } else { + res += toString(buffer_item) + ",\n"; + } + + c++; } + return res; } + +void DataBuffer::exportBuffer() { + //Export the entire buffer (make a local JSON file under client/src/) + FILE* fp = fopen("./example.json", "w"); + if (!fp) { + std::cerr << "Could not open file for writing exported buffer." << std::endl; + return; + } + + std::string json_content = "[\n" + toStringAll() + "]"; + //std::cout << "JSON CONTENT: " << json_content << std::endl; + fputs(json_content.c_str(), fp); + fclose(fp); + return; +} \ No newline at end of file diff --git a/client/src/json_reader.cpp b/client/src/json_reader.cpp index d79b536b..40fb9672 100644 --- a/client/src/json_reader.cpp +++ b/client/src/json_reader.cpp @@ -6,6 +6,7 @@ #include #include #include +#include JsonReader::JsonReader() {} @@ -41,24 +42,37 @@ buffer_data_t JsonReader::exportToBuffer(std::string json) { rapidjson::Document doc; rapidjson::ParseResult ok = doc.Parse(json.c_str()); - std::cout << json; + // if (doc.IsArray()) { + // std::cout << "dwda"; + // } + //std::cout << json << std::endl; if (!ok) { std::cerr << "JSON parse error at offset " << ok.Offset() << ": " << rapidjson::GetParseError_En(ok.Code()) << std::endl; + //std::cout << json; return ret; // return default-initialized struct - } + } if (!doc.IsObject()) { std::cerr << "JSON is not an object!\n"; return ret; } - if (doc["sensor"].GetString() == "temperature") { - ret.a_data = doc["value"].GetFloat(); - } else if (doc["sensor"].GetString() == "pressure") { - ret.b_data = doc["value"].GetFloat(); - std::cout << ret.b_data << "B DATA"; - } + std::string sensorTypeString ( + doc["sensor"].GetString(), + doc["sensor"].GetStringLength() + ); + std::string sensorUnitString ( + doc["unit"].GetString(), + doc["unit"].GetStringLength() + ); + double sensorValue = doc["value"].GetDouble(); + + ret.datatype = sensorTypeString.c_str(); + ret.data = sensorValue; + ret.dataunit = sensorUnitString.c_str(); + ret.timestamp = std::time(&ret.timestamp); + return ret; } \ No newline at end of file diff --git a/client/src/main.cpp b/client/src/main.cpp index e6169bf4..f67f562b 100644 --- a/client/src/main.cpp +++ b/client/src/main.cpp @@ -8,6 +8,7 @@ #include "client/argparser.hpp" #include "client/message_model.hpp" #include "client/tcp_client.hpp" +#include "client/DataBuffer.hpp" #include "client/json_reader.hpp" #include "client/config_manager.hpp" #include "client/data_logger.hpp" @@ -130,8 +131,11 @@ class PanoramaClient : public wxApp { std::cerr << "Warning: Data logger failed to initialize. Data will not be persisted." << std::endl; } + // --- Create DataBuffer --- + dataBuffer_ = std::make_shared(); + // --- Create and start TCP client on separate thread --- - tcpClient_ = std::make_unique("127.0.0.1", 3000, model_, dataLogger_); + tcpClient_ = std::make_unique("127.0.0.1", 3000, model_, dataBuffer_, dataLogger_); tcpClient_->start(); // For running without a gui @@ -146,9 +150,10 @@ class PanoramaClient : public wxApp { } // --- Create view --- - MainFrame* w = new MainFrame("Panorama Client", model_); + MainFrame* w = new MainFrame("Panorama Client", model_, dataBuffer_); w->Show(); + // --- If test mode, quit after 3 seconds --- if (parser.isTestMode()) { wxTimer* timer = new wxTimer(this); @@ -173,6 +178,7 @@ class PanoramaClient : public wxApp { private: std::shared_ptr model_; std::shared_ptr dataLogger_; + std::shared_ptr dataBuffer_; std::unique_ptr tcpClient_; }; diff --git a/client/src/mainframe.cpp b/client/src/mainframe.cpp index 4ff2536a..f2b31471 100644 --- a/client/src/mainframe.cpp +++ b/client/src/mainframe.cpp @@ -1,5 +1,6 @@ #include "client/mainframe.hpp" #include "client/message_model.hpp" +#include "client/DataBuffer.hpp" #include "client/graph_panel.hpp" #include "client/sensor_data_panel.h" #include "client/sensor_manager.hpp" @@ -7,8 +8,9 @@ #include MainFrame::MainFrame(const wxString& title, std::shared_ptr model, + std::shared_ptr dataBuffer, const wxPoint& pos, const wxSize& size) - : wxFrame(nullptr, wxID_ANY, title, pos, size), model_(model) { + : wxFrame(nullptr, wxID_ANY, title, pos, size), model_(model), dataBuffer_(dataBuffer) { // Create splitter for layout wxSplitterWindow* mainSplitter = new wxSplitterWindow(this, wxID_ANY); @@ -92,6 +94,14 @@ void MainFrame::updateMessageDisplay() { for (const auto& msg : messages) { text += wxString::FromUTF8(msg.c_str()) + "\n"; } + + if (dataBuffer_->size() > 0) { + //std::cout << dataBuffer_->toStringAll(); + text += "\n--- DataBuffer Contents ---\n"; + text += wxString::FromUTF8(dataBuffer_->toStringAll()); + text += "--- End of DataBuffer ---\n"; + } + messageDisplay_->SetValue(text); messageDisplay_->SetInsertionPointEnd(); } \ No newline at end of file diff --git a/client/src/tcp_client.cpp b/client/src/tcp_client.cpp index 4d463522..df5d45c4 100644 --- a/client/src/tcp_client.cpp +++ b/client/src/tcp_client.cpp @@ -1,5 +1,6 @@ #include "client/tcp_client.hpp" #include "client/message_model.hpp" +#include "client/DataBuffer.hpp" #include "client/data_logger.hpp" #include "common/panorama_utils.hpp" #include @@ -25,8 +26,8 @@ #define SOCKET_ERROR -1 #endif -TcpClient::TcpClient(const std::string& host, int port, std::shared_ptr model, std::shared_ptr logger) - : host_(host), port_(port), model_(model), logger_(logger), running_(false), socket_(INVALID_SOCKET) { +TcpClient::TcpClient(const std::string& host, int port, std::shared_ptr model, std::shared_ptr dataBuffer, std::shared_ptr logger) + : host_(host), port_(port), model_(model), logger_(logger), dataBuffer_(dataBuffer), running_(false), socket_(INVALID_SOCKET) { #ifdef _WIN32 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); @@ -82,7 +83,7 @@ void TcpClient::run() { cleanup(); break; } - + buffer[bytesRead] = '\0'; std::string received(buffer); @@ -95,6 +96,11 @@ void TcpClient::run() { //pinfo("Received JSON: ", received); reader.exportToBuffer(received); + + // Parse JSON and write to DataBuffer + buffer_data_t parsedData = reader.exportToBuffer(received); + dataBuffer_->writeData(parsedData); + model_->addMessage("Data" + std::to_string(dataBuffer_->size())); model_->addMessage("Received: " + received); } } diff --git a/tools/pserver/pstream_json.py b/tools/pserver/pstream_json.py index 926424d2..79972295 100644 --- a/tools/pserver/pstream_json.py +++ b/tools/pserver/pstream_json.py @@ -24,6 +24,11 @@ def __init__(self): "sensor": "pressure", "value": 1013.25, "unit": "hPa" + }, + { + "sensor": "light", + "value": 0.2, + "unit": "nm" } ] @@ -50,6 +55,7 @@ def get_next_data(self) -> bytes: # Convert to JSON string with newline delimiter ('\n') json_str = json.dumps(json_obj) message = json_str + '\n' + return message.encode('utf-8') \ No newline at end of file diff --git a/tools/pserver/pstreamer.py b/tools/pserver/pstreamer.py index aefcb45d..0aa1af38 100644 --- a/tools/pserver/pstreamer.py +++ b/tools/pserver/pstreamer.py @@ -63,6 +63,8 @@ def _stream_worker(self): # Wait for the specified interval or until stop is requested self._stop_event.wait(timeout=self.stream_interval) + #print(data) + except Exception as e: print(f"[PStreamer] Error in stream worker: {e}") finally: