Files
HC900-Crawler/industrial-comm/cpp/src/gateway.cpp
windpacer 4348fb49f8 feat: C++ 게이트웨이 write_addr 지원 + 헬스체크 기반 연결 상태 판정
- RegisterEntry에 write_addr 필드 추가 (기본값=addr)
  - .MD 태그: LOOPSTAT(읽기) ↔ MODEIN(쓰기) 분리
- ReadRegister() 개별 호출 제거 (batch ReadAllRegisters로 대체 완료)
- ListTags 대소문자 무시 검색
- 소멸자/Stop null 체크 추가
- HealthCheck: SERVING 상태 반환
2026-06-04 09:43:18 +09:00

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;
}