eip2nats 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eip2nats-1.0.0/.gitignore +67 -0
- eip2nats-1.0.0/LICENSE +21 -0
- eip2nats-1.0.0/PKG-INFO +333 -0
- eip2nats-1.0.0/README.md +304 -0
- eip2nats-1.0.0/eip2nats/EIPtoNATSBridge.cpp +321 -0
- eip2nats-1.0.0/eip2nats/EIPtoNATSBridge.h +141 -0
- eip2nats-1.0.0/eip2nats/__init__.py +42 -0
- eip2nats-1.0.0/eip2nats/bindings.cpp +66 -0
- eip2nats-1.0.0/pyproject.toml +95 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
pip-wheel-metadata/
|
|
20
|
+
share/python-wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
|
|
26
|
+
# Virtual Environment
|
|
27
|
+
venv/
|
|
28
|
+
env/
|
|
29
|
+
ENV/
|
|
30
|
+
.venv/
|
|
31
|
+
|
|
32
|
+
# PyPI
|
|
33
|
+
.pypirc
|
|
34
|
+
|
|
35
|
+
# Hatch
|
|
36
|
+
.hatch/
|
|
37
|
+
.env
|
|
38
|
+
|
|
39
|
+
# Testing
|
|
40
|
+
.pytest_cache/
|
|
41
|
+
.coverage
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.hypothesis/
|
|
45
|
+
|
|
46
|
+
# IDEs
|
|
47
|
+
.vscode/
|
|
48
|
+
.idea/
|
|
49
|
+
*.swp
|
|
50
|
+
*.swo
|
|
51
|
+
*~
|
|
52
|
+
.DS_Store
|
|
53
|
+
|
|
54
|
+
# Project specific
|
|
55
|
+
# Mantener la estructura pero ignorar archivos compilados
|
|
56
|
+
src/eip2nats/lib/*.so*
|
|
57
|
+
src/eip2nats/lib/*.a
|
|
58
|
+
|
|
59
|
+
# Build artifacts
|
|
60
|
+
build/dependencies/
|
|
61
|
+
*.cmake
|
|
62
|
+
CMakeCache.txt
|
|
63
|
+
CMakeFiles/
|
|
64
|
+
Makefile
|
|
65
|
+
|
|
66
|
+
# Logs
|
|
67
|
+
*.log
|
eip2nats-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
eip2nats-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eip2nats
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: EtherNet/IP to NATS Bridge with bundled dependencies
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/eip2nats
|
|
6
|
+
Project-URL: Documentation, https://github.com/yourusername/eip2nats/blob/main/README.md
|
|
7
|
+
Project-URL: Repository, https://github.com/yourusername/eip2nats
|
|
8
|
+
Author-email: Your Name <your.email@example.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: C++
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.7
|
|
23
|
+
Requires-Dist: pybind11>=2.6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: black>=22.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.0.243; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# eip2nats - EtherNet/IP to NATS Bridge
|
|
31
|
+
|
|
32
|
+
Puente completo entre dispositivos EtherNet/IP (PLCs) y servidores NATS, **con todas las dependencias incluidas** en el wheel.
|
|
33
|
+
|
|
34
|
+
## ✨ Características
|
|
35
|
+
|
|
36
|
+
- ✅ **Self-contained**: Incluye libnats y libEIPScanner compiladas
|
|
37
|
+
- ✅ **Zero dependencies**: No requiere instalación de librerías del sistema
|
|
38
|
+
- ✅ **Entorno virtual**: Compatible con Raspberry Pi OS (sin pip install global)
|
|
39
|
+
- ✅ **Simple instalación**: Setup automático con un comando
|
|
40
|
+
- ✅ **Alto rendimiento**: Bindings nativos C++ con pybind11
|
|
41
|
+
- ✅ **Thread-safe**: Manejo seguro de múltiples conexiones
|
|
42
|
+
|
|
43
|
+
## 🚀 Instalación Rápida
|
|
44
|
+
|
|
45
|
+
### Setup Completo Automático
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
./setup_project.sh
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Esto hace TODO automáticamente:
|
|
52
|
+
1. Crea un entorno virtual en `venv/`
|
|
53
|
+
2. Instala Hatch y pybind11
|
|
54
|
+
3. Compila nats.c, EIPScanner y el binding Python
|
|
55
|
+
4. Crea el wheel
|
|
56
|
+
5. Instala el wheel en el venv
|
|
57
|
+
|
|
58
|
+
### Uso Posterior
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Activar entorno virtual
|
|
62
|
+
source venv/bin/activate
|
|
63
|
+
|
|
64
|
+
# Ejecutar ejemplo básico
|
|
65
|
+
python examples/basic_example.py
|
|
66
|
+
|
|
67
|
+
# O test completo
|
|
68
|
+
python examples/test_bridge.py
|
|
69
|
+
|
|
70
|
+
# O usar el script helper
|
|
71
|
+
./run.sh # Ejecuta basic_example.py por defecto
|
|
72
|
+
./run.sh python examples/test_bridge.py
|
|
73
|
+
|
|
74
|
+
# Desactivar cuando termines
|
|
75
|
+
deactivate
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 💻 Uso
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import eip2nats
|
|
82
|
+
import time
|
|
83
|
+
|
|
84
|
+
# Crear el bridge
|
|
85
|
+
bridge = eip2nats.EIPtoNATSBridge(
|
|
86
|
+
"192.168.17.200", # IP del PLC
|
|
87
|
+
"nats://192.168.17.138:4222", # Servidor NATS
|
|
88
|
+
"plc.data" # Subject/topic NATS
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Iniciar
|
|
92
|
+
if bridge.start():
|
|
93
|
+
print("✅ Bridge corriendo!")
|
|
94
|
+
|
|
95
|
+
# Monitorear
|
|
96
|
+
while bridge.is_running():
|
|
97
|
+
time.sleep(5)
|
|
98
|
+
print(f"📊 RX={bridge.get_received_count()}, "
|
|
99
|
+
f"TX={bridge.get_published_count()}")
|
|
100
|
+
|
|
101
|
+
# Detener
|
|
102
|
+
bridge.stop()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Ver más ejemplos en [`examples/`](examples/README.md)**
|
|
106
|
+
|
|
107
|
+
## 📋 Requisitos
|
|
108
|
+
|
|
109
|
+
**Sistema:**
|
|
110
|
+
- Linux (ARM64/x86_64)
|
|
111
|
+
- Python 3.7+
|
|
112
|
+
- git, cmake, make, g++, python3-venv (solo para compilar)
|
|
113
|
+
|
|
114
|
+
**Para desarrollo:** Ejecutar `./setup_project.sh` (crea venv automáticamente)
|
|
115
|
+
|
|
116
|
+
## 🛠️ Desarrollo
|
|
117
|
+
|
|
118
|
+
### Modificar Código C++
|
|
119
|
+
|
|
120
|
+
Para desarrollo iterativo sin regenerar el wheel:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# 1. Editar código
|
|
124
|
+
nano src/eip2nats/EIPtoNATSBridge.cpp
|
|
125
|
+
|
|
126
|
+
# 2. Opción A: Test C++ standalone (recomendado para debugging)
|
|
127
|
+
./scripts/build_standalone.sh
|
|
128
|
+
./test_standalone
|
|
129
|
+
|
|
130
|
+
# 3. Opción B: Compilar binding Python (test de integración)
|
|
131
|
+
./scripts/build_python_binding.sh
|
|
132
|
+
python examples/basic_example.py
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Ver guía completa:** [`DEVELOPMENT.md`](DEVELOPMENT.md)
|
|
136
|
+
|
|
137
|
+
**Incluye:**
|
|
138
|
+
- Workflow de desarrollo iterativo
|
|
139
|
+
- Debugging con VSCode (recomendado) y GDB
|
|
140
|
+
- Detección de memory leaks con Valgrind
|
|
141
|
+
- Testing C++ standalone vs Python binding
|
|
142
|
+
- Cuándo usar cada enfoque
|
|
143
|
+
|
|
144
|
+
### Crear Release
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Cuando estés satisfecho con los cambios
|
|
148
|
+
hatch build
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Clonar el repositorio
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git clone https://github.com/yourusername/eip2nats.git
|
|
155
|
+
cd eip2nats
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Instalar Hatch
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install hatch
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Compilar dependencias
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Esto descarga y compila nats.c, EIPScanner y el binding Python
|
|
168
|
+
python scripts/build_dependencies.py
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
O usando Hatch:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
hatch run build-deps
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Crear el wheel
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
hatch build
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Esto genera:
|
|
184
|
+
- `dist/eip2nats-1.0.0-*.whl` - Wheel con todas las dependencias incluidas
|
|
185
|
+
- `dist/eip2nats-1.0.0.tar.gz` - Source distribution
|
|
186
|
+
|
|
187
|
+
### Ejecutar tests
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
hatch run test
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 📦 Estructura del Proyecto
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
eip2nats/
|
|
197
|
+
├── pyproject.toml # Configuración Hatch
|
|
198
|
+
├── README.md
|
|
199
|
+
├── src/
|
|
200
|
+
│ └── eip2nats/
|
|
201
|
+
│ ├── __init__.py # Package Python
|
|
202
|
+
│ ├── bindings.cpp # Bindings pybind11
|
|
203
|
+
│ ├── EIPtoNATSBridge.h # Header C++
|
|
204
|
+
│ ├── EIPtoNATSBridge.cpp # Implementación C++
|
|
205
|
+
│ └── lib/ # Librerías compiladas (auto-generado)
|
|
206
|
+
│ ├── libnats.so
|
|
207
|
+
│ ├── libEIPScanner.so
|
|
208
|
+
│ └── eip2nats.*.so
|
|
209
|
+
├── scripts/
|
|
210
|
+
│ └── build_dependencies.py # Script de compilación
|
|
211
|
+
├── tests/
|
|
212
|
+
│ └── test_basic.py
|
|
213
|
+
└── build/
|
|
214
|
+
└── dependencies/ # Clones de nats.c y EIPScanner
|
|
215
|
+
├── nats.c/
|
|
216
|
+
└── EIPScanner/
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## 🔧 Cómo Funciona
|
|
220
|
+
|
|
221
|
+
1. **`scripts/build_dependencies.py`**:
|
|
222
|
+
- Clona nats.c desde GitHub
|
|
223
|
+
- Compila nats.c → `libnats.so`
|
|
224
|
+
- Clona EIPScanner desde GitHub
|
|
225
|
+
- Compila EIPScanner → `libEIPScanner.so`
|
|
226
|
+
- Compila el binding Python → `eip2nats.*.so`
|
|
227
|
+
- Copia todos los `.so` a `src/eip2nats/lib/`
|
|
228
|
+
|
|
229
|
+
2. **`hatch build`**:
|
|
230
|
+
- Ejecuta el build script automáticamente
|
|
231
|
+
- Empaqueta `src/eip2nats/` completo (código + `.so`)
|
|
232
|
+
- Crea el wheel con RPATH relativo (`$ORIGIN`)
|
|
233
|
+
- El wheel contiene todo lo necesario
|
|
234
|
+
|
|
235
|
+
3. **`pip install`**:
|
|
236
|
+
- Instala el wheel
|
|
237
|
+
- Los `.so` quedan en el site-packages
|
|
238
|
+
- Python carga las librerías automáticamente
|
|
239
|
+
- ¡Funciona sin dependencias del sistema!
|
|
240
|
+
|
|
241
|
+
## 🎯 Ventajas de Este Enfoque
|
|
242
|
+
|
|
243
|
+
### ✅ Comparado con librerías del sistema:
|
|
244
|
+
- No requiere `sudo apt-get install`
|
|
245
|
+
- No hay conflictos de versiones
|
|
246
|
+
- Portabilidad entre sistemas
|
|
247
|
+
|
|
248
|
+
### ✅ Comparado con wheels normales:
|
|
249
|
+
- Incluye todas las dependencias C/C++
|
|
250
|
+
- Un solo archivo para instalar
|
|
251
|
+
- Funciona en sistemas sin compiladores
|
|
252
|
+
|
|
253
|
+
### ✅ Comparado con Docker:
|
|
254
|
+
- Más ligero (MBs vs GBs)
|
|
255
|
+
- Integración directa con Python
|
|
256
|
+
- No requiere privilegios de Docker
|
|
257
|
+
|
|
258
|
+
## 📊 API Reference
|
|
259
|
+
|
|
260
|
+
### Clase: `EIPtoNATSBridge`
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
bridge = eip2nats.EIPtoNATSBridge(
|
|
264
|
+
plc_address: str,
|
|
265
|
+
nats_url: str,
|
|
266
|
+
nats_subject: str,
|
|
267
|
+
use_binary_format: bool = True
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Métodos:**
|
|
272
|
+
- `start() -> bool`: Inicia el bridge
|
|
273
|
+
- `stop() -> None`: Detiene el bridge
|
|
274
|
+
- `is_running() -> bool`: Estado del bridge
|
|
275
|
+
- `get_received_count() -> int`: Mensajes del PLC
|
|
276
|
+
- `get_published_count() -> int`: Mensajes a NATS
|
|
277
|
+
|
|
278
|
+
## 🐛 Troubleshooting
|
|
279
|
+
|
|
280
|
+
### Error: "cannot open shared object file"
|
|
281
|
+
|
|
282
|
+
Aunque el wheel incluye las librerías, verifica RPATH:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
ldd $(python -c "import eip2nats; print(eip2nats.__file__.replace('__init__.py', 'lib/eip2nats.*.so'))")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Todas las dependencias deberían resolverse localmente.
|
|
289
|
+
|
|
290
|
+
### Recompilar en otro sistema
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
git clone <repo>
|
|
294
|
+
cd eip2nats
|
|
295
|
+
python scripts/build_dependencies.py
|
|
296
|
+
hatch build
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Limpiar builds
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
rm -rf build/ dist/ src/eip2nats/lib/
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## 📝 Changelog
|
|
306
|
+
|
|
307
|
+
### v1.0.0 (2024-02-10)
|
|
308
|
+
- Initial release
|
|
309
|
+
- Self-contained wheel con nats.c y EIPScanner
|
|
310
|
+
- Soporte para formato binario y JSON
|
|
311
|
+
- Thread-safe operations
|
|
312
|
+
|
|
313
|
+
## 🤝 Contribuir
|
|
314
|
+
|
|
315
|
+
1. Fork el proyecto
|
|
316
|
+
2. Crea una rama (`git checkout -b feature/amazing`)
|
|
317
|
+
3. Commit cambios (`git commit -m 'Add amazing feature'`)
|
|
318
|
+
4. Push (`git push origin feature/amazing`)
|
|
319
|
+
5. Abre un Pull Request
|
|
320
|
+
|
|
321
|
+
## 📄 Licencia
|
|
322
|
+
|
|
323
|
+
MIT License - ver LICENSE file
|
|
324
|
+
|
|
325
|
+
## 🙏 Créditos
|
|
326
|
+
|
|
327
|
+
- [nats.c](https://github.com/nats-io/nats.c) - Cliente NATS para C
|
|
328
|
+
- [EIPScanner](https://github.com/nimbuscontrols/EIPScanner) - Librería EtherNet/IP
|
|
329
|
+
- [pybind11](https://github.com/pybind/pybind11) - Python bindings
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
**Hecho con ❤️ para facilitar la integración industrial**
|
eip2nats-1.0.0/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# eip2nats - EtherNet/IP to NATS Bridge
|
|
2
|
+
|
|
3
|
+
Puente completo entre dispositivos EtherNet/IP (PLCs) y servidores NATS, **con todas las dependencias incluidas** en el wheel.
|
|
4
|
+
|
|
5
|
+
## ✨ Características
|
|
6
|
+
|
|
7
|
+
- ✅ **Self-contained**: Incluye libnats y libEIPScanner compiladas
|
|
8
|
+
- ✅ **Zero dependencies**: No requiere instalación de librerías del sistema
|
|
9
|
+
- ✅ **Entorno virtual**: Compatible con Raspberry Pi OS (sin pip install global)
|
|
10
|
+
- ✅ **Simple instalación**: Setup automático con un comando
|
|
11
|
+
- ✅ **Alto rendimiento**: Bindings nativos C++ con pybind11
|
|
12
|
+
- ✅ **Thread-safe**: Manejo seguro de múltiples conexiones
|
|
13
|
+
|
|
14
|
+
## 🚀 Instalación Rápida
|
|
15
|
+
|
|
16
|
+
### Setup Completo Automático
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
./setup_project.sh
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Esto hace TODO automáticamente:
|
|
23
|
+
1. Crea un entorno virtual en `venv/`
|
|
24
|
+
2. Instala Hatch y pybind11
|
|
25
|
+
3. Compila nats.c, EIPScanner y el binding Python
|
|
26
|
+
4. Crea el wheel
|
|
27
|
+
5. Instala el wheel en el venv
|
|
28
|
+
|
|
29
|
+
### Uso Posterior
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Activar entorno virtual
|
|
33
|
+
source venv/bin/activate
|
|
34
|
+
|
|
35
|
+
# Ejecutar ejemplo básico
|
|
36
|
+
python examples/basic_example.py
|
|
37
|
+
|
|
38
|
+
# O test completo
|
|
39
|
+
python examples/test_bridge.py
|
|
40
|
+
|
|
41
|
+
# O usar el script helper
|
|
42
|
+
./run.sh # Ejecuta basic_example.py por defecto
|
|
43
|
+
./run.sh python examples/test_bridge.py
|
|
44
|
+
|
|
45
|
+
# Desactivar cuando termines
|
|
46
|
+
deactivate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 💻 Uso
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import eip2nats
|
|
53
|
+
import time
|
|
54
|
+
|
|
55
|
+
# Crear el bridge
|
|
56
|
+
bridge = eip2nats.EIPtoNATSBridge(
|
|
57
|
+
"192.168.17.200", # IP del PLC
|
|
58
|
+
"nats://192.168.17.138:4222", # Servidor NATS
|
|
59
|
+
"plc.data" # Subject/topic NATS
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Iniciar
|
|
63
|
+
if bridge.start():
|
|
64
|
+
print("✅ Bridge corriendo!")
|
|
65
|
+
|
|
66
|
+
# Monitorear
|
|
67
|
+
while bridge.is_running():
|
|
68
|
+
time.sleep(5)
|
|
69
|
+
print(f"📊 RX={bridge.get_received_count()}, "
|
|
70
|
+
f"TX={bridge.get_published_count()}")
|
|
71
|
+
|
|
72
|
+
# Detener
|
|
73
|
+
bridge.stop()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Ver más ejemplos en [`examples/`](examples/README.md)**
|
|
77
|
+
|
|
78
|
+
## 📋 Requisitos
|
|
79
|
+
|
|
80
|
+
**Sistema:**
|
|
81
|
+
- Linux (ARM64/x86_64)
|
|
82
|
+
- Python 3.7+
|
|
83
|
+
- git, cmake, make, g++, python3-venv (solo para compilar)
|
|
84
|
+
|
|
85
|
+
**Para desarrollo:** Ejecutar `./setup_project.sh` (crea venv automáticamente)
|
|
86
|
+
|
|
87
|
+
## 🛠️ Desarrollo
|
|
88
|
+
|
|
89
|
+
### Modificar Código C++
|
|
90
|
+
|
|
91
|
+
Para desarrollo iterativo sin regenerar el wheel:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 1. Editar código
|
|
95
|
+
nano src/eip2nats/EIPtoNATSBridge.cpp
|
|
96
|
+
|
|
97
|
+
# 2. Opción A: Test C++ standalone (recomendado para debugging)
|
|
98
|
+
./scripts/build_standalone.sh
|
|
99
|
+
./test_standalone
|
|
100
|
+
|
|
101
|
+
# 3. Opción B: Compilar binding Python (test de integración)
|
|
102
|
+
./scripts/build_python_binding.sh
|
|
103
|
+
python examples/basic_example.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Ver guía completa:** [`DEVELOPMENT.md`](DEVELOPMENT.md)
|
|
107
|
+
|
|
108
|
+
**Incluye:**
|
|
109
|
+
- Workflow de desarrollo iterativo
|
|
110
|
+
- Debugging con VSCode (recomendado) y GDB
|
|
111
|
+
- Detección de memory leaks con Valgrind
|
|
112
|
+
- Testing C++ standalone vs Python binding
|
|
113
|
+
- Cuándo usar cada enfoque
|
|
114
|
+
|
|
115
|
+
### Crear Release
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Cuando estés satisfecho con los cambios
|
|
119
|
+
hatch build
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Clonar el repositorio
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://github.com/yourusername/eip2nats.git
|
|
126
|
+
cd eip2nats
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Instalar Hatch
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install hatch
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Compilar dependencias
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Esto descarga y compila nats.c, EIPScanner y el binding Python
|
|
139
|
+
python scripts/build_dependencies.py
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
O usando Hatch:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
hatch run build-deps
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Crear el wheel
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
hatch build
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Esto genera:
|
|
155
|
+
- `dist/eip2nats-1.0.0-*.whl` - Wheel con todas las dependencias incluidas
|
|
156
|
+
- `dist/eip2nats-1.0.0.tar.gz` - Source distribution
|
|
157
|
+
|
|
158
|
+
### Ejecutar tests
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
hatch run test
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 📦 Estructura del Proyecto
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
eip2nats/
|
|
168
|
+
├── pyproject.toml # Configuración Hatch
|
|
169
|
+
├── README.md
|
|
170
|
+
├── src/
|
|
171
|
+
│ └── eip2nats/
|
|
172
|
+
│ ├── __init__.py # Package Python
|
|
173
|
+
│ ├── bindings.cpp # Bindings pybind11
|
|
174
|
+
│ ├── EIPtoNATSBridge.h # Header C++
|
|
175
|
+
│ ├── EIPtoNATSBridge.cpp # Implementación C++
|
|
176
|
+
│ └── lib/ # Librerías compiladas (auto-generado)
|
|
177
|
+
│ ├── libnats.so
|
|
178
|
+
│ ├── libEIPScanner.so
|
|
179
|
+
│ └── eip2nats.*.so
|
|
180
|
+
├── scripts/
|
|
181
|
+
│ └── build_dependencies.py # Script de compilación
|
|
182
|
+
├── tests/
|
|
183
|
+
│ └── test_basic.py
|
|
184
|
+
└── build/
|
|
185
|
+
└── dependencies/ # Clones de nats.c y EIPScanner
|
|
186
|
+
├── nats.c/
|
|
187
|
+
└── EIPScanner/
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 🔧 Cómo Funciona
|
|
191
|
+
|
|
192
|
+
1. **`scripts/build_dependencies.py`**:
|
|
193
|
+
- Clona nats.c desde GitHub
|
|
194
|
+
- Compila nats.c → `libnats.so`
|
|
195
|
+
- Clona EIPScanner desde GitHub
|
|
196
|
+
- Compila EIPScanner → `libEIPScanner.so`
|
|
197
|
+
- Compila el binding Python → `eip2nats.*.so`
|
|
198
|
+
- Copia todos los `.so` a `src/eip2nats/lib/`
|
|
199
|
+
|
|
200
|
+
2. **`hatch build`**:
|
|
201
|
+
- Ejecuta el build script automáticamente
|
|
202
|
+
- Empaqueta `src/eip2nats/` completo (código + `.so`)
|
|
203
|
+
- Crea el wheel con RPATH relativo (`$ORIGIN`)
|
|
204
|
+
- El wheel contiene todo lo necesario
|
|
205
|
+
|
|
206
|
+
3. **`pip install`**:
|
|
207
|
+
- Instala el wheel
|
|
208
|
+
- Los `.so` quedan en el site-packages
|
|
209
|
+
- Python carga las librerías automáticamente
|
|
210
|
+
- ¡Funciona sin dependencias del sistema!
|
|
211
|
+
|
|
212
|
+
## 🎯 Ventajas de Este Enfoque
|
|
213
|
+
|
|
214
|
+
### ✅ Comparado con librerías del sistema:
|
|
215
|
+
- No requiere `sudo apt-get install`
|
|
216
|
+
- No hay conflictos de versiones
|
|
217
|
+
- Portabilidad entre sistemas
|
|
218
|
+
|
|
219
|
+
### ✅ Comparado con wheels normales:
|
|
220
|
+
- Incluye todas las dependencias C/C++
|
|
221
|
+
- Un solo archivo para instalar
|
|
222
|
+
- Funciona en sistemas sin compiladores
|
|
223
|
+
|
|
224
|
+
### ✅ Comparado con Docker:
|
|
225
|
+
- Más ligero (MBs vs GBs)
|
|
226
|
+
- Integración directa con Python
|
|
227
|
+
- No requiere privilegios de Docker
|
|
228
|
+
|
|
229
|
+
## 📊 API Reference
|
|
230
|
+
|
|
231
|
+
### Clase: `EIPtoNATSBridge`
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
bridge = eip2nats.EIPtoNATSBridge(
|
|
235
|
+
plc_address: str,
|
|
236
|
+
nats_url: str,
|
|
237
|
+
nats_subject: str,
|
|
238
|
+
use_binary_format: bool = True
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Métodos:**
|
|
243
|
+
- `start() -> bool`: Inicia el bridge
|
|
244
|
+
- `stop() -> None`: Detiene el bridge
|
|
245
|
+
- `is_running() -> bool`: Estado del bridge
|
|
246
|
+
- `get_received_count() -> int`: Mensajes del PLC
|
|
247
|
+
- `get_published_count() -> int`: Mensajes a NATS
|
|
248
|
+
|
|
249
|
+
## 🐛 Troubleshooting
|
|
250
|
+
|
|
251
|
+
### Error: "cannot open shared object file"
|
|
252
|
+
|
|
253
|
+
Aunque el wheel incluye las librerías, verifica RPATH:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
ldd $(python -c "import eip2nats; print(eip2nats.__file__.replace('__init__.py', 'lib/eip2nats.*.so'))")
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Todas las dependencias deberían resolverse localmente.
|
|
260
|
+
|
|
261
|
+
### Recompilar en otro sistema
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
git clone <repo>
|
|
265
|
+
cd eip2nats
|
|
266
|
+
python scripts/build_dependencies.py
|
|
267
|
+
hatch build
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Limpiar builds
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
rm -rf build/ dist/ src/eip2nats/lib/
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## 📝 Changelog
|
|
277
|
+
|
|
278
|
+
### v1.0.0 (2024-02-10)
|
|
279
|
+
- Initial release
|
|
280
|
+
- Self-contained wheel con nats.c y EIPScanner
|
|
281
|
+
- Soporte para formato binario y JSON
|
|
282
|
+
- Thread-safe operations
|
|
283
|
+
|
|
284
|
+
## 🤝 Contribuir
|
|
285
|
+
|
|
286
|
+
1. Fork el proyecto
|
|
287
|
+
2. Crea una rama (`git checkout -b feature/amazing`)
|
|
288
|
+
3. Commit cambios (`git commit -m 'Add amazing feature'`)
|
|
289
|
+
4. Push (`git push origin feature/amazing`)
|
|
290
|
+
5. Abre un Pull Request
|
|
291
|
+
|
|
292
|
+
## 📄 Licencia
|
|
293
|
+
|
|
294
|
+
MIT License - ver LICENSE file
|
|
295
|
+
|
|
296
|
+
## 🙏 Créditos
|
|
297
|
+
|
|
298
|
+
- [nats.c](https://github.com/nats-io/nats.c) - Cliente NATS para C
|
|
299
|
+
- [EIPScanner](https://github.com/nimbuscontrols/EIPScanner) - Librería EtherNet/IP
|
|
300
|
+
- [pybind11](https://github.com/pybind/pybind11) - Python bindings
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
**Hecho con ❤️ para facilitar la integración industrial**
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#include "EIPtoNATSBridge.h"
|
|
2
|
+
#include "utils/Logger.h"
|
|
3
|
+
#include "utils/Buffer.h"
|
|
4
|
+
#include <sstream>
|
|
5
|
+
#include <iomanip>
|
|
6
|
+
#include <chrono>
|
|
7
|
+
|
|
8
|
+
using namespace bridge;
|
|
9
|
+
using namespace eipScanner;
|
|
10
|
+
using namespace eipScanner::cip;
|
|
11
|
+
using namespace eipScanner::cip::connectionManager;
|
|
12
|
+
using namespace eipScanner::utils;
|
|
13
|
+
|
|
14
|
+
EIPtoNATSBridge::EIPtoNATSBridge(const std::string& plcAddress,
|
|
15
|
+
const std::string& natsUrl,
|
|
16
|
+
const std::string& natsSubject,
|
|
17
|
+
bool useBinaryFormat)
|
|
18
|
+
: plcAddress_(plcAddress)
|
|
19
|
+
, natsUrl_(natsUrl)
|
|
20
|
+
, natsSubject_(natsSubject)
|
|
21
|
+
, useBinaryFormat_(useBinaryFormat)
|
|
22
|
+
, natsConn_(nullptr)
|
|
23
|
+
, natsOpts_(nullptr)
|
|
24
|
+
, connectionManager_(nullptr)
|
|
25
|
+
, running_(false)
|
|
26
|
+
, shouldStop_(false)
|
|
27
|
+
, publishedCount_(0)
|
|
28
|
+
, receivedCount_(0)
|
|
29
|
+
{
|
|
30
|
+
Logger(LogLevel::INFO) << "EIPtoNATSBridge creado - PLC: " << plcAddress
|
|
31
|
+
<< " NATS: " << natsUrl
|
|
32
|
+
<< " Subject: " << natsSubject
|
|
33
|
+
<< " Formato: " << (useBinaryFormat ? "Binario" : "JSON");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
EIPtoNATSBridge::~EIPtoNATSBridge() {
|
|
37
|
+
if (running_) {
|
|
38
|
+
Logger(LogLevel::WARNING) << "Bridge destruido mientras estaba corriendo - deteniendo...";
|
|
39
|
+
stop();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
bool EIPtoNATSBridge::start() {
|
|
44
|
+
if (running_) {
|
|
45
|
+
Logger(LogLevel::WARNING) << "Bridge ya está corriendo";
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Logger(LogLevel::INFO) << "Iniciando EIPtoNATS Bridge...";
|
|
50
|
+
|
|
51
|
+
// Inicializar NATS primero
|
|
52
|
+
if (!initNATS()) {
|
|
53
|
+
Logger(LogLevel::ERROR) << "Fallo al inicializar NATS";
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Inicializar EIP
|
|
58
|
+
if (!initEIP()) {
|
|
59
|
+
Logger(LogLevel::ERROR) << "Fallo al inicializar EIP";
|
|
60
|
+
closeNATS();
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Arrancar el thread worker
|
|
65
|
+
shouldStop_ = false;
|
|
66
|
+
running_ = true;
|
|
67
|
+
workerThread_ = std::thread(&EIPtoNATSBridge::workerLoop, this);
|
|
68
|
+
|
|
69
|
+
Logger(LogLevel::INFO) << "✅ Bridge iniciado exitosamente";
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void EIPtoNATSBridge::stop() {
|
|
74
|
+
if (!running_) {
|
|
75
|
+
Logger(LogLevel::WARNING) << "Bridge ya está detenido";
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Logger(LogLevel::INFO) << "Deteniendo EIPtoNATS Bridge...";
|
|
80
|
+
|
|
81
|
+
// Señalizar al thread que debe detenerse
|
|
82
|
+
shouldStop_ = true;
|
|
83
|
+
|
|
84
|
+
// Esperar a que el thread termine
|
|
85
|
+
if (workerThread_.joinable()) {
|
|
86
|
+
workerThread_.join();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Cerrar conexiones
|
|
90
|
+
closeEIP();
|
|
91
|
+
closeNATS();
|
|
92
|
+
|
|
93
|
+
running_ = false;
|
|
94
|
+
|
|
95
|
+
Logger(LogLevel::INFO) << "✅ Bridge detenido - Mensajes recibidos: "
|
|
96
|
+
<< receivedCount_ << " - Mensajes publicados: "
|
|
97
|
+
<< publishedCount_;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
bool EIPtoNATSBridge::isRunning() const {
|
|
101
|
+
return running_;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
uint64_t EIPtoNATSBridge::getPublishedCount() const {
|
|
105
|
+
return publishedCount_;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
uint64_t EIPtoNATSBridge::getReceivedCount() const {
|
|
109
|
+
return receivedCount_;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
bool EIPtoNATSBridge::initNATS() {
|
|
113
|
+
Logger(LogLevel::INFO) << "Conectando a NATS: " << natsUrl_;
|
|
114
|
+
|
|
115
|
+
natsStatus s;
|
|
116
|
+
|
|
117
|
+
// Crear opciones
|
|
118
|
+
s = natsOptions_Create(&natsOpts_);
|
|
119
|
+
if (s != NATS_OK) {
|
|
120
|
+
Logger(LogLevel::ERROR) << "Error creando opciones NATS: " << natsStatus_GetText(s);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Configurar URL
|
|
125
|
+
s = natsOptions_SetURL(natsOpts_, natsUrl_.c_str());
|
|
126
|
+
if (s != NATS_OK) {
|
|
127
|
+
Logger(LogLevel::ERROR) << "Error configurando URL NATS: " << natsStatus_GetText(s);
|
|
128
|
+
natsOptions_Destroy(natsOpts_);
|
|
129
|
+
natsOpts_ = nullptr;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Configurar timeout
|
|
134
|
+
s = natsOptions_SetTimeout(natsOpts_, 5000);
|
|
135
|
+
if (s != NATS_OK) {
|
|
136
|
+
Logger(LogLevel::ERROR) << "Error configurando timeout NATS: " << natsStatus_GetText(s);
|
|
137
|
+
natsOptions_Destroy(natsOpts_);
|
|
138
|
+
natsOpts_ = nullptr;
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Conectar
|
|
143
|
+
s = natsConnection_Connect(&natsConn_, natsOpts_);
|
|
144
|
+
if (s != NATS_OK) {
|
|
145
|
+
Logger(LogLevel::ERROR) << "Error conectando a NATS: " << natsStatus_GetText(s);
|
|
146
|
+
natsOptions_Destroy(natsOpts_);
|
|
147
|
+
natsOpts_ = nullptr;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Logger(LogLevel::INFO) << "✅ Conectado a NATS exitosamente";
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
bool EIPtoNATSBridge::initEIP() {
|
|
156
|
+
Logger(LogLevel::INFO) << "Conectando a PLC EIP: " << plcAddress_;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Crear SessionInfo
|
|
160
|
+
sessionInfo_ = std::make_shared<SessionInfo>(plcAddress_, 0xAF12);
|
|
161
|
+
|
|
162
|
+
// Crear ConnectionManager
|
|
163
|
+
connectionManager_ = std::make_unique<ConnectionManager>();
|
|
164
|
+
|
|
165
|
+
// Configurar parámetros de conexión (basado en tu código original)
|
|
166
|
+
ConnectionParameters parameters;
|
|
167
|
+
parameters.connectionPath = {0x20, 0x04, 0x24, 4, 0x2C, 2, 0x2C, 1};
|
|
168
|
+
parameters.o2tRealTimeFormat = true;
|
|
169
|
+
parameters.originatorVendorId = 342;
|
|
170
|
+
parameters.originatorSerialNumber = 0x12345;
|
|
171
|
+
|
|
172
|
+
parameters.t2oNetworkConnectionParams |= NetworkConnectionParams::P2P;
|
|
173
|
+
parameters.t2oNetworkConnectionParams |= NetworkConnectionParams::SCHEDULED_PRIORITY;
|
|
174
|
+
parameters.t2oNetworkConnectionParams |= 100;
|
|
175
|
+
|
|
176
|
+
parameters.o2tNetworkConnectionParams |= NetworkConnectionParams::P2P;
|
|
177
|
+
parameters.o2tNetworkConnectionParams |= NetworkConnectionParams::SCHEDULED_PRIORITY;
|
|
178
|
+
parameters.o2tNetworkConnectionParams |= 0;
|
|
179
|
+
|
|
180
|
+
parameters.o2tRPI = 2000;
|
|
181
|
+
parameters.t2oRPI = 2000;
|
|
182
|
+
parameters.transportTypeTrigger |= NetworkConnectionParams::CLASS1 | NetworkConnectionParams::TRIG_CYCLIC;
|
|
183
|
+
|
|
184
|
+
// Abrir conexión
|
|
185
|
+
ioConnection_ = connectionManager_->forwardOpen(sessionInfo_, parameters);
|
|
186
|
+
|
|
187
|
+
if (auto ptr = ioConnection_.lock()) {
|
|
188
|
+
// Configurar listener para datos recibidos
|
|
189
|
+
ptr->setReceiveDataListener([this](auto realTimeHeader, auto sequence, auto data) {
|
|
190
|
+
this->onEIPDataReceived(realTimeHeader, sequence, data);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Configurar listener para cierre de conexión
|
|
194
|
+
ptr->setCloseListener([this]() {
|
|
195
|
+
Logger(LogLevel::WARNING) << "Conexión EIP cerrada por el PLC";
|
|
196
|
+
shouldStop_ = true;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
Logger(LogLevel::INFO) << "✅ Conexión EIP abierta exitosamente";
|
|
200
|
+
return true;
|
|
201
|
+
} else {
|
|
202
|
+
Logger(LogLevel::ERROR) << "Error: No se pudo obtener el puntero de IOConnection";
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
} catch (const std::exception& e) {
|
|
207
|
+
Logger(LogLevel::ERROR) << "Excepción al inicializar EIP: " << e.what();
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void EIPtoNATSBridge::closeNATS() {
|
|
213
|
+
std::lock_guard<std::mutex> lock(natsMutex_);
|
|
214
|
+
|
|
215
|
+
if (natsConn_ != nullptr) {
|
|
216
|
+
Logger(LogLevel::INFO) << "Cerrando conexión NATS...";
|
|
217
|
+
natsConnection_Destroy(natsConn_);
|
|
218
|
+
natsConn_ = nullptr;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (natsOpts_ != nullptr) {
|
|
222
|
+
natsOptions_Destroy(natsOpts_);
|
|
223
|
+
natsOpts_ = nullptr;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
void EIPtoNATSBridge::closeEIP() {
|
|
228
|
+
Logger(LogLevel::INFO) << "Cerrando conexión EIP...";
|
|
229
|
+
|
|
230
|
+
if (connectionManager_ && sessionInfo_) {
|
|
231
|
+
try {
|
|
232
|
+
connectionManager_->forwardClose(sessionInfo_, ioConnection_);
|
|
233
|
+
Logger(LogLevel::INFO) << "Forward Close enviado";
|
|
234
|
+
} catch (const std::exception& e) {
|
|
235
|
+
Logger(LogLevel::ERROR) << "Error en forward close: " << e.what();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
ioConnection_.reset();
|
|
240
|
+
connectionManager_.reset();
|
|
241
|
+
sessionInfo_.reset();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
void EIPtoNATSBridge::workerLoop() {
|
|
245
|
+
Logger(LogLevel::INFO) << "Thread worker iniciado";
|
|
246
|
+
|
|
247
|
+
while (!shouldStop_ && connectionManager_->hasOpenConnections()) {
|
|
248
|
+
// Manejar las conexiones EIP (procesar datos entrantes)
|
|
249
|
+
connectionManager_->handleConnections(std::chrono::milliseconds(1));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Logger(LogLevel::INFO) << "Thread worker finalizando";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
bool EIPtoNATSBridge::publishToNATS(const std::vector<uint8_t>& data) {
|
|
256
|
+
std::lock_guard<std::mutex> lock(natsMutex_);
|
|
257
|
+
|
|
258
|
+
if (natsConn_ == nullptr) {
|
|
259
|
+
Logger(LogLevel::ERROR) << "No hay conexión NATS activa";
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
natsStatus s;
|
|
264
|
+
|
|
265
|
+
if (useBinaryFormat_) {
|
|
266
|
+
// Publicar datos binarios directamente (más eficiente)
|
|
267
|
+
s = natsConnection_Publish(natsConn_,
|
|
268
|
+
natsSubject_.c_str(),
|
|
269
|
+
data.data(),
|
|
270
|
+
data.size());
|
|
271
|
+
} else {
|
|
272
|
+
// Publicar como JSON (para debugging o interoperabilidad)
|
|
273
|
+
std::ostringstream jsonStream;
|
|
274
|
+
jsonStream << "{\"timestamp\":" << time(nullptr)
|
|
275
|
+
<< ",\"sequence\":" << receivedCount_
|
|
276
|
+
<< ",\"size\":" << data.size()
|
|
277
|
+
<< ",\"data\":\"";
|
|
278
|
+
|
|
279
|
+
// Convertir bytes a hexadecimal
|
|
280
|
+
for (const auto& byte : data) {
|
|
281
|
+
jsonStream << std::hex << std::setfill('0') << std::setw(2) << (int)byte;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
jsonStream << "\"}";
|
|
285
|
+
std::string jsonStr = jsonStream.str();
|
|
286
|
+
|
|
287
|
+
s = natsConnection_PublishString(natsConn_, natsSubject_.c_str(), jsonStr.c_str());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (s == NATS_OK) {
|
|
291
|
+
publishedCount_++;
|
|
292
|
+
Logger(LogLevel::DEBUG) << "Publicado a NATS [" << publishedCount_ << "]: "
|
|
293
|
+
<< data.size() << " bytes ("
|
|
294
|
+
<< (useBinaryFormat_ ? "binario" : "JSON") << ")";
|
|
295
|
+
return true;
|
|
296
|
+
} else {
|
|
297
|
+
Logger(LogLevel::ERROR) << "Error publicando a NATS: " << natsStatus_GetText(s);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
void EIPtoNATSBridge::onEIPDataReceived(uint32_t realTimeHeader,
|
|
303
|
+
uint16_t sequence,
|
|
304
|
+
const std::vector<uint8_t>& data) {
|
|
305
|
+
receivedCount_++;
|
|
306
|
+
|
|
307
|
+
// Log detallado de lo recibido
|
|
308
|
+
std::ostringstream ss;
|
|
309
|
+
ss << "EIP RX [" << receivedCount_ << "] seq=" << sequence
|
|
310
|
+
<< " size=" << data.size() << " data=";
|
|
311
|
+
for (const auto& byte : data) {
|
|
312
|
+
ss << std::hex << std::setfill('0') << std::setw(2) << (int)byte << " ";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
Logger(LogLevel::DEBUG) << ss.str();
|
|
316
|
+
|
|
317
|
+
// Publicar a NATS
|
|
318
|
+
if (!publishToNATS(data)) {
|
|
319
|
+
Logger(LogLevel::WARNING) << "Fallo al publicar datos a NATS";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#ifndef EIP_TO_NATS_BRIDGE_H
|
|
2
|
+
#define EIP_TO_NATS_BRIDGE_H
|
|
3
|
+
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <thread>
|
|
6
|
+
#include <atomic>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <vector>
|
|
10
|
+
#include <nats/nats.h>
|
|
11
|
+
#include <cip/connectionManager/NetworkConnectionParams.h>
|
|
12
|
+
#include "SessionInfo.h"
|
|
13
|
+
#include "ConnectionManager.h"
|
|
14
|
+
|
|
15
|
+
namespace bridge {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @brief Puente entre EtherNet/IP (usando EIPScanner) y NATS
|
|
19
|
+
*
|
|
20
|
+
* Esta clase gestiona una conexión EIP implícita y publica los datos
|
|
21
|
+
* recibidos a un servidor NATS en un thread separado.
|
|
22
|
+
*/
|
|
23
|
+
class EIPtoNATSBridge {
|
|
24
|
+
public:
|
|
25
|
+
/**
|
|
26
|
+
* @brief Constructor
|
|
27
|
+
* @param plcAddress Dirección IP del PLC
|
|
28
|
+
* @param natsUrl URL del servidor NATS (ej: "nats://192.168.17.138:4222")
|
|
29
|
+
* @param natsSubject Subject/topic donde publicar los datos
|
|
30
|
+
* @param useBinaryFormat Si es true usa binario, si es false usa JSON (default: true)
|
|
31
|
+
*/
|
|
32
|
+
EIPtoNATSBridge(const std::string& plcAddress,
|
|
33
|
+
const std::string& natsUrl,
|
|
34
|
+
const std::string& natsSubject,
|
|
35
|
+
bool useBinaryFormat = true);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @brief Destructor - asegura que todo esté limpiamente cerrado
|
|
39
|
+
*/
|
|
40
|
+
~EIPtoNATSBridge();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @brief Inicia el bridge: conecta a NATS, abre conexión EIP y arranca el thread
|
|
44
|
+
* @return true si todo se inició correctamente, false en caso de error
|
|
45
|
+
*/
|
|
46
|
+
bool start();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @brief Detiene el bridge: cierra conexión EIP, desconecta NATS y detiene el thread
|
|
50
|
+
*/
|
|
51
|
+
void stop();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @brief Verifica si el bridge está corriendo
|
|
55
|
+
* @return true si está activo, false si está detenido
|
|
56
|
+
*/
|
|
57
|
+
bool isRunning() const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @brief Obtiene el número de mensajes publicados
|
|
61
|
+
* @return Contador de mensajes enviados a NATS
|
|
62
|
+
*/
|
|
63
|
+
uint64_t getPublishedCount() const;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @brief Obtiene el número de mensajes recibidos del PLC
|
|
67
|
+
* @return Contador de mensajes recibidos por EIP
|
|
68
|
+
*/
|
|
69
|
+
uint64_t getReceivedCount() const;
|
|
70
|
+
|
|
71
|
+
private:
|
|
72
|
+
// Configuración
|
|
73
|
+
std::string plcAddress_;
|
|
74
|
+
std::string natsUrl_;
|
|
75
|
+
std::string natsSubject_;
|
|
76
|
+
bool useBinaryFormat_;
|
|
77
|
+
|
|
78
|
+
// NATS
|
|
79
|
+
natsConnection* natsConn_;
|
|
80
|
+
natsOptions* natsOpts_;
|
|
81
|
+
std::mutex natsMutex_;
|
|
82
|
+
|
|
83
|
+
// EIP Scanner
|
|
84
|
+
std::shared_ptr<eipScanner::SessionInfo> sessionInfo_;
|
|
85
|
+
std::unique_ptr<eipScanner::ConnectionManager> connectionManager_;
|
|
86
|
+
std::weak_ptr<eipScanner::IOConnection> ioConnection_;
|
|
87
|
+
|
|
88
|
+
// Thread control
|
|
89
|
+
std::thread workerThread_;
|
|
90
|
+
std::atomic<bool> running_;
|
|
91
|
+
std::atomic<bool> shouldStop_;
|
|
92
|
+
|
|
93
|
+
// Estadísticas
|
|
94
|
+
std::atomic<uint64_t> publishedCount_;
|
|
95
|
+
std::atomic<uint64_t> receivedCount_;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @brief Función principal del thread worker
|
|
99
|
+
*/
|
|
100
|
+
void workerLoop();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @brief Inicializa la conexión NATS
|
|
104
|
+
* @return true si se conectó correctamente
|
|
105
|
+
*/
|
|
106
|
+
bool initNATS();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @brief Inicializa la conexión EIP
|
|
110
|
+
* @return true si se conectó correctamente
|
|
111
|
+
*/
|
|
112
|
+
bool initEIP();
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @brief Cierra la conexión NATS
|
|
116
|
+
*/
|
|
117
|
+
void closeNATS();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @brief Cierra la conexión EIP
|
|
121
|
+
*/
|
|
122
|
+
void closeEIP();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @brief Publica datos a NATS
|
|
126
|
+
* @param data Vector de bytes a publicar
|
|
127
|
+
* @return true si se publicó correctamente
|
|
128
|
+
*/
|
|
129
|
+
bool publishToNATS(const std::vector<uint8_t>& data);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @brief Callback para datos recibidos del PLC
|
|
133
|
+
*/
|
|
134
|
+
void onEIPDataReceived(uint32_t realTimeHeader,
|
|
135
|
+
uint16_t sequence,
|
|
136
|
+
const std::vector<uint8_t>& data);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
} // namespace bridge
|
|
140
|
+
|
|
141
|
+
#endif // EIP_TO_NATS_BRIDGE_H
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eip2nats - EtherNet/IP to NATS Bridge
|
|
3
|
+
|
|
4
|
+
Puente entre dispositivos EtherNet/IP (PLCs) y servidores NATS con
|
|
5
|
+
todas las dependencias incluidas.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Agregar el directorio lib al path de búsqueda de librerías
|
|
13
|
+
_lib_dir = Path(__file__).parent / "lib"
|
|
14
|
+
if _lib_dir.exists():
|
|
15
|
+
# Agregar al LD_LIBRARY_PATH en tiempo de ejecución
|
|
16
|
+
if sys.platform.startswith('linux'):
|
|
17
|
+
os.environ['LD_LIBRARY_PATH'] = f"{_lib_dir}:{os.environ.get('LD_LIBRARY_PATH', '')}"
|
|
18
|
+
|
|
19
|
+
# Importar el módulo C++ - está directamente en este directorio
|
|
20
|
+
try:
|
|
21
|
+
# Buscar el archivo .so en el directorio actual
|
|
22
|
+
import importlib.util
|
|
23
|
+
_module_dir = Path(__file__).parent
|
|
24
|
+
|
|
25
|
+
# Buscar el módulo compilado (eip_nats_bridge*.so)
|
|
26
|
+
for so_file in _module_dir.glob("eip_nats_bridge*.so"):
|
|
27
|
+
if so_file.is_file():
|
|
28
|
+
# El nombre debe coincidir con PYBIND11_MODULE(eip_nats_bridge, m)
|
|
29
|
+
spec = importlib.util.spec_from_file_location("eip_nats_bridge", so_file)
|
|
30
|
+
if spec and spec.loader:
|
|
31
|
+
module = importlib.util.module_from_spec(spec)
|
|
32
|
+
spec.loader.exec_module(module)
|
|
33
|
+
EIPtoNATSBridge = module.EIPtoNATSBridge
|
|
34
|
+
break
|
|
35
|
+
else:
|
|
36
|
+
raise ImportError("No se encontró el módulo eip_nats_bridge compilado (.so)")
|
|
37
|
+
|
|
38
|
+
except ImportError as e:
|
|
39
|
+
raise ImportError(f"Error cargando el módulo eip2nats: {e}")
|
|
40
|
+
|
|
41
|
+
__version__ = "1.0.0"
|
|
42
|
+
__all__ = ["EIPtoNATSBridge"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#include <pybind11/pybind11.h>
|
|
2
|
+
#include <pybind11/stl.h>
|
|
3
|
+
#include "EIPtoNATSBridge.h"
|
|
4
|
+
|
|
5
|
+
namespace py = pybind11;
|
|
6
|
+
|
|
7
|
+
PYBIND11_MODULE(eip_nats_bridge, m) {
|
|
8
|
+
m.doc() = "EIP to NATS Bridge - Puente entre EtherNet/IP y NATS";
|
|
9
|
+
|
|
10
|
+
py::class_<bridge::EIPtoNATSBridge>(m, "EIPtoNATSBridge")
|
|
11
|
+
.def(py::init<const std::string&, const std::string&, const std::string&>(),
|
|
12
|
+
py::arg("plc_address"),
|
|
13
|
+
py::arg("nats_url"),
|
|
14
|
+
py::arg("nats_subject"),
|
|
15
|
+
"Constructor del bridge\n\n"
|
|
16
|
+
"Args:\n"
|
|
17
|
+
" plc_address (str): Dirección IP del PLC (ej: '192.168.17.200')\n"
|
|
18
|
+
" nats_url (str): URL del servidor NATS (ej: 'nats://192.168.17.138:4222')\n"
|
|
19
|
+
" nats_subject (str): Subject/topic NATS (ej: 'plc.data')")
|
|
20
|
+
|
|
21
|
+
.def(py::init<const std::string&, const std::string&, const std::string&, bool>(),
|
|
22
|
+
py::arg("plc_address"),
|
|
23
|
+
py::arg("nats_url"),
|
|
24
|
+
py::arg("nats_subject"),
|
|
25
|
+
py::arg("use_binary_format"),
|
|
26
|
+
"Constructor del bridge con formato personalizado\n\n"
|
|
27
|
+
"Args:\n"
|
|
28
|
+
" plc_address (str): Dirección IP del PLC\n"
|
|
29
|
+
" nats_url (str): URL del servidor NATS\n"
|
|
30
|
+
" nats_subject (str): Subject/topic NATS\n"
|
|
31
|
+
" use_binary_format (bool): True para binario, False para JSON")
|
|
32
|
+
|
|
33
|
+
.def("start", &bridge::EIPtoNATSBridge::start,
|
|
34
|
+
"Inicia el bridge: conecta a NATS, abre conexión EIP y arranca el thread\n\n"
|
|
35
|
+
"Returns:\n"
|
|
36
|
+
" bool: True si se inició correctamente, False en caso de error")
|
|
37
|
+
|
|
38
|
+
.def("stop", &bridge::EIPtoNATSBridge::stop,
|
|
39
|
+
"Detiene el bridge: cierra conexión EIP, desconecta NATS y detiene el thread")
|
|
40
|
+
|
|
41
|
+
.def("is_running", &bridge::EIPtoNATSBridge::isRunning,
|
|
42
|
+
"Verifica si el bridge está corriendo\n\n"
|
|
43
|
+
"Returns:\n"
|
|
44
|
+
" bool: True si está activo, False si está detenido")
|
|
45
|
+
|
|
46
|
+
.def("get_published_count", &bridge::EIPtoNATSBridge::getPublishedCount,
|
|
47
|
+
"Obtiene el número de mensajes publicados a NATS\n\n"
|
|
48
|
+
"Returns:\n"
|
|
49
|
+
" int: Contador de mensajes enviados")
|
|
50
|
+
|
|
51
|
+
.def("get_received_count", &bridge::EIPtoNATSBridge::getReceivedCount,
|
|
52
|
+
"Obtiene el número de mensajes recibidos del PLC\n\n"
|
|
53
|
+
"Returns:\n"
|
|
54
|
+
" int: Contador de mensajes recibidos")
|
|
55
|
+
|
|
56
|
+
.def("__repr__", [](const bridge::EIPtoNATSBridge &bridge) {
|
|
57
|
+
return "<EIPtoNATSBridge running=" +
|
|
58
|
+
std::string(bridge.isRunning() ? "True" : "False") +
|
|
59
|
+
" received=" + std::to_string(bridge.getReceivedCount()) +
|
|
60
|
+
" published=" + std::to_string(bridge.getPublishedCount()) + ">";
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Información del módulo
|
|
64
|
+
m.attr("__version__") = "1.0.0";
|
|
65
|
+
m.attr("__author__") = "Your Name";
|
|
66
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "pybind11>=2.6.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "eip2nats"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "EtherNet/IP to NATS Bridge with bundled dependencies"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Your Name", email = "your.email@example.com"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.7",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: C++",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pybind11>=2.6.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"black>=22.0",
|
|
37
|
+
"ruff>=0.0.243",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/yourusername/eip2nats"
|
|
42
|
+
Documentation = "https://github.com/yourusername/eip2nats/blob/main/README.md"
|
|
43
|
+
Repository = "https://github.com/yourusername/eip2nats"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build]
|
|
46
|
+
packages = ["src/eip2nats"]
|
|
47
|
+
# Excluir archivos fuente C++, solo incluir Python y .so
|
|
48
|
+
exclude = [
|
|
49
|
+
"src/eip2nats/*.cpp",
|
|
50
|
+
"src/eip2nats/*.h",
|
|
51
|
+
"src/eip2nats/*.hpp",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/eip2nats"]
|
|
56
|
+
# Incluir el módulo Python compilado y las librerías auxiliares
|
|
57
|
+
artifacts = [
|
|
58
|
+
"src/eip2nats/eip_nats_bridge*.so",
|
|
59
|
+
"src/eip2nats/lib/*.so*",
|
|
60
|
+
]
|
|
61
|
+
# Excluir archivos fuente del wheel
|
|
62
|
+
exclude = [
|
|
63
|
+
"src/eip2nats/*.cpp",
|
|
64
|
+
"src/eip2nats/*.h",
|
|
65
|
+
"src/eip2nats/*.hpp",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.hatch.build.targets.sdist]
|
|
69
|
+
exclude = [
|
|
70
|
+
"/.git",
|
|
71
|
+
"/build",
|
|
72
|
+
"/dist",
|
|
73
|
+
"/venv",
|
|
74
|
+
"*.pyc",
|
|
75
|
+
"__pycache__",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.hatch.envs.default]
|
|
79
|
+
dependencies = [
|
|
80
|
+
"pytest",
|
|
81
|
+
"pytest-cov",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.hatch.envs.default.scripts]
|
|
85
|
+
build-deps = "python scripts/build_dependencies.py"
|
|
86
|
+
test = "pytest {args:tests}"
|
|
87
|
+
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src/eip2nats {args:tests}"
|
|
88
|
+
|
|
89
|
+
[tool.black]
|
|
90
|
+
line-length = 100
|
|
91
|
+
target-version = ['py37']
|
|
92
|
+
|
|
93
|
+
[tool.ruff]
|
|
94
|
+
line-length = 100
|
|
95
|
+
select = ["E", "F", "W", "I", "N"]
|