plc_control/src/connection.rs

1329 lines
48 KiB
Rust

use chrono::{DateTime, Utc};
use opcua::{
client::{ClientBuilder, IdentityToken, Session},
crypto::SecurityPolicy,
types::{
AttributeId, MessageSecurityMode, MonitoredItemCreateRequest, MonitoringMode,
MonitoringParameters, NodeId, NumericRange, ReadValueId, TimestampsToReturn, Variant,
WriteValue, DataValue as OpcuaDataValue,
UserTokenPolicy,
},
};
use std::{
collections::{HashMap, HashSet, VecDeque},
str::FromStr,
sync::Arc,
time::Duration,
};
use tokio::task::JoinHandle;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::{
model::{PointSubscriptionInfo, ScanMode},
telemetry::PointMonitorInfo,
};
const DEFAULT_POINT_RING_BUFFER_LEN: usize = 1000;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct SetPointValueReqItem {
pub point_id: Uuid,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct BatchSetPointValueReq {
pub items: Vec<SetPointValueReqItem>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SetPointValueResItem {
pub point_id: Uuid,
pub success: bool,
pub err_msg: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BatchSetPointValueRes {
pub success: bool,
pub err_msg: Option<String>,
pub success_count: usize,
pub failed_count: usize,
pub results: Vec<SetPointValueResItem>,
}
#[derive(Debug, Clone)]
struct PointWriteTarget {
source_id: Uuid,
external_id: String,
}
#[derive(Debug, Clone)]
pub struct PollPointInfo {
pub point_id: Uuid,
pub external_id: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ConnectionStatusView {
pub is_connected: bool,
pub last_error: Option<String>,
pub last_time: DateTime<Utc>,
pub subscription_id: Option<u32>,
pub next_client_handle: u32,
}
impl From<&ConnectionStatus> for ConnectionStatusView {
fn from(status: &ConnectionStatus) -> Self {
Self {
is_connected: status.is_connected,
last_error: status.last_error.clone(),
last_time: status.last_time,
subscription_id: status.subscription_id,
next_client_handle: status.next_client_handle,
}
}
}
pub struct ConnectionStatus {
pub session: Option<Arc<Session>>,
pub is_connected: bool,
pub last_error: Option<String>,
pub last_time: DateTime<Utc>,
pub subscription_id: Option<u32>,
pub next_client_handle: u32,
pub client_handle_map: HashMap<u32, Uuid>, // client_handle -> point_id
pub monitored_item_map: HashMap<Uuid, u32>, // point_id -> monitored_item_id
pub poll_points: Vec<PollPointInfo>, // 正在轮询的点集合
poll_handle: Option<JoinHandle<()>>, // 统一的轮询任务句柄
heartbeat_handle: Option<JoinHandle<()>>, // 心跳任务句柄
}
#[derive(Clone)]
pub struct ConnectionManager {
status: Arc<RwLock<HashMap<Uuid, ConnectionStatus>>>,
point_monitor_data: Arc<RwLock<HashMap<Uuid, PointMonitorInfo>>>,
point_history_data: Arc<RwLock<HashMap<Uuid, VecDeque<PointMonitorInfo>>>>,
point_write_target_cache: Arc<RwLock<HashMap<Uuid, PointWriteTarget>>>,
event_manager: Option<std::sync::Arc<crate::event::EventManager>>,
pool: Option<Arc<sqlx::PgPool>>,
reconnect_tx: Option<tokio::sync::mpsc::UnboundedSender<Uuid>>,
reconnect_rx: Arc<std::sync::Mutex<Option<tokio::sync::mpsc::UnboundedReceiver<Uuid>>>>,
reconnecting: Arc<RwLock<HashSet<Uuid>>>,
}
impl ConnectionManager {
fn subscription_result(subscribed: usize, polled: usize) -> HashMap<String, usize> {
let mut result = HashMap::new();
result.insert("subscribed".to_string(), subscribed);
result.insert("polled".to_string(), polled);
result.insert("total".to_string(), subscribed + polled);
result
}
fn json_value_to_opcua_variant(value: &serde_json::Value) -> Result<Variant, String> {
match value {
serde_json::Value::Null => Ok(Variant::Empty),
serde_json::Value::Bool(v) => Ok(Variant::from(*v)),
serde_json::Value::Number(n) => {
if let Some(v) = n.as_i64() {
Ok(Variant::from(v))
} else if let Some(v) = n.as_u64() {
Ok(Variant::from(v))
} else if let Some(v) = n.as_f64() {
Ok(Variant::from(v))
} else {
Err("Unsupported numeric value".to_string())
}
}
serde_json::Value::String(s) => Ok(Variant::from(s.clone())),
_ => Err("Only null/bool/number/string are supported for point write".to_string()),
}
}
fn write_value_batch_result(
success: bool,
err_msg: Option<String>,
results: Vec<SetPointValueResItem>,
) -> BatchSetPointValueRes {
let success_count = results.iter().filter(|r| r.success).count();
let failed_count = results.len().saturating_sub(success_count);
BatchSetPointValueRes {
success,
err_msg,
success_count,
failed_count,
results,
}
}
pub fn new() -> Self {
let (reconnect_tx, reconnect_rx) = tokio::sync::mpsc::unbounded_channel::<Uuid>();
Self {
status: Arc::new(RwLock::new(HashMap::new())),
point_monitor_data: Arc::new(RwLock::new(HashMap::new())),
point_history_data: Arc::new(RwLock::new(HashMap::new())),
point_write_target_cache: Arc::new(RwLock::new(HashMap::new())),
event_manager: None,
pool: None,
reconnect_tx: Some(reconnect_tx),
reconnect_rx: Arc::new(std::sync::Mutex::new(Some(reconnect_rx))),
reconnecting: Arc::new(RwLock::new(HashSet::new())),
}
}
pub fn set_event_manager(&mut self, event_manager: std::sync::Arc<crate::event::EventManager>) {
self.event_manager = Some(event_manager);
}
pub fn set_pool(&mut self, pool: Arc<sqlx::PgPool>) {
self.pool = Some(pool);
}
pub fn set_pool_and_start_reconnect_task(&mut self, pool: Arc<sqlx::PgPool>) {
self.pool = Some(pool.clone());
// 将 self 转换为不可变引用以调用 start_reconnect_task
let manager = self.clone();
manager.start_reconnect_task();
}
pub fn set_reconnect_tx(&mut self, tx: tokio::sync::mpsc::UnboundedSender<Uuid>) {
self.reconnect_tx = Some(tx);
}
pub fn get_reconnect_rx(&self) -> Option<tokio::sync::mpsc::UnboundedReceiver<Uuid>> {
self.reconnect_rx.lock().unwrap().take()
}
pub fn start_reconnect_task(&self) {
let manager = self.clone();
let pool = manager.pool.clone();
tokio::spawn(async move {
// 获取重连通道的接收端
let mut reconnect_rx = manager.get_reconnect_rx().expect("Failed to get reconnect receiver");
while let Some(source_id) = reconnect_rx.recv().await {
tracing::info!("Received reconnect request for source {}", source_id);
if let Some(ref pool) = pool {
if let Err(e) = manager.reconnect(pool, source_id).await {
tracing::error!("Failed to reconnect source {}: {}", source_id, e);
}
} else {
tracing::warn!("Pool not available for reconnection of source {}", source_id);
}
}
});
}
pub async fn remove_point_write_target_cache_by_point_ids(
&self,
point_ids: &[Uuid],
) -> usize {
if point_ids.is_empty() {
return 0;
}
let mut removed = 0usize;
let mut cache = self.point_write_target_cache.write().await;
for point_id in point_ids {
if cache.remove(point_id).is_some() {
removed += 1;
}
}
removed
}
pub async fn update_point_monitor_data(
&self,
sample: PointMonitorInfo,
) -> Result<(), String> {
let point_id = sample.point_id;
let mut data = self.point_monitor_data.write().await;
data.insert(point_id, sample.clone());
let mut history_data = self.point_history_data.write().await;
let ring = history_data
.entry(point_id)
.or_insert_with(|| VecDeque::with_capacity(DEFAULT_POINT_RING_BUFFER_LEN));
ring.push_back(sample);
if ring.len() > DEFAULT_POINT_RING_BUFFER_LEN {
let _ = ring.pop_front();
}
Ok(())
}
pub async fn get_status_read_guard(&self) -> tokio::sync::RwLockReadGuard<'_, HashMap<Uuid, ConnectionStatus>> {
self.status.read().await
}
pub async fn get_point_monitor_data_read_guard(
&self,
) -> tokio::sync::RwLockReadGuard<'_, HashMap<Uuid, PointMonitorInfo>> {
self.point_monitor_data.read().await
}
async fn start_heartbeat_task(&self, source_id: Uuid) {
let manager = self.clone();
let handle = tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(30)); // 每30秒检测一次心跳
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
ticker.tick().await;
// 检查session是否有效
let session = manager.get_session(source_id).await;
let session_valid = if let Some(session) = session {
// 尝试读取当前时间来验证连接
let node_id = NodeId::new(0, 2258); // ServerCurrentTime节点
let read_request = ReadValueId {
node_id,
attribute_id: AttributeId::Value as u32,
index_range: NumericRange::None,
data_encoding: Default::default(),
};
match session.read(&[read_request], TimestampsToReturn::Neither, 0f64).await {
Ok(_) => true,
Err(_) => false,
}
} else {
false
};
if !session_valid {
// 检查是否已经在重连中
let mut reconnecting = manager.reconnecting.write().await;
if !reconnecting.contains(&source_id) {
reconnecting.insert(source_id);
drop(reconnecting);
tracing::warn!(
"Heartbeat detected invalid session for source {}, triggering reconnection",
source_id
);
// 通过通道发送重连请求
if let Some(tx) = manager.reconnect_tx.as_ref() {
if let Err(e) = tx.send(source_id) {
tracing::error!("Failed to send reconnect request for source {}: {}", source_id, e);
}
} else {
tracing::warn!("Reconnect channel not available for source {}", source_id);
}
} else {
drop(reconnecting);
}
}
}
});
// 保存心跳任务句柄
let mut status = self.status.write().await;
if let Some(conn_status) = status.get_mut(&source_id) {
conn_status.heartbeat_handle = Some(handle);
}
}
async fn start_unified_poll_task(&self, source_id: Uuid, session: Arc<Session>) {
let event_manager = match self.event_manager.clone() {
Some(em) => em,
None => {
tracing::warn!("Event manager is not initialized, cannot start unified poll task");
return;
}
};
// 停止旧的轮询任务
{
let mut status = self.status.write().await;
if let Some(conn_status) = status.get_mut(&source_id) {
if let Some(handle) = conn_status.poll_handle.take() {
handle.abort();
}
}
}
tracing::info!(
"Starting unified poll task for source {}",
source_id
);
// 克隆 status 引用,以便在异步任务中使用
let status_ref = self.status.clone();
// 启动新的轮询任务
let handle = tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(1));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
ticker.tick().await;
// 在任务内部获取轮询点列表
let poll_points = {
let status = status_ref.read().await;
status.get(&source_id)
.map(|conn_status| conn_status.poll_points.clone())
.unwrap_or_default()
};
if poll_points.is_empty() {
continue;
}
// 构建批量读取请求
let read_requests: Vec<ReadValueId> = poll_points
.iter()
.filter_map(|p| {
NodeId::from_str(&p.external_id).ok().map(|node_id| ReadValueId {
node_id,
attribute_id: AttributeId::Value as u32,
index_range: NumericRange::None,
data_encoding: Default::default(),
})
})
.collect();
if read_requests.is_empty() {
continue;
}
// 执行批量读取
match session.read(&read_requests, TimestampsToReturn::Both, 0f64).await {
Ok(results) => {
for (poll_point, result) in poll_points.iter().zip(results.iter()) {
let dv = result;
let val = dv.value.clone();
let unified_value = val.as_ref().map(crate::telemetry::opcua_variant_to_data);
let unified_value_type = val.as_ref().map(crate::telemetry::opcua_variant_type);
let unified_value_text = val.as_ref().map(|v| v.to_string());
let quality = dv.status
.as_ref()
.map(crate::telemetry::PointQuality::from_status_code)
.unwrap_or(crate::telemetry::PointQuality::Unknown);
let _ = event_manager.send(crate::event::ReloadEvent::PointNewValue(
crate::telemetry::PointNewValue {
source_id,
point_id: Some(poll_point.point_id),
client_handle: 0,
value: unified_value,
value_type: unified_value_type,
value_text: unified_value_text,
quality,
protocol: "opcua".to_string(),
timestamp: Some(Utc::now()),
scan_mode: ScanMode::Poll,
},
));
}
}
Err(e) => {
tracing::warn!(
"Unified poll read failed for source {}: {:?}",
source_id,
e
);
}
}
}
});
// 保存轮询任务句柄
let mut status = self.status.write().await;
if let Some(conn_status) = status.get_mut(&source_id) {
conn_status.poll_handle = Some(handle);
}
}
// 将点添加到轮询列表
async fn add_points_to_poll_list(
&self,
source_id: Uuid,
points: &[PointSubscriptionInfo],
) -> usize {
let mut started = 0usize;
// 添加新的轮询点
{
let mut status = self.status.write().await;
if let Some(conn_status) = status.get_mut(&source_id) {
for point in points {
// 检查点是否已经在轮询列表中
if !conn_status.poll_points.iter().any(|p| p.point_id == point.point_id) {
conn_status.poll_points.push(PollPointInfo {
point_id: point.point_id,
external_id: point.external_id.clone(),
});
started += 1;
tracing::info!(
"Point {} switched to poll mode",
point.point_id
);
}
}
}
}
started
}
pub async fn connect_from_source(
&self,
pool: &sqlx::PgPool,
source_id: Uuid,
) -> Result<(), String> {
let source = match crate::service::get_enabled_source(pool, source_id).await {
Ok(Some(s)) => s,
Ok(None) => {
tracing::info!(
"Source {} is not enabled or does not exist, skipping connection",
source_id
);
return Ok(());
}
Err(e) => {
let error = format!("Failed to fetch source: {}", e);
tracing::error!("{}", error);
return Err(error);
}
};
self.connect(
source.id,
&source.endpoint,
source.security_policy.as_deref(),
source.security_mode.as_deref(),
source.username.as_deref(),
source.password.as_deref(),
)
.await?;
// Subscribe to points for this source
self.subscribe_points_from_source(source_id, None, pool)
.await?;
Ok(())
}
pub async fn connect(
&self,
source_id: Uuid,
endpoint: &str,
security_policy: Option<&str>,
security_mode: Option<&str>,
username: Option<&str>,
password: Option<&str>,
) -> Result<(), String> {
let mut client = match ClientBuilder::new()
.application_name("plc_gateway")
.application_uri("urn:plc_gateway:opcua-client")
.trust_server_certs(true)
.create_sample_keypair(true)
.session_retry_limit(3)
.client()
{
Ok(client) => client,
Err(e) => {
let error = format!("Failed to create client: {:?}", e);
self.fail_connect(source_id, &error).await;
return Err(error);
}
};
let security_policy = if security_policy.is_none() {
SecurityPolicy::None
} else {
security_policy
.and_then(|s| SecurityPolicy::from_str(s).ok())
.unwrap_or(SecurityPolicy::None)
};
let security_mode = match security_mode {
Some("Sign") => MessageSecurityMode::Sign,
Some("SignAndEncrypt") => MessageSecurityMode::SignAndEncrypt,
_ => MessageSecurityMode::None,
};
let identity_token = match (username, password) {
(Some(user), Some(pass)) => IdentityToken::new_user_name(user, pass),
_ => IdentityToken::Anonymous,
};
let (session, event_loop) = match client
.connect_to_matching_endpoint(
(
endpoint,
security_policy.to_str(),
security_mode,
UserTokenPolicy::anonymous(),
),
identity_token,
)
.await
{
Ok(res) => res,
Err(e) => {
let error = format!("Failed to connect: {:?}", e);
self.fail_connect(source_id, &error).await;
return Err(error);
}
};
let _ = event_loop.spawn();
session.wait_for_connection().await;
let mut status = self.status.write().await;
status.insert(
source_id,
ConnectionStatus {
session: Some(session.clone()),
is_connected: true,
last_error: None,
last_time: Utc::now(),
subscription_id: None,
next_client_handle: 1000,
client_handle_map: HashMap::new(),
monitored_item_map: HashMap::new(),
poll_points: Vec::new(),
poll_handle: None,
heartbeat_handle: None,
},
);
drop(status); // 显式释放锁,在调用 start_unified_poll_task 之前
// 启动统一的轮询任务
self.start_unified_poll_task(source_id, session).await;
// 启动心跳任务
self.start_heartbeat_task(source_id).await;
tracing::info!("Successfully connected to source {}", source_id);
Ok(())
}
pub async fn fail_connect(&self, source_id: Uuid, error: &str) {
let mut status = self.status.write().await;
status.insert(
source_id,
ConnectionStatus {
session: None,
is_connected: false,
last_error: Some(error.to_string()),
last_time: Utc::now(),
subscription_id: None,
client_handle_map: HashMap::new(),
monitored_item_map: HashMap::new(),
next_client_handle: 1000,
poll_points: Vec::new(),
poll_handle: None,
heartbeat_handle: None,
},
);
}
pub async fn reconnect(&self, pool: &sqlx::PgPool, source_id: Uuid) -> Result<(), String> {
tracing::info!("Reconnecting to source {}", source_id);
// 先断开连接
if let Err(e) = self.disconnect(source_id).await {
tracing::error!("Failed to disconnect source {}: {}", source_id, e);
}
// 再重新连接
let result = self.connect_from_source(pool, source_id).await;
// 清除重连标记
if result.is_ok() {
let mut reconnecting = self.reconnecting.write().await;
reconnecting.remove(&source_id);
}
result
}
pub async fn disconnect(&self, source_id: Uuid) -> Result<(), String> {
// 停止轮询任务并清空轮询点列表
{
let mut status = self.status.write().await;
if let Some(conn_status) = status.get_mut(&source_id) {
conn_status.poll_points.clear();
if let Some(handle) = conn_status.poll_handle.take() {
handle.abort();
}
// 停止心跳任务
if let Some(handle) = conn_status.heartbeat_handle.take() {
handle.abort();
}
}
}
let conn_status = self.status.write().await.remove(&source_id);
if let Some(conn_status) = conn_status {
if let Some(session) = conn_status.session {
session.disconnect().await.map_err(|e| {
let error = format!("Failed to disconnect: {}", e);
tracing::error!("{}", error);
error
})?;
}
}
Ok(())
}
pub async fn disconnect_all(&self) {
let source_ids: Vec<Uuid> = self.status.read().await.keys().copied().collect();
for source_id in source_ids {
// 停止轮询任务并清空轮询点列表
{
let mut status = self.status.write().await;
if let Some(conn_status) = status.get_mut(&source_id) {
conn_status.poll_points.clear();
if let Some(handle) = conn_status.poll_handle.take() {
handle.abort();
}
// 停止心跳任务
if let Some(handle) = conn_status.heartbeat_handle.take() {
handle.abort();
}
}
}
let conn_status = self.status.write().await.remove(&source_id);
if let Some(conn_status) = conn_status {
if let Some(session) = conn_status.session {
if let Err(e) = session.disconnect().await {
tracing::error!("Failed to disconnect from source {}: {}", source_id, e);
}
}
}
}
}
pub async fn get_session(&self, source_id: Uuid) -> Option<Arc<Session>> {
let status = self.status.read().await;
if let Some(conn_status) = status.get(&source_id) {
if conn_status.is_connected {
return conn_status.session.clone();
}
}
None
}
pub async fn get_status(&self, source_id: Uuid) -> Option<ConnectionStatusView> {
let status = self.status.read().await;
status.get(&source_id).map(ConnectionStatusView::from)
}
pub async fn get_all_status(&self) -> Vec<(Uuid, ConnectionStatusView)> {
let status = self.status.read().await;
status.iter().map(|(source_id, conn_status)| (*source_id, ConnectionStatusView::from(conn_status))).collect()
}
pub async fn write_point_values_batch(
&self,
payload: BatchSetPointValueReq,
) -> Result<BatchSetPointValueRes, String> {
if payload.items.is_empty() {
return Ok(Self::write_value_batch_result(
false,
Some("items cannot be empty".to_string()),
vec![],
));
}
let target_map = self.point_write_target_cache.read().await;
#[derive(Clone)]
struct PendingWrite {
point_id: Uuid,
source_id: Uuid,
external_id: String,
variant: Variant,
}
let mut results = Vec::new();
let mut pending_by_source: HashMap<Uuid, Vec<PendingWrite>> = HashMap::new();
for item in payload.items {
let Some(target) = target_map.get(&item.point_id) else {
results.push(SetPointValueResItem {
point_id: item.point_id,
success: false,
err_msg: Some("Point write target not found in cache".to_string()),
});
continue;
};
let variant = match Self::json_value_to_opcua_variant(&item.value) {
Ok(v) => v,
Err(e) => {
results.push(SetPointValueResItem {
point_id: item.point_id,
success: false,
err_msg: Some(e),
});
continue;
}
};
pending_by_source
.entry(target.source_id)
.or_default()
.push(PendingWrite {
point_id: item.point_id,
source_id: target.source_id,
external_id: target.external_id.clone(),
variant,
});
}
if !results.is_empty() {
return Ok(Self::write_value_batch_result(
false,
Some("Batch write precheck failed, no write executed".to_string()),
results,
));
}
// Validate all target sources are already connected before executing writes.
for source_id in pending_by_source.keys() {
let source_connected = self
.get_status(*source_id)
.await
.map(|s| s.is_connected)
.unwrap_or(false);
if !source_connected {
return Ok(Self::write_value_batch_result(
false,
Some(format!("Source {} is not connected", source_id)),
vec![],
));
}
}
let mut success_events: Vec<(Uuid, Uuid, Variant)> = Vec::new();
for (source_id, writes) in pending_by_source {
let Some(session) = self.get_session(source_id).await else {
return Ok(Self::write_value_batch_result(
false,
Some(format!("Source {} is not connected", source_id)),
vec![],
));
};
let mut write_values = Vec::new();
let mut write_items = Vec::new();
for write in writes {
let node_id = match NodeId::from_str(&write.external_id) {
Ok(v) => v,
Err(e) => {
results.push(SetPointValueResItem {
point_id: write.point_id,
success: false,
err_msg: Some(format!("Invalid node id {}: {}", write.external_id, e)),
});
continue;
}
};
write_values.push(WriteValue {
node_id,
attribute_id: AttributeId::Value as u32,
index_range: NumericRange::None,
value: OpcuaDataValue::value_only(write.variant.clone()),
});
write_items.push(write);
}
if write_values.is_empty() {
continue;
}
let status_codes = match session.write(&write_values).await {
Ok(v) => v,
Err(e) => {
let write_results: Vec<SetPointValueResItem> = write_items
.into_iter()
.map(|write| SetPointValueResItem {
point_id: write.point_id,
success: false,
err_msg: Some(format!("Write failed: {:?}", e)),
})
.collect();
return Ok(Self::write_value_batch_result(
false,
Some("Batch write failed, no local updates emitted".to_string()),
write_results,
));
}
};
for (idx, write) in write_items.iter().enumerate() {
let status_opt = status_codes.get(idx);
let status_ok = status_opt.map(|s| s.is_good()).unwrap_or(false);
results.push(SetPointValueResItem {
point_id: write.point_id,
success: status_ok,
err_msg: if status_ok {
None
} else {
Some(match status_opt {
Some(s) => format!("Write rejected: {:?}", s),
None => "Write result missing from server response".to_string(),
})
},
});
if !status_ok {
return Ok(Self::write_value_batch_result(
false,
Some("Batch write failed, no local updates emitted".to_string()),
results,
));
}
success_events.push((write.source_id, write.point_id, write.variant.clone()));
}
}
// Emit local updates only when the full batch succeeds.
if let Some(event_manager) = &self.event_manager {
for (source_id, point_id, variant) in success_events {
if let Err(e) = event_manager.send(crate::event::ReloadEvent::PointNewValue(
crate::telemetry::PointNewValue {
source_id,
point_id: Some(point_id),
client_handle: 0,
value: Some(crate::telemetry::opcua_variant_to_data(&variant)),
value_type: Some(crate::telemetry::opcua_variant_type(&variant)),
value_text: Some(variant.to_string()),
quality: crate::telemetry::PointQuality::Good,
protocol: "opcua".to_string(),
timestamp: Some(Utc::now()),
scan_mode: crate::model::ScanMode::Poll,
},
)) {
tracing::warn!(
"Batch write succeeded but failed to dispatch point update for point {}: {}",
point_id,
e
);
}
}
}
Ok(Self::write_value_batch_result(true, None, results))
}
pub async fn subscribe_points_from_source(
&self,
source_id: Uuid,
point_ids: Option<Vec<Uuid>>,
pool: &sqlx::PgPool,
) -> Result<HashMap<String, usize>, String> {
let point_ids = point_ids.unwrap_or_default();
let mut points = crate::service::get_points_with_ids(pool, source_id, &point_ids)
.await
.map_err(|e| format!("Failed to get points for source {}: {}", source_id, e))?;
{
let mut cache = self.point_write_target_cache.write().await;
for p in &points {
cache.insert(
p.point_id,
PointWriteTarget {
source_id,
external_id: p.external_id.clone(),
},
);
}
}
if points.is_empty() {
tracing::info!("No valid points found to subscribe for source {}", source_id);
return Ok(Self::subscription_result(0, 0));
}
tracing::info!(
"Processing subscription for source {} with {} points",
source_id,
points.len()
);
let session = self
.get_session(source_id)
.await
.ok_or_else(|| format!("Failed to get session for source {}", source_id))?;
let subscription_id = {
let status = self.status.read().await;
if let Some(conn_status) = status.get(&source_id) {
let subscribed_point_ids: std::collections::HashSet<Uuid> =
conn_status.client_handle_map.values().copied().collect();
let original_count = points.len();
points.retain(|p| !subscribed_point_ids.contains(&p.point_id));
let filtered_count = points.len();
if filtered_count < original_count {
tracing::info!(
"Filtered out {} already subscribed points for source {}",
original_count - filtered_count,
source_id
);
}
if points.is_empty() {
tracing::info!("All points are already subscribed for source {}", source_id);
return Ok(Self::subscription_result(0, 0));
}
conn_status.subscription_id
} else {
None
}
};
let subscription_id = match subscription_id {
Some(id) => Some(id),
None => {
let manager = self.clone();
let current_source_id = source_id;
match session
.create_subscription(
Duration::from_secs(1),
10,
30,
0,
0,
true,
opcua::client::DataChangeCallback::new(move |dv, item| {
let client_handle = item.client_handle();
let val = dv.value;
let timex = Some(Utc::now());
let unified_value =
val.as_ref().map(crate::telemetry::opcua_variant_to_data);
let unified_value_type =
val.as_ref().map(crate::telemetry::opcua_variant_type);
let unified_value_text = val.as_ref().map(|v| v.to_string());
let quality = dv.status
.as_ref()
.map(crate::telemetry::PointQuality::from_status_code)
.unwrap_or(crate::telemetry::PointQuality::Unknown);
if let Some(event_manager) = &manager.event_manager {
let _ = event_manager.send(crate::event::ReloadEvent::PointNewValue(
crate::telemetry::PointNewValue {
source_id: current_source_id,
point_id: None,
client_handle,
value: unified_value,
value_type: unified_value_type,
value_text: unified_value_text,
quality,
protocol: "opcua".to_string(),
timestamp: timex,
scan_mode: ScanMode::Subscribe,
}));
}
}),
)
.await
{
Ok(id) => {
if let Some(conn_status) = manager.status.write().await.get_mut(&source_id) {
conn_status.subscription_id = Some(id);
}
tracing::info!("Created subscription {} for source {}", id, source_id);
Some(id)
}
Err(e) => {
tracing::warn!(
"Failed to create subscription for source {}, falling back to poll mode: {:?}",
source_id,
e
);
None
}
}
}
};
if subscription_id.is_none() {
let polled_count = self
.add_points_to_poll_list(source_id, &points)
.await;
return Ok(Self::subscription_result(0, polled_count));
}
let subscription_id = subscription_id.unwrap_or_default();
let mut client_handle_seed: u32 = self
.status
.read()
.await
.get(&source_id)
.map_or(1000, |s| s.next_client_handle);
let mut items_to_create: Vec<MonitoredItemCreateRequest> = Vec::new();
let mut item_points: Vec<PointSubscriptionInfo> = Vec::new();
for p in points.iter().cloned() {
let node_id = NodeId::from_str(&p.external_id)
.map_err(|e| format!("Invalid node id {}: {}", p.external_id, e))?;
let client_handle = client_handle_seed;
client_handle_seed += 1;
if let Some(s) = self.status.write().await.get_mut(&source_id) {
s.client_handle_map.insert(client_handle, p.point_id);
}
let request = MonitoredItemCreateRequest {
item_to_monitor: ReadValueId {
node_id,
attribute_id: AttributeId::Value as u32,
index_range: Default::default(),
data_encoding: Default::default(),
},
monitoring_mode: MonitoringMode::Reporting,
requested_parameters: MonitoringParameters {
client_handle,
sampling_interval: 0.0,
filter: Default::default(),
queue_size: 10,
discard_oldest: true,
},
};
items_to_create.push(request);
item_points.push(p);
}
let results = match session
.create_monitored_items(subscription_id, TimestampsToReturn::Both, items_to_create)
.await
{
Ok(results) => results,
Err(e) => {
tracing::warn!(
"Failed to create monitored items for source {}, falling back to poll mode: {:?}",
source_id,
e
);
let item_point_ids: HashSet<Uuid> = item_points.iter().map(|p| p.point_id).collect();
if let Some(conn_status) = self.status.write().await.get_mut(&source_id) {
conn_status
.client_handle_map
.retain(|_, point_id| !item_point_ids.contains(point_id));
conn_status.next_client_handle = client_handle_seed;
}
let polled_count = self
.add_points_to_poll_list(source_id, &item_points)
.await;
return Ok(Self::subscription_result(0, polled_count));
}
};
let mut successfully_subscribed_points = Vec::new();
let mut successfully_subscribed_set: HashSet<Uuid> = HashSet::new();
let mut failed_points: Vec<PointSubscriptionInfo> = Vec::new();
for (i, monitored_item_result) in results.iter().enumerate() {
if i >= item_points.len() {
break;
}
let point = &item_points[i];
if monitored_item_result.result.status_code.is_good() {
successfully_subscribed_points.push(point.point_id);
successfully_subscribed_set.insert(point.point_id);
if let Some(conn_status) = self.status.write().await.get_mut(&source_id) {
conn_status
.monitored_item_map
.insert(point.point_id, monitored_item_result.result.monitored_item_id);
// 从轮询列表中移除该点
conn_status.poll_points.retain(|p| p.point_id != point.point_id);
}
} else {
tracing::error!(
"Failed to create monitored item for point {}: {:?}",
point.point_id,
monitored_item_result.result.status_code
);
failed_points.push(point.clone());
}
}
if results.len() < item_points.len() {
for point in item_points.iter().skip(results.len()) {
tracing::warn!(
"No monitored item result returned for point {}, fallback to poll",
point.point_id
);
failed_points.push(point.clone());
}
}
failed_points.retain(|p| !successfully_subscribed_set.contains(&p.point_id));
if let Some(conn_status) = self.status.write().await.get_mut(&source_id) {
let failed_point_ids: HashSet<Uuid> = failed_points.iter().map(|p| p.point_id).collect();
conn_status
.client_handle_map
.retain(|_, point_id| !failed_point_ids.contains(point_id));
conn_status.next_client_handle = client_handle_seed;
}
let polled_count = self
.add_points_to_poll_list(source_id, &failed_points)
.await;
Ok(Self::subscription_result(
successfully_subscribed_points.len(),
polled_count,
))
}
///
pub async fn unsubscribe_points_from_source(
&self,
source_id: Uuid,
point_ids: Vec<Uuid>,
) -> Result<usize, String> {
if point_ids.is_empty() {
return Ok(0);
}
let target_ids: std::collections::HashSet<Uuid> = point_ids.into_iter().collect();
let session = self.get_session(source_id).await;
let Some(session) = session else {
return Ok(0);
};
let (subscription_id, point_item_pairs) = {
let status = self.status.read().await;
let Some(conn_status) = status.get(&source_id) else {
return Ok(0);
};
let Some(subscription_id) = conn_status.subscription_id else {
return Ok(0);
};
let items: Vec<(Uuid, u32)> = conn_status
.monitored_item_map
.iter()
.filter(|(point_id, _)| target_ids.contains(point_id))
.map(|(point_id, monitored_item_id)| (*point_id, *monitored_item_id))
.collect();
(subscription_id, items)
};
if point_item_pairs.is_empty() {
return Ok(0);
}
let monitored_item_ids: Vec<u32> =
point_item_pairs.iter().map(|(_, item_id)| *item_id).collect();
let status_codes = session
.delete_monitored_items(subscription_id, &monitored_item_ids)
.await
.map_err(|e| {
format!(
"Failed to unsubscribe monitored items for source {}: {:?}",
source_id, e
)
})?;
let mut removed_point_ids: Vec<Uuid> = Vec::new();
for (idx, status_code) in status_codes.iter().enumerate() {
if idx >= point_item_pairs.len() {
break;
}
if status_code.is_good() {
removed_point_ids.push(point_item_pairs[idx].0);
} else {
tracing::warn!(
"Failed to unsubscribe monitored item {} for source {}: {:?}",
point_item_pairs[idx].1,
source_id,
status_code
);
}
}
if removed_point_ids.is_empty() {
return Ok(0);
}
let removed_set: std::collections::HashSet<Uuid> =
removed_point_ids.iter().copied().collect();
if let Some(conn_status) = self.status.write().await.get_mut(&source_id) {
for point_id in &removed_point_ids {
conn_status.monitored_item_map.remove(point_id);
}
conn_status
.client_handle_map
.retain(|_, point_id| !removed_set.contains(point_id));
}
let _ = self
.remove_point_write_target_cache_by_point_ids(&removed_point_ids)
.await;
{
let mut monitor_data = self.point_monitor_data.write().await;
for point_id in &removed_point_ids {
monitor_data.remove(point_id);
}
}
{
let mut history_data = self.point_history_data.write().await;
for point_id in &removed_point_ids {
history_data.remove(point_id);
}
}
// 从轮询列表中移除传入的点,并记录移除的轮询点数量
let polling_removed_count = {
let mut status = self.status.write().await;
if let Some(conn_status) = status.get_mut(&source_id) {
let before_count = conn_status.poll_points.len();
conn_status.poll_points.retain(|p| !target_ids.contains(&p.point_id));
let after_count = conn_status.poll_points.len();
before_count - after_count
} else {
0
}
};
// 计算从订阅点和轮询点移除的总数
let total_removed = removed_point_ids.len() + polling_removed_count;
tracing::info!(
"Unsubscribed {} points (subscription: {}, polling: {}) from source {}",
total_removed,
removed_point_ids.len(),
polling_removed_count,
source_id
);
Ok(removed_point_ids.len())
}
}