- RegisterEntry에 write_addr 필드 추가 (기본값=addr) - .MD 태그: LOOPSTAT(읽기) ↔ MODEIN(쓰기) 분리 - ReadRegister() 개별 호출 제거 (batch ReadAllRegisters로 대체 완료) - ListTags 대소문자 무시 검색 - 소멸자/Stop null 체크 추가 - HealthCheck: SERVING 상태 반환
389 lines
13 KiB
C++
389 lines
13 KiB
C++
#include "gateway.h"
|
|
#include "controller.hpp"
|
|
#include "modbus_tcp.hpp"
|
|
#include "vendor_formats.hpp"
|
|
#include "codec.hpp"
|
|
#include "logger.hpp"
|
|
#include "json.hpp"
|
|
|
|
#include <fstream>
|
|
#include <numeric>
|
|
#include <algorithm>
|
|
#include <unistd.h>
|
|
#include <grpcpp/server.h>
|
|
#include <grpcpp/server_builder.h>
|
|
#include <grpcpp/server_context.h>
|
|
#include <google/protobuf/timestamp.pb.h>
|
|
|
|
// ─── Constructor / Destructor ───
|
|
|
|
Hc900Gateway::Hc900Gateway(const std::string& host, uint16_t port,
|
|
const std::string& map_path, int poll_interval_ms,
|
|
int grpc_port)
|
|
: host_(host), port_(port), poll_interval_ms_(poll_interval_ms),
|
|
grpc_listen_("0.0.0.0:" + std::to_string(grpc_port))
|
|
{
|
|
LoadRegisterMap(map_path);
|
|
auto transport = std::make_unique<ModbusTCP>();
|
|
controller_ = std::make_unique<Controller>(std::move(transport));
|
|
}
|
|
|
|
Hc900Gateway::~Hc900Gateway() { Stop(); }
|
|
|
|
// ─── Start / Stop ───
|
|
|
|
bool Hc900Gateway::Start()
|
|
{
|
|
Logger::instance().log("INFO", "[Gateway] Connecting to " + host_ + ":" + std::to_string(port_));
|
|
if (!controller_->connect(host_.c_str(), port_)) {
|
|
// 연결 실패해도 종료하지 않음 — 폴 루프에서 재접속 재시도
|
|
Logger::instance().log("WARN", "[Gateway] Initial connection failed. Will retry in poll loop...");
|
|
} else {
|
|
Logger::instance().log("INFO", "[Gateway] Connected.");
|
|
}
|
|
Logger::instance().log("INFO", "[Gateway] Starting poll thread (interval=" + std::to_string(poll_interval_ms_) + "ms)");
|
|
|
|
grpc_service_ = std::make_unique<GatewayServiceImpl>(*this);
|
|
grpc::ServerBuilder builder;
|
|
builder.AddListeningPort(grpc_listen_, grpc::InsecureServerCredentials());
|
|
builder.RegisterService(grpc_service_.get());
|
|
grpc_server_ = builder.BuildAndStart();
|
|
Logger::instance().log("INFO", "[Gateway] gRPC server listening on " + grpc_listen_);
|
|
|
|
running_ = true;
|
|
poll_thread_ = std::thread(&Hc900Gateway::PollLoop, this);
|
|
return true;
|
|
}
|
|
|
|
void Hc900Gateway::Stop()
|
|
{
|
|
running_ = false;
|
|
if (poll_thread_.joinable()) poll_thread_.join();
|
|
if (grpc_server_) grpc_server_->Shutdown();
|
|
if (controller_) controller_->disconnect();
|
|
}
|
|
|
|
// ─── Register Map Loading ───
|
|
|
|
void Hc900Gateway::LoadRegisterMap(const std::string& path)
|
|
{
|
|
std::ifstream f(path);
|
|
if (!f.is_open()) {
|
|
Logger::instance().log("ERROR", "[Gateway] Cannot open register map: " + path);
|
|
return;
|
|
}
|
|
|
|
auto j = nlohmann::json::parse(f);
|
|
for (const auto& item : j["registers"]) {
|
|
RegisterEntry e;
|
|
e.tag = item["tag"];
|
|
e.addr = item["addr"];
|
|
e.write_addr = item.value("write_addr", e.addr); // default: write where we read
|
|
e.count = item.value("count", 2);
|
|
e.type = item.value("type", "float32");
|
|
e.access = item.value("access", "R");
|
|
registers_.push_back(e);
|
|
tag_index_[e.tag] = registers_.size() - 1;
|
|
}
|
|
|
|
// Build address-sorted index for batch reads
|
|
sorted_indices_.resize(registers_.size());
|
|
std::iota(sorted_indices_.begin(), sorted_indices_.end(), 0);
|
|
std::sort(sorted_indices_.begin(), sorted_indices_.end(),
|
|
[this](size_t a, size_t b){ return registers_[a].addr < registers_[b].addr; });
|
|
|
|
Logger::instance().log("INFO", "[Gateway] Loaded " + std::to_string(registers_.size()) + " registers from " + path);
|
|
}
|
|
|
|
// ─── Poll Loop ───
|
|
|
|
void Hc900Gateway::PollLoop()
|
|
{
|
|
while (running_) {
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
|
|
if (controller_->is_connected()) {
|
|
ReadAllRegisters();
|
|
poll_count_++;
|
|
} else {
|
|
controller_->poll();
|
|
}
|
|
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - t0);
|
|
last_poll_duration_ = elapsed;
|
|
|
|
auto sleep_for = std::chrono::milliseconds(poll_interval_ms_) - elapsed;
|
|
if (sleep_for.count() > 0) {
|
|
std::this_thread::sleep_for(sleep_for);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Hc900Gateway::ReadAllRegisters()
|
|
{
|
|
constexpr uint16_t MAX_BATCH = 120;
|
|
decltype(cache_) fresh;
|
|
|
|
size_t i = 0;
|
|
while (i < sorted_indices_.size()) {
|
|
uint32_t batch_start = registers_[sorted_indices_[i]].addr;
|
|
|
|
// Collect all registers that fit within [batch_start, batch_start + MAX_BATCH)
|
|
size_t j = i;
|
|
while (j < sorted_indices_.size()) {
|
|
const auto& e = registers_[sorted_indices_[j]];
|
|
if (e.addr + e.count - batch_start > MAX_BATCH) break;
|
|
++j;
|
|
}
|
|
|
|
// Read count = up to end of last register in this batch
|
|
const auto& last = registers_[sorted_indices_[j - 1]];
|
|
uint16_t read_count = static_cast<uint16_t>(last.addr + last.count - batch_start);
|
|
|
|
std::vector<uint16_t> regs;
|
|
bool ok;
|
|
{
|
|
std::lock_guard<std::mutex> lock(transport_mutex_);
|
|
ok = controller_->read_raw(static_cast<uint16_t>(batch_start), read_count, regs);
|
|
}
|
|
|
|
if (ok) {
|
|
auto now = std::chrono::system_clock::now();
|
|
for (size_t k = i; k < j; k++) {
|
|
const auto& entry = registers_[sorted_indices_[k]];
|
|
size_t off = entry.addr - batch_start;
|
|
|
|
CachedValue cv{};
|
|
cv.timestamp = now;
|
|
cv.quality = 192;
|
|
|
|
if (entry.type == "uint16") {
|
|
cv.is_float = false;
|
|
cv.uint16_val = regs[off];
|
|
} else {
|
|
cv.is_float = true;
|
|
cv.float32_val = decode_float(regs[off], regs[off + 1], VendorFormat::HC900_FLOAT);
|
|
}
|
|
fresh[entry.tag] = cv;
|
|
}
|
|
}
|
|
|
|
i = j;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
|
cache_ = std::move(fresh);
|
|
}
|
|
}
|
|
|
|
// ─── gRPC Service Implementation ───
|
|
|
|
Hc900Gateway::GatewayServiceImpl::GatewayServiceImpl(Hc900Gateway& gateway)
|
|
: gateway_(gateway) {}
|
|
|
|
namespace {
|
|
void TagValueFromCache(hc900::TagValue* tv, const std::string& name, const CachedValue& cv) {
|
|
tv->set_tag_name(name);
|
|
tv->set_quality(cv.quality);
|
|
auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
cv.timestamp.time_since_epoch()).count();
|
|
tv->mutable_timestamp()->set_seconds(static_cast<int64_t>(ns / 1000000000));
|
|
tv->mutable_timestamp()->set_nanos(static_cast<int32_t>(ns % 1000000000));
|
|
if (cv.is_float) {
|
|
tv->set_float32_val(cv.float32_val);
|
|
} else {
|
|
tv->set_uint16_val(cv.uint16_val);
|
|
}
|
|
}
|
|
}
|
|
|
|
grpc::Status Hc900Gateway::GatewayServiceImpl::ReadTags(
|
|
grpc::ServerContext*,
|
|
const hc900::ReadTagsRequest* req,
|
|
hc900::ReadTagsResponse* resp)
|
|
{
|
|
std::lock_guard<std::mutex> lock(gateway_.cache_mutex_);
|
|
|
|
if (req->tag_names().empty()) {
|
|
for (const auto& [tag, cv] : gateway_.cache_) {
|
|
TagValueFromCache(resp->add_values(), tag, cv);
|
|
}
|
|
} else {
|
|
for (const auto& name : req->tag_names()) {
|
|
auto it = gateway_.cache_.find(name);
|
|
if (it != gateway_.cache_.end()) {
|
|
TagValueFromCache(resp->add_values(), it->first, it->second);
|
|
}
|
|
}
|
|
}
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status Hc900Gateway::GatewayServiceImpl::WriteTag(
|
|
grpc::ServerContext*,
|
|
const hc900::WriteTagRequest* req,
|
|
hc900::WriteTagResponse* resp)
|
|
{
|
|
auto it = gateway_.tag_index_.find(req->tag_name());
|
|
if (it == gateway_.tag_index_.end()) {
|
|
resp->set_success(false);
|
|
resp->set_error("Tag not found");
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
auto& entry = gateway_.registers_[it->second];
|
|
if (entry.access != "RW") {
|
|
resp->set_success(false);
|
|
resp->set_error("Read-only tag");
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
bool ok = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(gateway_.transport_mutex_);
|
|
if (entry.type == "uint16") {
|
|
ok = gateway_.controller_->write_register(entry.write_addr,
|
|
static_cast<uint16_t>(req->value()));
|
|
} else if (entry.type == "float32") {
|
|
ok = gateway_.controller_->write_float(entry.write_addr,
|
|
static_cast<float>(req->value()),
|
|
VendorFormat::HC900_FLOAT);
|
|
}
|
|
}
|
|
|
|
resp->set_success(ok);
|
|
if (!ok) resp->set_error("Modbus write failed");
|
|
|
|
if (ok) {
|
|
std::lock_guard<std::mutex> lock(gateway_.cache_mutex_);
|
|
CachedValue cv;
|
|
cv.is_float = (entry.type == "float32");
|
|
if (cv.is_float) cv.float32_val = static_cast<float>(req->value());
|
|
else cv.uint16_val = static_cast<uint16_t>(req->value());
|
|
cv.quality = 192;
|
|
cv.timestamp = std::chrono::system_clock::now();
|
|
gateway_.cache_[entry.tag] = cv;
|
|
}
|
|
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status Hc900Gateway::GatewayServiceImpl::ListTags(
|
|
grpc::ServerContext*,
|
|
const hc900::ListTagsRequest* req,
|
|
hc900::ListTagsResponse* resp)
|
|
{
|
|
// ListTags 필터: 대소문자 무시 검색 (ReadTags/WriteTag는 exact-match, register-map 원형 표기 필요)
|
|
int count = 0;
|
|
for (const auto& entry : gateway_.registers_) {
|
|
if (!req->filter().empty()) {
|
|
auto lower_tag = entry.tag;
|
|
auto lower_filter = req->filter();
|
|
std::transform(lower_tag.begin(), lower_tag.end(), lower_tag.begin(), ::tolower);
|
|
std::transform(lower_filter.begin(), lower_filter.end(), lower_filter.begin(), ::tolower);
|
|
if (lower_tag.find(lower_filter) == std::string::npos) continue;
|
|
}
|
|
auto* meta = resp->add_tags();
|
|
meta->set_tag_name(entry.tag);
|
|
meta->set_address(entry.addr);
|
|
meta->set_count(entry.count);
|
|
meta->set_type(entry.type);
|
|
meta->set_access(entry.access);
|
|
count++;
|
|
if (req->limit() > 0 && count >= req->limit()) break;
|
|
}
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status Hc900Gateway::GatewayServiceImpl::HealthCheck(
|
|
grpc::ServerContext*,
|
|
const hc900::HealthCheckRequest*,
|
|
hc900::HealthCheckResponse* resp)
|
|
{
|
|
resp->set_status(gateway_.controller_->is_connected()
|
|
? hc900::HealthCheckResponse::SERVING
|
|
: hc900::HealthCheckResponse::NOT_SERVING);
|
|
resp->set_uptime_sec(0);
|
|
resp->set_poll_count(gateway_.poll_count_);
|
|
resp->set_last_poll_ms(static_cast<int32_t>(gateway_.last_poll_duration_.count()));
|
|
resp->set_controller_ip(gateway_.host_);
|
|
resp->set_active_tags(static_cast<int32_t>(gateway_.registers_.size()));
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status Hc900Gateway::GatewayServiceImpl::StreamTags(
|
|
grpc::ServerContext* ctx,
|
|
const hc900::StreamTagsRequest* req,
|
|
grpc::ServerWriter<hc900::TagValue>* writer)
|
|
{
|
|
int interval = req->interval_ms() > 0 ? req->interval_ms() : 1000;
|
|
|
|
std::vector<std::string> names;
|
|
if (req->tag_names().empty()) {
|
|
std::lock_guard<std::mutex> lock(gateway_.cache_mutex_);
|
|
for (const auto& [tag, _] : gateway_.cache_) {
|
|
names.push_back(tag);
|
|
}
|
|
} else {
|
|
names.assign(req->tag_names().begin(), req->tag_names().end());
|
|
}
|
|
|
|
while (!ctx->IsCancelled()) {
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(gateway_.cache_mutex_);
|
|
for (const auto& name : names) {
|
|
auto it = gateway_.cache_.find(name);
|
|
if (it == gateway_.cache_.end()) continue;
|
|
auto& cv = it->second;
|
|
|
|
hc900::TagValue tv;
|
|
TagValueFromCache(&tv, name, cv);
|
|
if (!writer->Write(tv)) return grpc::Status::CANCELLED;
|
|
}
|
|
}
|
|
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - t0);
|
|
auto sleep_for = std::chrono::milliseconds(interval) - elapsed;
|
|
if (sleep_for.count() > 0) {
|
|
std::this_thread::sleep_for(sleep_for);
|
|
}
|
|
}
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
// ─── main ───
|
|
//
|
|
// One process per controller. The C# ControllerProcessManager launches this with:
|
|
// hc900_gateway <host> <register-map> <poll_ms> <grpc_port> <modbus_port>
|
|
int main(int argc, char* argv[])
|
|
{
|
|
std::string host = "192.168.0.240";
|
|
std::string map_path = "docs/register-map.json";
|
|
int poll_ms = 1000;
|
|
int grpc_port = 50051;
|
|
uint16_t modbus_port = 502;
|
|
|
|
if (argc > 1) host = argv[1];
|
|
if (argc > 2) map_path = argv[2];
|
|
if (argc > 3) poll_ms = std::atoi(argv[3]);
|
|
if (argc > 4) grpc_port = std::atoi(argv[4]);
|
|
if (argc > 5) modbus_port = static_cast<uint16_t>(std::atoi(argv[5]));
|
|
|
|
Logger::instance().set_file("/tmp/hc900_gateway.log");
|
|
|
|
Hc900Gateway gateway(host, modbus_port, map_path, poll_ms, grpc_port);
|
|
if (!gateway.Start()) {
|
|
return 1;
|
|
}
|
|
|
|
Logger::instance().log("INFO", "[Gateway] Running. Press Ctrl+C to stop.");
|
|
pause();
|
|
gateway.Stop();
|
|
return 0;
|
|
}
|