A clean architecture OPC UA client for SI3 monitoring and history with Bokeh visualization.
- Python 3.8 or higher
- pip (Python package manager)
git clone https://github.com/FranVusm/client_py_LIDER.git
cd client_py_LIDERIt's recommended to use a virtual environment to isolate dependencies.
# Create virtual environment
python3 -m venv .venv
# Activate virtual environment
source .venv/bin/activate# Create virtual environment
python -m venv .venv
# Activate virtual environment
.venv\Scripts\activateOnce the virtual environment is activated, install the package and its dependencies:
pip install -e .This will install:
asyncua- OPC UA client librarybokeh- Interactive visualization librarytornado- Web server for Bokehpython-dotenv- Environment variable management
Run the client with the OPC UA server URL:
python main.py opc.tcp://localhost:4840To use secure mode, add the following parameters:
python main.py opc.tcp://localhost:4840 --secureThe key and cert flags are optional in secure mode. If specified, the certificate and key in the client_py_LIDAR folder are used. If not specified, the path can be passed as a parameter (both -key and cert must be .pem files).
To use polling instead of subscriptions (useful for slower update rates):
python main.py opc.tcp://localhost:4840 --RATE 1.0The --RATE parameter specifies the polling interval in seconds (e.g., 0.5, 1.0, 2.5).
To use secure mode, add the following parameters:
python main.py opc.tcp://localhost:4840 --RATE 1.0 --secureOnce running, the client provides an interactive menu:
================= SI3 OPC UA =================
1) Show Graph (Bokeh)
2) Call generic method (call: <name>)
3) Test OPC UA getters (usage example)
4) Test OPC UA setters (usage example)
b) Put in background (recover terminal)
q) Exit
==============================================
Option:
Menu Options:
- 1 - Opens/refreshes the Bokeh web interface for real-time data visualization
- 2 - Calls a generic OPC UA method by name
- 3 - Demonstrates how to use the getter functions with example calls
- 4 - Demonstrates how to use the setter functions with example calls
- b/bg/background - Puts the process in background mode (frees the terminal)
- q/quit/exit - Exits the client
The client automatically starts a Bokeh server when launched. Access the visualization at:
http://<your-ip>:5010/bokeh_app(or the port shown in the console)
The Bokeh interface allows you to:
- Select multiple variables to graph
- Adjust the time window (1-60 minutes)
- View real-time sensor data
- Hide/show individual variables via legend clicks
- Automatic connection and reconnection
- Connection health monitoring
- Support for subscriptions and polling modes
- Subscription Mode (default): Uses OPC UA subscriptions for efficient real-time updates (500ms period)
- Polling Mode: Periodic reads at specified intervals (useful for slower networks)
- In-memory history repository
- Configurable retention period (default: 10 minutes)
- Automatic data pruning
- Bokeh-based web interface
- Multi-variable plotting
- Real-time updates
- Interactive controls (zoom, pan, reset)
- Generic node value reading
- Support for all methods under the "Controls" object
- Examples:
ServFixed,ServRandom,change_fix_val,update_time,ServOutOfRange
All getter functions are located in:
src/domain/dto.py
The client provides async getter functions for reading OPC UA node values. All getters follow the pattern:
async def get_<attribute_name>(connector) -> Optional[<type>]:
"""Description"""
return await connector.read_node("ns=2;s=si3_get_<attribute_name>")get_state(connector)- Server stateget_status(connector)- Server statusget_app_name(connector)- Application nameget_opcua_port(connector)- OPC UA portget_web_port(connector)- Web portget_app_start_time(connector)- Application start timeget_serial_number(connector)- Serial numberget_prosys_sdk_version(connector)- Prosys SDK versionget_current_session_number(connector)- Current session numberget_sessions_name(connector)- Sessions nameget_random_generator_code(connector)- Random generator codeget_verbose_status(connector)- Verbose statusget_updatetime(connector)- Update timeget_isotstamp(connector)- ISO timestampget_heartbeat(connector)- Heartbeat counterget_table_file_name(connector)- Table file name
get_error_number(connector)- Error numberget_error_information(connector)- Error informationget_error_recovering(connector)- Error recovery statusget_error_number_recovered(connector)- Recovered error numberget_error_number_outofrange(connector)- Out of range error flag
get_elastic_channel_355_nm(connector)- Elastic Channel 355nmget_elastic_channel_532_nm(connector)- Elastic Channel 532nmget_elastic_channel_1064_nm(connector)- Elastic Channel 1064nmget_raman_channel_n2_387_nm(connector)- Raman Channel N2 387nmget_raman_channel_h2o(connector)- Raman Channel H2Oget_raman_range_signal_counts(connector)- Raman Range Signal Countsget_statistical_error_per_bin(connector)- Statistical Error Per Binget_integration_time(connector)- Integration Timeget_co_polar_355_nm(connector)- Co-Polar 355nmget_cross_polar_355_nm(connector)- Cross-Polar 355nmget_co_polar_532_nm(connector)- Co-Polar 532nmget_cross_polar_532_nm(connector)- Cross-Polar 532nmget_depolarisation_ratio_profile(connector)- Depolarisation Ratio Profile
get_backscatter_coefficient_beta_z(connector)- Backscatter Coefficient Beta Zget_extinction_coefficient_alpha_z(connector)- Extinction Coefficient Alpha Zget_aerosol_optical_depth(connector)- Aerosol Optical Depthget_lidar_ratio_s_z(connector)- Lidar Ratio S Zget_humidity_profile_h2o(connector)- Humidity Profile H2Oget_pbl_height(connector)- PBL Heightget_cloud_base_height(connector)- Cloud Base Heightget_snr_per_bin(connector)- SNR Per Bin
get_timestamp_utc(connector)- Timestamp UTCget_integration_accumulation_time(connector)- Integration Accumulation Timeget_number_of_accumulated_pulses(connector)- Number of Accumulated Pulsesget_vertical_resolution_bin_size(connector)- Vertical Resolution Bin Sizeget_temporal_resolution(connector)- Temporal Resolutionget_global_snr(connector)- Global SNRget_quality_flags(connector)- Quality Flagsget_internal_temperatures(connector)- Internal Temperaturesget_laser_readings_energy_voltage_prf(connector)- Laser Readings
get_motorised_2_axis_mount(connector)- Motorised 2-Axis Mountget_three_d_scanning_capability(connector)- 3D Scanning Capabilityget_azimuth_range_0_360_deg(connector)- Azimuth Rangeget_elevation_range_neg5_90_deg(connector)- Elevation Rangeget_pointing_accuracy(connector)- Pointing Accuracyget_angular_speed_configurable(connector)- Configurable Angular Speedget_mode_stare_fixed(connector)- Mode Stare Fixedget_mode_raster_scan(connector)- Mode Raster Scanget_mode_cone_scan(connector)- Mode Cone Scanget_mode_volume_scan(connector)- Mode Volume Scan
get_ethernet_api_gui_control(connector)- Ethernet API/GUI Controlget_cmd_set_az(connector)- Command Set Azimuthget_cmd_set_el(connector)- Command Set Elevationget_cmd_home(connector)- Command Homeget_cmd_park(connector)- Command Parkget_cmd_start_scan(connector)- Command Start Scan
import asyncio
from infrastructure.opcua_connector import OpcUaConnector
from domain.dto import get_state, get_status, get_heartbeat, get_elastic_channel_355_nm
async def main():
# Connect to OPC UA server
connector = OpcUaConnector("opc.tcp://localhost:4840")
await connector.connect()
try:
# Read values using getters
state = await get_state(connector)
status = await get_status(connector)
heartbeat = await get_heartbeat(connector)
elastic_355 = await get_elastic_channel_355_nm(connector)
print(f"State: {state}")
print(f"Status: {status}")
print(f"Heartbeat: {heartbeat}")
print(f"Elastic Channel 355nm: {elastic_355}")
finally:
await connector.disconnect()
if __name__ == "__main__":
asyncio.run(main())import asyncio
from infrastructure.opcua_connector import OpcUaConnector
from application.use_cases.controls import serv_fixed, serv_random, method_cmd
async def main():
connector = OpcUaConnector("opc.tcp://localhost:4840")
await connector.connect()
try:
NS_IDX = 2 # Namespace index for Controls object
# Call specific methods
result1 = await serv_fixed(connector, NS_IDX)
result2 = await serv_random(connector, NS_IDX)
# Read generic node by name
result3 = await method_cmd(connector, NS_IDX, "lidar_simul_on") ## lidar_simul_on is the NodeID
print(f"ServFixed result: {result1}")
print(f"ServRandom result: {result2}")
print(f"update_time result: {result3}")
finally:
await connector.disconnect()
if __name__ == "__main__":
asyncio.run(main())import asyncio
from infrastructure.opcua_connector import OpcUaConnector
from infrastructure.memory_history_repo import MemoryHistoryRepo
from application.use_cases.monitor import monitor_subscription
from domain.entity import LIDER
async def main():
connector = OpcUaConnector("opc.tcp://localhost:4840")
await connector.connect()
lider = LIDER()
history = MemoryHistoryRepo(retention_minutes=10)
# Attribute map: domain attribute -> OPC UA node ID
ATTR_MAP = {
"HEARTBEAT": "ns=2;s=heartbeat",
"ELASTIC_CHANNEL_355_NM": "ns=2;s=lidar_get_ElasticChannel355Nm",
}
try:
# Start monitoring with subscription (500ms period)
await monitor_subscription(connector, ATTR_MAP, lider, history, period_ms=500)
# Access history
heartbeat_history = history.get_history("heartbeat")
for timestamp, value in heartbeat_history:
print(f"{timestamp}: {value}")
finally:
await connector.disconnect()
if __name__ == "__main__":
asyncio.run(main())si3-python-client-sub/
├── main.py # Main entry point
├── setup.py # Package setup
├── pyproject.toml # Project configuration
├── src/
│ ├── domain/
│ │ ├── entity.py # SI3 entity (data model)
│ │ └── dto.py # Data Transfer Objects + Getter functions
│ ├── infrastructure/
│ │ ├── opcua_connector.py # OPC UA connection handler
│ │ └── memory_history_repo.py # In-memory history storage
│ ├── application/
│ │ └── use_cases/
│ │ ├── controls.py # Control method calls
│ │ ├── history.py # History queries
│ │ └── monitor.py # Monitoring logic
│ └── presentation/
│ └── bokeh_app.py # Bokeh visualization
You can optionally create a .env file in the project root:
opcua_url=opc.tcp://localhost:4840The client will automatically load this if present.
- Verify the OPC UA server is running and accessible
- Check the URL format:
opc.tcp://hostname:port - Ensure firewall allows the connection
- Check if port 5010 (or the shown port) is available
- Verify all dependencies are installed:
pip list
- Ensure the package is installed:
pip install -e . - Activate the virtual environment before running
[Add your license information here]
[Add contribution guidelines here]