rust_ethernet_ip/
python.rs

1use pyo3::prelude::*;
2use pyo3::wrap_pyfunction;
3use pyo3::types::{PyDict, PyList, PyTuple};
4use pyo3::types::IntoPyDict;
5use tokio::runtime::Runtime;
6use std::collections::HashMap;
7use crate::{
8    EipClient, PlcValue, BatchOperation, BatchResult, BatchError,
9    SubscriptionOptions, TagSubscription, Result as EipResult
10};
11
12/// Python module for rust_ethernet_ip
13#[pymodule]
14fn rust_ethernet_ip(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
15    m.add_class::<PyEipClient>()?;
16    m.add_class::<PyPlcValue>()?;
17    m.add_class::<PySubscriptionOptions>()?;
18    Ok(())
19}
20
21/// Python wrapper for EipClient
22#[pyclass]
23struct PyEipClient {
24    client: EipClient,
25    runtime: Runtime,
26}
27
28// Newtype for (String, PyPlcValue)
29struct TagValueArg {
30    name: String,
31    value: PyPlcValue,
32}
33
34impl<'a> FromPyObject<'a> for TagValueArg {
35    fn extract(ob: &'a pyo3::PyAny) -> PyResult<Self> {
36        let tuple = ob.downcast::<PyTuple>()?;
37        if tuple.len() != 2 {
38            return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
39                "Expected tuple of length 2"
40            ));
41        }
42        let name = tuple.get_item(0)?.extract::<String>()?;
43        let value = tuple.get_item(1)?.extract::<PyPlcValue>()?;
44        Ok(TagValueArg { name, value })
45    }
46}
47
48// Newtype for (String, PySubscriptionOptions)
49struct TagSubOptArg {
50    name: String,
51    options: PySubscriptionOptions,
52}
53
54impl<'a> FromPyObject<'a> for TagSubOptArg {
55    fn extract(ob: &'a pyo3::PyAny) -> PyResult<Self> {
56        let tuple = ob.downcast::<PyTuple>()?;
57        if tuple.len() != 2 {
58            return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
59                "Expected tuple of length 2"
60            ));
61        }
62        let name = tuple.get_item(0)?.extract::<String>()?;
63        let options = tuple.get_item(1)?.extract::<PySubscriptionOptions>()?;
64        Ok(TagSubOptArg { name, options })
65    }
66}
67
68#[pymethods]
69impl PyEipClient {
70    /// Create a new EipClient instance
71    #[new]
72    fn new(addr: &str) -> PyResult<Self> {
73        let runtime = Runtime::new().unwrap();
74        let client = runtime.block_on(async {
75            EipClient::connect(addr).await
76        }).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
77        
78        Ok(PyEipClient { client, runtime })
79    }
80
81    /// Read a tag value
82    fn read_tag(&mut self, tag_name: &str) -> PyResult<PyPlcValue> {
83        let value = self.runtime.block_on(async {
84            self.client.read_tag(tag_name).await
85        }).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
86        
87        Ok(PyPlcValue { value })
88    }
89
90    /// Write a value to a tag
91    fn write_tag(&mut self, tag_name: &str, value: &PyPlcValue) -> PyResult<bool> {
92        let result = self.runtime.block_on(async {
93            self.client.write_tag(tag_name, value.value.clone()).await
94        });
95        match result {
96            Ok(_) => Ok(true),
97            Err(e) => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string())),
98        }
99    }
100
101    /// Read multiple tags in batch
102    fn read_tags_batch(&mut self, tag_names: Vec<String>) -> PyResult<Vec<(String, PyObject)>> {
103        Python::with_gil(|py| {
104            let runtime = tokio::runtime::Runtime::new().unwrap();
105            let results = runtime.block_on(async {
106                self.client.read_tags_batch(&tag_names.iter().map(|s| s.as_str()).collect::<Vec<_>>()).await
107            }).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
108            Ok(results.into_iter().map(|(name, result)| {
109                let obj = match result {
110                    Ok(v) => PyPlcValue { value: v }.into_py(py),
111                    Err(e) => PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()).into_py(py),
112                };
113                (name, obj)
114            }).collect())
115        })
116    }
117
118    /// Write multiple tags in batch
119    fn write_tags_batch(&mut self, tag_values: Vec<TagValueArg>) -> PyResult<Vec<(String, PyObject)>> {
120        Python::with_gil(|py| {
121            let runtime = tokio::runtime::Runtime::new().unwrap();
122            let results = runtime.block_on(async {
123                self.client.write_tags_batch(&tag_values.iter()
124                    .map(|arg| (arg.name.as_str(), arg.value.value.clone()))
125                    .collect::<Vec<_>>()).await
126            }).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
127            Ok(results.into_iter().map(|(name, result)| {
128                let obj = match result {
129                    Ok(()) => py.None(),
130                    Err(e) => PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()).into_py(py),
131                };
132                (name, obj)
133            }).collect())
134        })
135    }
136
137    /// Subscribe to a tag
138    fn subscribe_to_tag(&self, tag_path: &str, options: &PySubscriptionOptions) -> PyResult<()> {
139        self.runtime.block_on(async {
140            self.client.subscribe_to_tag(tag_path, options.options.clone()).await
141        }).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
142        
143        Ok(())
144    }
145
146    /// Subscribe to multiple tags
147    fn subscribe_to_tags(&self, tags: Vec<TagSubOptArg>) -> PyResult<()> {
148        self.runtime.block_on(async {
149            self.client.subscribe_to_tags(&tags.iter()
150                .map(|arg| (arg.name.as_str(), arg.options.options.clone()))
151                .collect::<Vec<_>>()).await
152        }).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
153        Ok(())
154    }
155
156    /// Unregister the session
157    fn unregister_session(&mut self) -> PyResult<()> {
158        self.runtime.block_on(async {
159            self.client.unregister_session().await
160        }).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
161        
162        Ok(())
163    }
164}
165
166/// Python wrapper for PlcValue
167#[pyclass]
168struct PyPlcValue {
169    value: PlcValue,
170}
171
172impl FromPyObject<'_> for PyPlcValue {
173    fn extract(ob: &PyAny) -> PyResult<Self> {
174        if let Ok(bool_val) = ob.extract::<bool>() {
175            Ok(PyPlcValue { value: PlcValue::Bool(bool_val) })
176        } else if let Ok(int_val) = ob.extract::<i32>() {
177            Ok(PyPlcValue { value: PlcValue::Dint(int_val) })
178        } else if let Ok(float_val) = ob.extract::<f64>() {
179            Ok(PyPlcValue { value: PlcValue::Lreal(float_val) })
180        } else if let Ok(string_val) = ob.extract::<String>() {
181            Ok(PyPlcValue { value: PlcValue::String(string_val) })
182        } else if let Ok(dict) = ob.downcast::<PyDict>() {
183            let mut map = HashMap::new();
184            for (key, value) in dict.iter() {
185                let key = key.extract::<String>()?;
186                let value = value.extract::<PyPlcValue>()?.value;
187                map.insert(key, value);
188            }
189            Ok(PyPlcValue { value: PlcValue::Udt(map) })
190        } else {
191            Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
192                "Unsupported value type"
193            ))
194        }
195    }
196}
197
198#[pymethods]
199impl PyPlcValue {
200    #[new]
201    fn new(value: PyObject) -> PyResult<Self> {
202        Python::with_gil(|py| {
203            if let Ok(val) = value.extract::<bool>(py) {
204                Ok(PyPlcValue { value: PlcValue::Bool(val) })
205            } else if let Ok(val) = value.extract::<i32>(py) {
206                Ok(PyPlcValue { value: PlcValue::Dint(val) })
207            } else if let Ok(val) = value.extract::<f32>(py) {
208                Ok(PyPlcValue { value: PlcValue::Real(val) })
209            } else if let Ok(val) = value.extract::<f64>(py) {
210                Ok(PyPlcValue { value: PlcValue::Real(val as f32) })
211            } else if let Ok(val) = value.extract::<String>(py) {
212                Ok(PyPlcValue { value: PlcValue::String(val) })
213            } else {
214                Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>("Unsupported value type"))
215            }
216        })
217    }
218
219    #[staticmethod]
220    fn real(val: f32) -> Self {
221        PyPlcValue { value: PlcValue::Real(val) }
222    }
223    #[staticmethod]
224    fn lreal(val: f64) -> Self {
225        PyPlcValue { value: PlcValue::Lreal(val) }
226    }
227    #[staticmethod]
228    fn dint(val: i32) -> Self {
229        PyPlcValue { value: PlcValue::Dint(val) }
230    }
231    #[staticmethod]
232    fn lint(val: i64) -> Self {
233        PyPlcValue { value: PlcValue::Lint(val) }
234    }
235    #[staticmethod]
236    fn string(val: String) -> Self {
237        PyPlcValue { value: PlcValue::String(val) }
238    }
239
240    #[getter]
241    fn value(&self, py: Python) -> PyResult<PyObject> {
242        match &self.value {
243            PlcValue::Bool(b) => Ok(b.into_py(py)),
244            PlcValue::Sint(i) => Ok(i.into_py(py)),
245            PlcValue::Int(i) => Ok(i.into_py(py)),
246            PlcValue::Dint(i) => Ok(i.into_py(py)),
247            PlcValue::Lint(i) => Ok(i.into_py(py)),
248            PlcValue::Usint(u) => Ok(u.into_py(py)),
249            PlcValue::Uint(u) => Ok(u.into_py(py)),
250            PlcValue::Udint(u) => Ok(u.into_py(py)),
251            PlcValue::Ulint(u) => Ok(u.into_py(py)),
252            PlcValue::Real(f) => Ok(f.into_py(py)),
253            PlcValue::Lreal(f) => Ok(f.into_py(py)),
254            PlcValue::String(s) => Ok(s.into_py(py)),
255            PlcValue::Udt(map) => {
256                let dict = PyDict::new(py);
257                for (k, v) in map.iter() {
258                    let v_py = PyPlcValue { value: v.clone() }.value(py)?;
259                    dict.set_item(k, v_py)?;
260                }
261                Ok(dict.into_py(py))
262            }
263        }
264    }
265
266    fn __str__(&self) -> String {
267        format!("{:?}", self.value)
268    }
269
270    fn __repr__(&self) -> String {
271        format!("PyPlcValue({:?})", self.value)
272    }
273}
274
275/// Python wrapper for SubscriptionOptions
276#[pyclass]
277struct PySubscriptionOptions {
278    options: SubscriptionOptions,
279}
280
281impl FromPyObject<'_> for PySubscriptionOptions {
282    fn extract(ob: &PyAny) -> PyResult<Self> {
283        let update_rate = ob.getattr("update_rate")?.extract::<u32>()?;
284        let change_threshold = ob.getattr("change_threshold")?.extract::<f32>()?;
285        let timeout = ob.getattr("timeout")?.extract::<u32>()?;
286        
287        Ok(PySubscriptionOptions {
288            options: SubscriptionOptions {
289                update_rate,
290                change_threshold,
291                timeout,
292            }
293        })
294    }
295}
296
297#[pymethods]
298impl PySubscriptionOptions {
299    #[new]
300    fn new(update_rate: u32, change_threshold: f32, timeout: u32) -> PyResult<Self> {
301        let options = SubscriptionOptions {
302            update_rate,
303            change_threshold,
304            timeout,
305        };
306        
307        Ok(PySubscriptionOptions { options })
308    }
309
310    /// Get the update rate in milliseconds
311    #[getter]
312    fn update_rate(&self) -> u32 {
313        self.options.update_rate
314    }
315
316    /// Get the change threshold for numeric values
317    #[getter]
318    fn change_threshold(&self) -> f32 {
319        self.options.change_threshold
320    }
321
322    /// Get the timeout in milliseconds
323    #[getter]
324    fn timeout(&self) -> u32 {
325        self.options.timeout
326    }
327}