use crate::cip::{CipConnection, CipOptions, GenericMessage, RoutePath};
use crate::epath::{LogicalValue, encode_epath, parse_cip_route};
use crate::identity::ModuleIdentity;
use crate::reader::Reader;
use crate::transport::Connector;
use crate::types::Elementary;
use crate::{Error, Result, Value};
use std::net::Ipv4Addr;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, mpsc};
pub const PORT_OBJECT: u16 = 0xF4;
const TCP_IP_INTERFACE: u16 = 0xF5;
const ETHERNET_LINK: u16 = 0xF6;
const GET_ATTRIBUTES_ALL: u8 = 0x01;
const GET_ATTRIBUTE_SINGLE: u8 = 0x0E;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PortInfo {
pub number: u16,
pub port_type: u16,
pub name: String,
pub address: Option<String>,
pub node_range: Option<(u16, u16)>,
pub ip: Option<(Ipv4Addr, Ipv4Addr)>,
}
impl PortInfo {
pub fn type_name(&self) -> String {
port_type_name(self.port_type)
}
pub fn nodes(&self) -> Option<Vec<String>> {
let range = |first: u16, last: u16| Some((first..=last).map(|n| n.to_string()).collect());
if let Some((first, last)) = self.node_range {
return range(first, last);
}
match (self.port_type, self.ip) {
(4, Some((ip, mask))) => Some(subnet_hosts(ip, mask)),
(1, _) => range(0, 16), (2 | 3, _) => range(1, 99), (5, _) => range(0, 63), _ => None,
}
}
}
fn subnet_hosts(ip: Ipv4Addr, mask: Ipv4Addr) -> Vec<String> {
let mask = u32::from(mask).max(0xFFFF_FF00);
let network = u32::from(ip) & mask;
let broadcast = network | !mask;
(network.saturating_add(1)..broadcast)
.map(|a| Ipv4Addr::from(a).to_string())
.collect()
}
fn link_instance(data: &[u8]) -> Option<u16> {
let mut r = Reader::new(data);
r.u16().ok()?;
let (mut class, mut instance) = (None, None);
while let Ok(segment) = r.u8() {
match segment {
0x20 => class = Some(u16::from(r.u8().ok()?)),
0x21 => class = r.u8().and_then(|_| r.u16()).ok(),
0x24 => instance = Some(u16::from(r.u8().ok()?)),
0x25 => instance = r.u8().and_then(|_| r.u16()).ok(),
_ => break,
}
}
(class? == ETHERNET_LINK).then_some(instance?)
}
pub fn port_type_name(port_type: u16) -> String {
match port_type {
0 => "None".into(),
1 => "Backplane".into(),
2 => "ControlNet".into(),
3 => "ControlNet (redundant)".into(),
4 => "EtherNet/IP".into(),
5 => "DeviceNet".into(),
200 => "CompoNet".into(),
201 => "Modbus/TCP".into(),
202 => "Modbus/SL".into(),
203 => "SERCOS III".into(),
65535 => "No active link".into(),
n @ 100..=199 => format!("Vendor specific ({n})"),
n => format!("Type {n}"),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Node {
pub route: String,
pub address: String,
pub identity: Option<ModuleIdentity>,
pub error: Option<String>,
}
pub fn child_route(route: &str, port: u16, address: &str) -> String {
let route = route.trim_matches('/');
if route.is_empty() {
format!("{port}/{address}")
} else {
format!("{route}/{port}/{address}")
}
}
fn port_segment(data: &[u8]) -> Option<(u16, String)> {
let first = *data.first()?;
if first & 0xE0 != 0 {
return None; }
let mut at = 1;
let link_size = if first & 0x10 != 0 {
at += 1;
usize::from(*data.get(1)?)
} else {
1
};
let port = if first & 0x0F == 0x0F {
at += 2;
u16::from_le_bytes([*data.get(at - 2)?, *data.get(at - 1)?])
} else {
u16::from(first & 0x0F)
};
let link = data.get(at..at + link_size)?;
let link = match link {
[n] => n.to_string(),
text => String::from_utf8_lossy(text).into_owned(),
};
Some((port, link))
}
impl CipConnection {
fn request_at(
&mut self,
route: &str,
service: u8,
class: u16,
instance: u16,
attribute: Option<u16>,
name: &str,
) -> Result<std::result::Result<Vec<u8>, String>> {
let route = route.trim_matches('/');
let route_path = if route.is_empty() {
RoutePath::None
} else {
RoutePath::Encoded(encode_epath(
&parse_cip_route(route, false)?,
true,
true,
true,
)?)
};
let msg = GenericMessage {
service,
class_code: LogicalValue::Int(class.into()),
instance: LogicalValue::Int(instance.into()),
attribute: attribute.map(|a| LogicalValue::Int(a.into())),
name: name.into(),
connected: false,
unconnected_send: !route.is_empty(),
route_path,
..GenericMessage::default()
};
let reply = self.generic_message(&msg)?;
Ok(match reply.tag.error {
Some(err) => Err(err.message),
None => Ok(reply.data().unwrap_or_default().to_vec()),
})
}
pub fn identity_at(&mut self, route: &str) -> Result<ModuleIdentity> {
let data = self
.request_at(route, GET_ATTRIBUTES_ALL, 0x01, 1, None, "identity")?
.map_err(Error::response)?;
ModuleIdentity::decode(&mut Reader::new(&data))
}
pub fn ports_at(&mut self, route: &str) -> Result<Vec<PortInfo>> {
let get = |conn: &mut Self, instance: u16, attribute: u16| {
conn.request_at(
route,
GET_ATTRIBUTE_SINGLE,
PORT_OBJECT,
instance,
Some(attribute),
"port",
)
.map(|r| r.ok())
};
let Some(max) = get(self, 0, 2)? else {
return Ok(Vec::new());
};
let max = Reader::new(&max).u16().unwrap_or(0);
let mut ports = Vec::new();
for instance in 1..=max {
let Some(port_type) = get(self, instance, 1)? else {
continue;
};
let Ok(port_type) = Reader::new(&port_type).u16() else {
continue;
};
let number = get(self, instance, 2)?
.and_then(|d| Reader::new(&d).u16().ok())
.unwrap_or(instance);
let name = match get(self, instance, 4)? {
Some(d) => match Elementary::ShortString.decode(&mut Reader::new(&d)) {
Ok(Value::String(s)) => s,
_ => String::new(),
},
None => String::new(),
};
let address = get(self, instance, 7)?
.and_then(|d| port_segment(&d))
.map(|(_, link)| link);
let node_range = get(self, instance, 8)?.and_then(|d| {
let mut r = Reader::new(&d);
Some((r.u16().ok()?, r.u16().ok()?))
});
let ip = if port_type == 4 {
let link = get(self, instance, 3)?.and_then(|d| link_instance(&d));
self.port_ip(route, link.unwrap_or(1))?
} else {
None
};
ports.push(PortInfo {
number,
port_type,
name,
address,
node_range,
ip,
});
}
Ok(ports)
}
fn port_ip(&mut self, route: &str, instance: u16) -> Result<Option<(Ipv4Addr, Ipv4Addr)>> {
let reply = self.request_at(
route,
GET_ATTRIBUTE_SINGLE,
TCP_IP_INTERFACE,
instance,
Some(5),
"tcp/ip",
)?;
Ok(reply.ok().and_then(|d| {
let mut r = Reader::new(&d);
let (ip, mask) = (Ipv4Addr::from(r.u32().ok()?), Ipv4Addr::from(r.u32().ok()?));
(!ip.is_unspecified()).then_some((ip, mask))
}))
}
pub fn scan(&mut self, route: &str, port: u16, nodes: Option<&[String]>) -> Result<Scan> {
let nodes = match nodes {
Some(nodes) => nodes.to_vec(),
None => self
.ports_at(route)?
.into_iter()
.find(|p| p.number == port)
.and_then(|p| p.nodes())
.unwrap_or_default(),
};
Ok(Scan {
host: format!("{}:{}", self.host(), self.port()),
socket_timeout: self.socket_timeout(),
connector: Arc::new(self.share_connector()),
route: route.to_owned(),
port,
nodes,
})
}
}
pub struct Scan {
host: String,
socket_timeout: Option<std::time::Duration>,
connector: Arc<Connector>,
route: String,
port: u16,
nodes: Vec<String>,
}
impl Scan {
pub fn nodes(&self) -> &[String] {
&self.nodes
}
pub fn run(self, workers: usize, found: &mut dyn FnMut(Node) -> bool) {
let (next, stop) = (AtomicUsize::new(0), AtomicBool::new(false));
let (tx, rx) = mpsc::channel();
let scan = &self;
std::thread::scope(|threads| {
for _ in 0..workers.clamp(1, self.nodes.len().max(1)) {
let (tx, next, stop) = (tx.clone(), &next, &stop);
threads.spawn(move || {
let mut conn = None;
while !stop.load(Ordering::Relaxed) {
let Some(address) = scan.nodes.get(next.fetch_add(1, Ordering::Relaxed))
else {
break;
};
if tx.send(scan.probe(&mut conn, address)).is_err() {
break;
}
}
if let Some(mut conn) = conn {
let _ = conn.close();
}
});
}
drop(tx);
for node in rx.iter() {
if !found(node) {
stop.store(true, Ordering::Relaxed);
break;
}
}
drop(rx); });
}
pub fn collect(self, workers: usize) -> Vec<Node> {
let order: std::collections::HashMap<String, usize> = (self.nodes.iter().cloned())
.enumerate()
.map(|(i, a)| (a, i))
.collect();
let mut nodes = Vec::with_capacity(self.nodes.len());
self.run(workers, &mut |node| {
nodes.push(node);
true
});
nodes.sort_by_key(|n| order.get(&n.address).copied());
nodes
}
fn probe(&self, conn: &mut Option<CipConnection>, address: &str) -> Node {
let route = child_route(&self.route, self.port, address);
let result = (|| {
if conn.is_none() {
let options = CipOptions {
socket_timeout: self.socket_timeout,
..CipOptions::default()
};
let connector = Arc::clone(&self.connector);
let connector: Connector = Box::new(move |h, p, t| connector(h, p, t));
let mut c = CipConnection::new(&self.host, options, connector, false)?;
if !c.open()? {
return Err(Error::comm("failed to register a session"));
}
*conn = Some(c);
}
let c = conn.as_mut().ok_or_else(|| Error::comm("no connection"))?;
c.identity_at(&route)
})();
let (identity, error) = match result {
Ok(identity) => (Some(identity), None),
Err(err) => {
if matches!(err, Error::Comm(_)) {
*conn = None;
}
(None, Some(err.to_string()))
}
};
Node {
route,
address: address.to_owned(),
identity,
error,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn port_segments_format_as_port_and_link() {
let seg = |data: &[u8]| port_segment(data).map(|(p, l)| format!("{p}/{l}"));
assert_eq!(seg(&[0x01, 0x03]).as_deref(), Some("1/3"));
let mut ip = vec![0x12, 8];
ip.extend(b"10.0.0.1");
assert_eq!(seg(&ip).as_deref(), Some("2/10.0.0.1"));
assert_eq!(seg(&[0x0F, 0x12, 0x00, 0x05]).as_deref(), Some("18/5"));
assert_eq!(seg(&[0x20, 0x01]), None);
assert_eq!(seg(&[]), None);
}
#[test]
fn child_routes_extend_the_route() {
assert_eq!(child_route("", 1, "3"), "1/3");
assert_eq!(child_route("1/2/", 2, "10.0.0.5"), "1/2/2/10.0.0.5");
}
#[test]
fn ports_without_a_node_range_use_their_type_default() {
let port = |port_type, node_range, ip| PortInfo {
number: 1,
port_type,
name: String::new(),
address: None,
node_range,
ip,
};
let span = |nodes: Option<Vec<String>>| {
nodes.map(|n| (n.len(), n[0].clone(), n[n.len() - 1].clone()))
};
let s = |t: &str| t.to_owned();
assert_eq!(
span(port(1, None, None).nodes()),
Some((17, s("0"), s("16")))
);
assert_eq!(
span(port(1, Some((0, 9)), None).nodes()),
Some((10, s("0"), s("9")))
);
assert_eq!(
span(port(5, None, None).nodes()),
Some((64, s("0"), s("63")))
);
assert_eq!(port(4, None, None).nodes(), None);
let ip = |a: &str, m: &str| Some((a.parse().expect("ip"), m.parse().expect("mask")));
let enet = port(4, None, ip("192.168.1.5", "255.255.255.0")).nodes();
assert_eq!(
span(enet),
Some((254, s("192.168.1.1"), s("192.168.1.254")))
);
let wide = port(4, None, ip("10.20.30.40", "255.255.0.0")).nodes();
assert_eq!(span(wide), Some((254, s("10.20.30.1"), s("10.20.30.254"))));
let small = port(4, None, ip("10.0.0.70", "255.255.255.192")).nodes();
assert_eq!(span(small), Some((62, s("10.0.0.65"), s("10.0.0.126"))));
}
#[test]
fn link_object_paths_name_the_ethernet_link_instance() {
assert_eq!(link_instance(&[2, 0, 0x20, 0xF6, 0x24, 0x02]), Some(2));
assert_eq!(
link_instance(&[3, 0, 0x20, 0xF6, 0x25, 0, 0x01, 0x01]),
Some(0x101)
);
assert_eq!(link_instance(&[2, 0, 0x20, 0xF5, 0x24, 0x01]), None);
assert_eq!(link_instance(&[]), None);
}
}