Skip to content

Commit dbc3f10

Browse files
committed
feat(vucm): add integration with NI-cDAQ
1 parent bc2b57a commit dbc3f10

File tree

5 files changed

+226
-0
lines changed

5 files changed

+226
-0
lines changed

examples/vucm/ni-daq/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM python:3.11-alpine3.22
2+
3+
WORKDIR /app
4+
5+
RUN apk add build-base
6+
7+
RUN python -m venv .venv
8+
COPY requirements.txt requirements.txt
9+
RUN .venv/bin/pip install -r requirements.txt
10+
11+
COPY script.py script.py
12+
13+
CMD [".venv/bin/python", "script.py"]

examples/vucm/ni-daq/docker_run.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
IFS=$'\n\t'
5+
6+
SCRIPT_DIR="$(realpath "$(dirname "$0")")"
7+
IMAGE_TAG="${IMAGE_TAG:-"enapter-vucm-examples/$(basename "$SCRIPT_DIR"):latest"}"
8+
9+
docker build --tag "$IMAGE_TAG" "$SCRIPT_DIR"
10+
11+
docker run --rm -it \
12+
--name "ni-daq" \
13+
--network host \
14+
-e ENAPTER_LOG_LEVEL="${ENAPTER_LOG_LEVEL:-info}" \
15+
-e ENAPTER_VUCM_BLOB="$ENAPTER_VUCM_BLOB" \
16+
-e LISTEN_TCP_PORT="$LISTEN_TCP_PORT" \
17+
"$IMAGE_TAG"

examples/vucm/ni-daq/manifest.yml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
blueprint_spec: "device/1.0"
2+
3+
display_name: ATS stack
4+
5+
communication_module:
6+
product: ENP-VIRTUAL
7+
8+
properties:
9+
vendor:
10+
display_name: Vendor
11+
type: string
12+
model:
13+
display_name: Model
14+
type: string
15+
16+
alerts:
17+
parse_error:
18+
display_name: Data processing failed
19+
severity: error
20+
telemetry:
21+
status:
22+
display_name: Status
23+
type: string
24+
enum:
25+
- ok
26+
- error
27+
- no_data
28+
T1:
29+
display_name: T1
30+
type: float
31+
T2:
32+
display_name: T2
33+
type: float
34+
T3:
35+
display_name: T2
36+
type: float
37+
Current:
38+
display_name: Current
39+
type: float
40+
PSU:
41+
display_name: Current
42+
type: float
43+
P1:
44+
display_name: P1
45+
type: float
46+
P2:
47+
display_name: P2
48+
type: float
49+
P3:
50+
display_name: P3
51+
type: float
52+
Flow:
53+
display_name: Flow
54+
type: float
55+
Conductivity:
56+
display_name: Conductivity
57+
type: float
58+
MFMH2:
59+
display_name: MFMH2
60+
type: float
61+
Theoretical_h2:
62+
display_name: MFMH2
63+
type: float
64+
MCM02:
65+
display_name: MCM02
66+
type: float
67+
Refilling:
68+
display_name: Refilling
69+
type: float
70+
PC:
71+
display_name: PC
72+
type: float
73+
C1:
74+
display_name: Cell 1
75+
type: float
76+
C2:
77+
display_name: Cell 2
78+
type: float
79+
C3:
80+
display_name: Cell 3
81+
type: float
82+
C4:
83+
display_name: Cell 4
84+
type: float
85+
C5:
86+
display_name: Cell 5
87+
type: float
88+
C6:
89+
display_name: Cell 6
90+
type: float
91+
C7:
92+
display_name: Cell 7
93+
type: float
94+
C8:
95+
display_name: Cell 8
96+
type: float
97+
C9:
98+
display_name: Cell 9
99+
type: float
100+
C10:
101+
display_name: Cell 10
102+
type: float
103+
104+
commands: {}

examples/vucm/ni-daq/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
enapter==0.9.2

examples/vucm/ni-daq/script.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import asyncio
2+
import functools
3+
import json
4+
import os
5+
import socket
6+
from datetime import datetime
7+
8+
import enapter
9+
10+
11+
async def main():
12+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
13+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
14+
sock.bind(("127.0.0.1", int(os.environ["LISTEN_TCP_PORT"])))
15+
sock.listen()
16+
sock.setblocking(False)
17+
device_factory = functools.partial(NIDAQ, socket=sock)
18+
await enapter.vucm.run(device_factory)
19+
20+
21+
class NIDAQ(enapter.vucm.Device):
22+
def __init__(self, socket, **kwargs):
23+
super().__init__(**kwargs)
24+
self.socket = socket
25+
26+
async def task_accept_conns(self):
27+
sock = self.socket
28+
while True:
29+
(conn, addr) = await asyncio.get_event_loop().sock_accept(sock)
30+
asyncio.create_task(self.handle_conn(conn, addr))
31+
32+
async def handle_conn(self, conn, addr):
33+
print(addr, "accept")
34+
data = bytearray()
35+
try:
36+
while True:
37+
try:
38+
async with asyncio.timeout(5):
39+
chunk = await asyncio.get_event_loop().sock_recv(conn, 1024)
40+
except TimeoutError:
41+
print(addr, "timeout")
42+
return
43+
if chunk:
44+
print(addr, "read chunk: ", chunk)
45+
data.extend(chunk)
46+
continue
47+
print(addr, "read data: ", data)
48+
await self._process_and_send_telemetry(data)
49+
return
50+
finally:
51+
print(addr, "close")
52+
conn.close()
53+
54+
async def task_properties_sender(self):
55+
"""Periodically send device properties."""
56+
while True:
57+
await self.send_properties(
58+
{"vendor": "National Instruments", "model": "cDAQ 9178"}
59+
)
60+
await asyncio.sleep(10)
61+
62+
async def _process_and_send_telemetry(self, data):
63+
"""Parse, enrich, and send telemetry data."""
64+
telemetry = {}
65+
status = "no_data"
66+
try:
67+
if data:
68+
status = "ok"
69+
telemetry = json.loads(data.decode())
70+
self._add_timestamp_if_present(telemetry)
71+
telemetry["status"] = status
72+
await self.send_telemetry(telemetry)
73+
self.alerts.clear()
74+
except Exception as e:
75+
self.alerts.add("parse_error")
76+
await self.log.error(f"Failed to process data: {e}")
77+
78+
def _add_timestamp_if_present(self, telemetry):
79+
"""If 'Date' and 'Time' are present, combine and convert to timestamp."""
80+
date_str = telemetry.get("Date")
81+
time_str = telemetry.get("Time")
82+
if date_str and time_str:
83+
dt_str = f"{date_str} {time_str}"
84+
date = datetime.strptime(dt_str, "%d/%m/%Y %H:%M:%S")
85+
telemetry.pop("Date")
86+
telemetry.pop("Time")
87+
telemetry["timestamp"] = int(date.timestamp())
88+
89+
90+
if __name__ == "__main__":
91+
asyncio.run(main())

0 commit comments

Comments
 (0)