Skip to main content

OqlOS — Operation Query Language runtime for hardware testing

Project description

OqlOS — Operation Query Language Runtime

AI Cost Tracking

PyPI Version Python License AI Cost Human Time Model

  • 🤖 LLM usage: $5.1000 (34 commits)
  • 👤 Human dev: ~$2461 (24.6h @ $100/h, 30min dedup)

Generated on 2026-05-05 using openrouter/qwen/qwen3-coder-next


Version Python License

OqlOS is the core runtime for executing OQL (Operation Query Language) hardware testing scenarios. It provides the execution engine, hardware abstraction layer, and API server for running automated hardware tests.

Installation

# Install from source with development dependencies
pip install -e ".[dev]"

# Basic installation
pip install -e .

CLI Quick Check (step by step)

If you see:

oqlos: command not found

that is expected — oqlos is the package name, not the CLI command.

Use this sequence:

# 1) Activate your virtualenv
source .venv/bin/activate

# 2) Install the project in editable mode (creates console scripts)
python -m pip install -e .

# 3) Check available CLI help
oqlctl --help

# 4) If PATH still does not see scripts, use module form directly
python -m oqlos.tools.cql_cli.main --help

Main commands provided by this project:

  • oqlctl — scenario CLI (validate / dry-run / execute)
  • oqlctl detect — smart local hardware detection (USB/serial/I2C/Modbus + config)
  • oqlctl doctor — operator-facing hardware/config doctor with repair hints
  • oqlos-server — API server
  • oqlos-events — event server

Hardware Doctor Quick Check

Use doctor before executing real scenarios. It compares what the host can see with oqlos.yaml and with the firmware bridge, then reports actionable issues such as mock mode, missing device mounts, a busy serial port, stale HTTP driver services, or a Modbus port/baud mismatch.

# Human-readable report
oqlctl doctor

# Machine-readable report for scripts
oqlctl doctor --json

# Local host detection only
oqlctl detect

# Backward-compatible aliases
oqlctl --status
oqlctl --identify

# Apply safe repairs only (currently: update detected Modbus params in oqlos.yaml)
oqlctl doctor --fix

# Example operator workflow
bash examples/hardware/doctor-workflow.sh

If oqlctl --help shows only legacy Click subcommands such as run, cmd, and scenarios, activate this repository virtualenv or call .venv/bin/oqlctl directly. The smart detect/doctor and hardware preflight paths live in the current repository CLI.

Current expected Modbus RTU defaults for the Waveshare 8CH IO controller are 19200 8N1; prefer a stable /dev/serial/by-id/... path in oqlos.yaml instead of relying on changing /dev/ttyACM* numbering. doctor resolves those symlinks, so it can still report the real busy device, e.g. /dev/ttyACM0, when another process owns the configured by-id path. If the hardware is moved to another port, run oqlctl doctor --fix after confirming the detected device is correct. Runtime changes such as switching firmware from mock to real, restarting containers, or mounting /dev/ttyACM*//dev/ttyUSB* are reported as manual/unsafe repairs and are not applied automatically.

Requirements

  • Python 3.10+
  • FastAPI, Uvicorn (for API server)
  • Modbus support (for hardware communication)

Quick Start

Start the API Server

# Start with real hardware
HARDWARE_MODE=real oqlos-server --host 0.0.0.0 --port 8200

# Run with mock hardware (development/testing)
OQLOS_HARDWARE_MODE=mock oqlos-server --port 8200

oqlos-server supports --host and --port flags. Environment-based defaults are still respected when flags are omitted.

Run a Scenario (OQL v3 — flat syntax)

from oqlos.core.interpreter import CqlInterpreter

source = """
SCENARIO: Test
DEVICE_TYPE: BA

GOAL:
  SET NAME 'Check'
  SET pompa-1 5.0 l/min
  WAIT 500ms
  GET AI01
  IF AI01 0.5 .. 0.8 V
  CORRECT 'Voltage OK'
  ERROR 'Voltage out of range'
  SAVE high-voltage
"""

interp = CqlInterpreter(mode="dry-run")
result = interp.run(source, "test.oql")
print(result.ok)  # True if successful

OQL v3 is a flat, quote-free syntax with 12 base commands (SET, GET, WAIT, SAVE, CHECK, MIN, MAX, SAMPLE, LOG, ERROR, CALL, INCLUDE). See docs/oql-spec.md for the full specification and oqlos/scenarios/OQL-CHEATSHEET.md for a quick reference. The interpreter still parses legacy v1/v2 scripts with quoted identifiers for backward compatibility.

Package Structure

oqlos/
├── core/
│   ├── interpreter.py   # CqlInterpreter — main execution engine
│   ├── oql_parser.py    # OQL v3 flat parser (12 base commands)
│   ├── _oql_adapter.py  # v3 AST → legacy CqlDocument bridge (+ INCLUDE/MACRO)
│   ├── cql_parser.py    # Legacy v1/v2 parser (dispatches to v3 on detection)
│   └── …
├── models/              # Data models (dsl_models, scenario, execution, peripheral)
├── hardware/            # Hardware abstraction (Modbus, HTTP adapters, …)
├── api/                 # FastAPI REST server and routes
├── executor/            # Scenario execution helpers
├── scenarios/           # Scenario files (.oql) — all in v3 flat syntax
│   ├── lib/             # Macro libraries (hardware.oql, peripherals.oql)
│   └── examples/        # Didactic examples
└── shared/              # Utilities (logger, config, version)

Core Components

CqlInterpreter

The main execution engine for OQL scenarios:

from oqlos.core.interpreter import CqlInterpreter

# Modes: "dry-run", "execute", "validate"
interp = CqlInterpreter(
    mode="dry-run",
    firmware_url="http://localhost:8202",
    quiet=False
)

result = interp.run(source_code, filename)
# result.ok: bool — execution success
# result.events: list — execution trace
# result.variables: dict — captured variables

Parser

Auto-detecting parser pipeline:

  1. parse_cql(source, filename) first checks the source with is_flat_oql().
  2. If the source uses v3 flat grammar (GOAL: + SET NAME, no quotes, INCLUDE "..."), it dispatches to parse_flat_oql() which returns a legacy CqlDocument via oqlos/core/_oql_adapter.py (INCLUDE + MACRO/CALL expansion happens here).
  3. Otherwise the legacy state-machine parser handles it.
from oqlos.core.cql_parser import parse_cql
from oqlos.core.oql_parser import parse_oql

doc = parse_cql(source, "test.oql")      # either path
raw = parse_oql(source, "test.oql")      # just the v3 AST (OqlDoc)

API Endpoints

When running oqlos-server:

Endpoint Method Description
/api/hardware/peripherals GET List connected hardware
/api/scenarios GET List available scenarios
/api/scenarios/{id}/run POST Execute a scenario
/health GET Health check

OQL Scenario Format (v3 Flat Syntax)

OQL scenarios describe hardware tests with a minimal set of 12 base commands: SET, GET, WAIT, SAVE, CHECK, MIN, MAX, SAMPLE, LOG, ERROR, CALL, INCLUDE — plus block headers GOAL, CONFIG and MACRO. Full specification: docs/oql-spec.md.

SCENARIO: PSS 7000 Mask Test
DEVICE_TYPE: BA
DEVICE_MODEL: PSS 7000
MANUFACTURER: Dräger

INCLUDE "lib/peripherals.oql"

CONFIG reset:
  CALL init-all

GOAL:
  SET NAME 'Visual inspection'
  SET valve-nc 1
  WAIT 2s
  GET AI01
  IF AI01 0.60 .. 0.67 V
  CORRECT 'NC voltage in range'
  ERROR 'NC voltage out of range'
  SAVE nc-voltage-reading

Key rules:

  • Identifiers are bare — no surrounding quotes (pump-main, not 'pump-main'). For names with spaces use brackets: SET [pompa głównego obiegu] 5 l/min.
  • GOAL name set via SET NAME — use GOAL: followed by SET NAME 'nazwa' inside the block. Legacy GOAL name: still works for backward compatibility.
  • No IF/ELSE/ENDIF — use IF min .. max unit with CORRECT/ERROR messages for range assertions, or split into multiple GOAL blocks for sequencing.
  • Unicode is welcomeciśnienie-NC, °C, %RH, μV, m³/h

CONFIG Blocks

CONFIG blocks are semantically identical to GOAL but marked [CONFIG] in logs — convention for initialization and cleanup:

SCENARIO: System Startup
DEVICE_TYPE: BA

INCLUDE "lib/peripherals.oql"

CONFIG safety-initialization:
  CALL init-pump
  CALL init-valves-main
  WAIT 500ms

CONFIG pump-calibration:
  # 10 l/min corresponds to 100% PWM by default
  SET PUMP_FLOW_FULL_SCALE_LPM 10.0

GOAL:
  SET NAME 'Voltage test'
  SET valve-nc 1
  WAIT 1s
  GET AI01
  SAVE voltage-test

Macros and INCLUDE

Reusable sequences live in oqlos/scenarios/lib/ and are pulled in with INCLUDE. Positional arguments use $1, $2, … placeholders:

INCLUDE "lib/hardware.oql"

MACRO pump-ramp:
  SET pump-main $1 l/min
  WAIT $2
  SET pump-main 0

GOAL:
  SET NAME 'Smoke'
  CALL pump-ramp 5 2s
  CALL hw-valves-smoke
  CALL hw-sensors-baseline

Running Scenarios

# Dry-run (validate and simulate)
oqlctl scenarios/config-peripherals.oql --mode dry-run
oqlctl run scenarios/config-peripherals.oql --mode dry-run

# Execute on real hardware
oqlctl scenarios/config-peripherals.oql --mode execute
oqlctl run scenarios/config-peripherals.oql --mode execute

# Execute with custom firmware URL
oqlctl scenarios/config-peripherals.oql \
  --firmware-url http://localhost:8202 \
  --mode execute

# Run a scenario directly from a raw .oql URL or JSON source endpoint
oqlctl run "http://localhost:9000/scenarios/maskleaktest-nadcisnieniestatyczne.oql" \
  --mode dry-run

# Fastest single-command hardware execution (v3 syntax)
oqlctl cmd "SET pompa-1 0"

# Single command without touching hardware
oqlctl cmd "SET pompa-1 0" --mode dry-run

# Parseable single-command dry-run output
oqlctl cmd "SET pompa-1 0" --mode dry-run --json -q

# Validate every .oql in a directory tree
oqlctl --validate-dir oqlos/scenarios

For URL runs, the response must be raw OQL/CQL text or JSON with one of code, dsl, source, or content. Editor/browser routes such as http://localhost:8096/scenarios?scenario=... return HTML and are rejected.

Use cmd when you want to send a single OQL line to the firmware; use a file path when the action requires multiple steps.

Scenario Sync (DB <-> local)

This repo includes scripts for synchronizing scenario DSL between database rows and local .oql files.

1) DB -> local files

Export all scenarios from DB API to a ZIP archive:

python3 scripts/scenarios_export.py \
  --base "http://localhost:8096" \
  --all \
  --out scenarios.zip

Unpack to a local directory:

mkdir -p scenarios
unzip -o scenarios.zip -d scenarios

The archive includes one <id>.oql file per scenario and manifest.json.

Export a single scenario (id or UI URL with ?scenario=):

python3 scripts/scenarios_export.py \
  --base "http://localhost:8096" \
  --scenario "ts-temp-wilgotnosc" \
  --out ts-temp-wilgotnosc.oql.bash

2) local files -> DB (Import)

Import all .oql files from a local directory into the database, overwriting existing scenarios:

python3 scripts/scenarios_export.py --import --dir ./scenarios

With custom API base and validation disabled:

python3 scripts/scenarios_export.py \
  --base "http://localhost:8096" \
  --import \
  --dir ./scenarios \
  --no-validate

Each file named <id>.oql updates the scenario <id> via PATCH. Files are validated against OQL v4 by default before import.

Alternative: Use the migration/sync script for more control:

Dry-run preview (no write):

python3 scripts/oql_v2_to_v4_migrate_db.py \
  --source-url "http://localhost:8100/connect-data/test-scenarios" \
  --prefer-local \
  --pretty

Apply updates to DB:

python3 scripts/oql_v2_to_v4_migrate_db.py \
  --source-url "http://localhost:8100/connect-data/test-scenarios" \
  --prefer-local \
  --apply \
  --write-method PATCH \
  --write-url "http://localhost:8101/api/v1/data/test_scenarios/{id}" \
  --pretty

Notes:

  • --prefer-local reads local files from oqlos/scenarios/<id>.oql.
  • DB row id must match local filename (without .oql).
  • Run without --apply first to verify changes and runtime validation output.

CLI Output Example

📋 CQL: Konfiguracja Peryferii
🔧 Device: BA / PSS 7000
🎯 GOAL: [CONFIG] init-pompa
  ⚙️ SET [pump-main] = [0]
  ⚙️ SET [pompa-1] = [0]
  ⏳ WAIT 0.5s
  ✅ [passed] [CONFIG] init-pompa
🎯 GOAL: [CONFIG] init-zawory-nc
  ⚙️ SET [valve-nc] = [0]
  ...
✅ Konfiguracja Peryferii: 10/10 passed

Supported Hardware

  • Valves: valve-1 through valve-14, valve-nc, valve-sc, valve-wc (Modbus RTU via /dev/serial/by-id/... or /dev/ttyACM* @ 19200 8N1)
  • Pump: pump-main (DRI0050 PWM motor driver via HTTP :49055)
  • Artificial lung: lung-main (Tic T249 stepper via HTTP :8205)
  • Sensors: AI01 (NC), AI02 (SC), AI03 (WC) (piADC ADS1115 via HTTP :8204; raw ADC voltage)

Hardware Adapters

Adapter Class Protocol Default URL
Motor (pump) _DRI0050MotorAdapter HTTP POST /api/speed http://localhost:49055
Lung (artificial lung) _Tic249LungAdapter HTTP POST /api/lung http://localhost:8205
Valves _ModbusAdapter Modbus RTU (pymodbus) /dev/ttyACM1 serial
Sensors _PiAdcAdapter HTTP GET /api/v1/hardware/sensor/{id} http://localhost:8204

Hardware Identification & Diagnostics

Preferred operator commands:

oqlctl detect              # local USB/serial/I2C/Modbus probe
oqlctl doctor              # detect runtime/config problems
oqlctl doctor --json       # parseable report
oqlctl doctor --fix        # safe config repair for detected Modbus settings

The /api/v1/hardware/identify endpoint returns the adapter registry, live probe status, and a diagnostics block with:

  • USB device inventory
  • Serial port inventory (ttyACM* and ttyUSB*)
  • I2C bus inventory (/dev/i2c-*)
  • Best-effort bridge health snapshot for piadc, motor, lung, and modbus

The current valve calibration flow uses raw piADC voltage windows in the test scenario oqlos/oqlos/scenarios/test-zaworu.oql, while hardware-valves-smoke.oql only verifies basic open/close actuation.

Environment Variables

Variable Default Description
OQLOS_HARDWARE_MODE mock mock or real
OQLOS_MOTOR_URL http://localhost:49055 DRI0050 motor service
OQLOS_LUNG_MOTOR_URL http://localhost:8205 Tic T249 lung service
OQLOS_PIADC_URL http://localhost:8080 piADC sensor service
OQLOS_MODBUS_SERIAL_PORT /dev/ttyACM1 Modbus RTU serial port
OQLOS_MODBUS_BAUD 19200 Modbus baud rate
OQLOS_PUMP_FLOW_FULL_SCALE_LPM 10 Flow rate that maps to 100% PWM for pompa 1

Notes:

  • Both prefixed and legacy env names are accepted (for easier rollout): OQLOS_HARDWARE_MODE or HARDWARE_MODE, OQLOS_FIRMWARE_PORT or FIRMWARE_PORT, etc.
  • Prefer the OQLOS_* namespace in new deployments to avoid collisions with other services.

Docker Deployment

# Development
docker-compose -f docker/docker-compose.dev.yml up

# Production
docker-compose -f docker/docker-compose.prod.yml up -d

Testing

# Run all tests
pytest -q

# Run with coverage
pytest --cov=oqlos

# Run specific test file
pytest tests/test_interpreter.py -v

# Run OQL scenarios (dry-run)
python -m oqlos.core.interpreter scenarios/test-pompy.oql --mode dry-run

Status: current local verification: 356 passed.

Documentation

License

Licensed under Apache-2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oqlos-0.1.11.tar.gz (190.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oqlos-0.1.11-py3-none-any.whl (211.7 kB view details)

Uploaded Python 3

File details

Details for the file oqlos-0.1.11.tar.gz.

File metadata

  • Download URL: oqlos-0.1.11.tar.gz
  • Upload date:
  • Size: 190.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for oqlos-0.1.11.tar.gz
Algorithm Hash digest
SHA256 b03a40d85ac9896b946a15924142638dc19b24c77f37745054cbee8b0464994e
MD5 f3b81b1854ef704bdc249fa278a44bd0
BLAKE2b-256 294d28e931c51987f407e5d7ab0a314326d7294f4d9bf8ff24f812c36a00ab54

See more details on using hashes here.

File details

Details for the file oqlos-0.1.11-py3-none-any.whl.

File metadata

  • Download URL: oqlos-0.1.11-py3-none-any.whl
  • Upload date:
  • Size: 211.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for oqlos-0.1.11-py3-none-any.whl
Algorithm Hash digest
SHA256 2440b30f45190f396491bd6bf23c06ca1ed0d1e24c5fadb6d42061161fdc055b
MD5 7e51d912df9ea3dc9ff8c14518f1b06b
BLAKE2b-256 ef86373e4cf3f642ea9c4961e9da57568c91e73e765dd1fb2ef2d5dacce8f2a0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page