Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Below is a list of all tutorials available in this repository:
- 📊 [Visualize](https://app.foxglove.dev/~/view?ds=foxglove-sample-stream&ds.recordingId=vqKKQcot421Kwg84&ds.overrideLayoutId=b7513959-1d46-4a89-bc24-1584d9677ca1&ds.start=2023-09-01T13:19:45.047438263Z&ds.end=2023-09-01T13:20:15.047438263Z)

## Foxglove SDK
### [Using Foxglove to Visualize Ethernet/IP data](foxglove_sdk/ethernet_ip_integration/README.md)
- 📝 Using Foxglove data, it's easier than ever to stream time series data. In this project, we show you how.
- 🔗 [Related Blog Post](https://foxglove.dev/blog/use-foxglove-sdk-for-real-time-industrial-plc-data-visualization-and-playback)
### [Visualizing LeRobot (SO-100) using Foxglove](foxglove_sdk/foxglove_so_100/README.md)
- 📝 Use LeRobot API and Foxglove SDK to visualize SO-100 state in real-time
- 🔗 [Related Blog Post](https://foxglove.dev/blog/visualizing-lerobot-so-100-using-foxglove)
Expand Down
39 changes: 39 additions & 0 deletions foxglove_sdk/ethernet_ip_integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: "Using Foxglove to Visualize Ethernet/IP data"
blog_post_url: "https://foxglove.dev/blog/use-foxglove-sdk-for-real-time-industrial-plc-data-visualization-and-playback"
short_description: "Using Foxglove data, it's easier than ever to stream time series data. In this project, we show you how."
---

![Ethernet/IP data visualization in Foxglove](media/eip_layout.png)

# Use Foxglove for Real-Time Industrial PLC Data Visualization and Playback

In this tutorial, we implement a simple script to stream values of Ethernet/IP tags to Foxglove. The example includes a simulated plant to demonstrate this proof of concept.

## Dependencies
Run `pip install -r requirements.txt` to install dependencies for this tutorial. We depend on `cpppo` for simulating an Ethernet/IP device, and `pylogix` to run the simulated plant and read the tag values for our Foxglove bridge.

## Running the example
Make sure you’ve cloned the repository and installed the dependencies from the previous section.

Navigate to the server directory (`foxglove_sdk/ethernet_ip_integration/server`) and execute these two scripts in separate terminal windows:
```
python3 eip_server.py
python3 run_plant.py
```
After the plant is started, run the foxglove_eip_bridge.py (located in `foxglove_sdk/ethernet_ip_integration/`):
```
python3 foxglove_eip_bridge.py
```
If successful, it should start Foxglove server and show the following output:
```
2025-07-04 18:18:52,547 [INFO] Started server on 127.0.0.1:8765
2025-07-04 18:18:52,548 [INFO] Starting EtherNet/IP to Foxglove bridge
2025-07-04 18:18:52,548 [INFO] Connecting to PLC at 0.0.0.0
2025-07-04 18:18:52,548 [INFO] Streaming 35 tags to Foxglove
```

## Viewing the data
To view the data, start Foxglove and open a new connection to `ws://localhost:8765`. If everything is well, you will see several topics derived from EIP tags. Now, you should be able to load different panels and view the data. You can use a layout in `foxglove_sdk/ethernet_ip_integration/layouts/eip_layout.json`.

Alternatively, you can open our [logged MCAP file](foxglove_sdk/ethernet_ip_integration/mcap/foxglove_eip_bridge_2025-07-04_17-53-27.mcap) and view our recording of a simulated run.
157 changes: 157 additions & 0 deletions foxglove_sdk/ethernet_ip_integration/foxglove_eip_bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@

import datetime
import json
import logging
import time
from typing import Dict

import foxglove
from foxglove import Channel, Schema
from pylogix import PLC


# Define schema for timeseries data
timeseries_schema = {
"type": "object",
"properties": {
"timestamp": {"type": "number"},
"value": {"type": "number"},
"tag_name": {"type": "string"},
},
}

# All tags from the EtherNet/IP server configuration
ALL_TAGS = [
# Process variables
"Temperature",
"Pressure",
"Flow_Rate",
"Level",
"Vibration",

# Control variables
"Motor_Running",
"Pump_Speed",
"Valve_Position",
"Pump_Enable",
"Emergency_Stop",

# Status and alarms
"High_Temp_Alarm",
"Low_Pressure_Alarm",
"System_Status",
"Error_Code",
"Alarm_Count",

# Production data
"Production_Count",
"Runtime_Hours",
"Batch_Number",
"Recipe_Number",
"Quality_Rating",

# Setpoints
"Setpoint_Temp",
"Setpoint_Pressure",
"Setpoint_Flow",

# Tank levels
"Tank_Level_1",
"Tank_Level_2",
"Tank_Level_3",
"Tank_Level_4",

# Sensor arrays
"Sensor_1",
"Sensor_2",
"Sensor_3",
"Sensor_4",

# Motor data
"Motor_Current",
"Motor_Voltage",
"Motor_Power",
"Motor_Temp",
]

def create_channels() -> Dict[str, Channel]:
"""Create Foxglove channels for all tags"""
channels = {}

for tag in ALL_TAGS:
# Create channel with topic name based on tag
topic = f"/eip/{tag.lower().replace(' ', '_')}"

channels[tag] = Channel(
topic=topic,
message_encoding="json",
schema=Schema(
name=f"{tag}_data",
encoding="jsonschema",
data=json.dumps(timeseries_schema).encode("utf-8"),
),
)
return channels


def main() -> None:
foxglove.set_log_level(logging.INFO)

server = foxglove.start_server()
now_str = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
file_name = f"foxglove_eip_bridge_{now_str}.mcap"
writer = foxglove.open_mcap(file_name) # Comment out if not logging to mcap

# Create channels for all tags
channels = create_channels()

# Initialize PLC connection
plc_host = '0.0.0.0' # EtherNet/IP server address

logging.info(f"Starting EtherNet/IP to Foxglove bridge")
logging.info(f"Connecting to PLC at {plc_host}")
logging.info(f"Streaming {len(ALL_TAGS)} tags to Foxglove")

try:
with PLC() as comm:
comm.IPAddress = plc_host
comm.Micro800 = True

while True:
now = time.time()

# Read and stream all tags
for tag_name in ALL_TAGS:
try:
# Read value from PLC
result = comm.Read(tag_name)

if result.Status == "Success" and result.Value is not None:
message = {
"timestamp": now,
"value": result.Value,
"tag_name": tag_name,
}

# Log to channel
if tag_name in channels:
channels[tag_name].log(json.dumps(message).encode("utf-8"))

else:
logging.warning(f"Failed to read {tag_name}: {result.Status}")

except Exception as e:
logging.error(f"Error reading tag {tag_name}: {e}")

time.sleep(0.1)
except KeyboardInterrupt:
logging.info("Shutting down bridge...")
except Exception as e:
logging.error(f"Bridge error: {e}")
finally:
server.stop()
writer.close()
logging.info("Bridge stopped")

if __name__ == "__main__":
main()
Loading