Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
11e9d9a
Add plugin driver system with config parsing
marconetsf Sep 11, 2025
fb3b5d0
Refactor plugin driver for Python Modbus support
marconetsf Sep 11, 2025
cd10661
sync plugin driver
marconetsf Sep 11, 2025
158b390
Adjusting python.h include
marconetsf Sep 12, 2025
77dbf44
fix init driver's args encapsulation
marconetsf Sep 12, 2025
b10e7b8
adjusting brackets position and function identation
marconetsf Sep 12, 2025
75dfe2f
fix cmakelist
marconetsf Sep 12, 2025
e89ff10
adjust python_plugin_bridge.h identation
marconetsf Sep 15, 2025
ccef749
python start funct running within a thread
marconetsf Sep 15, 2025
ce90100
fixing pointer dereferencing
marconetsf Sep 15, 2025
3063116
Fix buffer access in Python plugin driver
marconetsf Sep 16, 2025
ef9b1ae
deleting stop call
marconetsf Sep 16, 2025
39a893d
Remove unused _runtime_args_capsule variable
marconetsf Sep 16, 2025
df1ac5d
Refactor Python plugin threading and lifecycle management
marconetsf Sep 17, 2025
89e8a3e
Refactor plugin driver cleanup and GIL management
marconetsf Sep 17, 2025
32fc0ec
Refactor plugin loop to run in a separate thread
marconetsf Sep 17, 2025
993cfef
Refactor Modbus plugin for safer buffer access
marconetsf Sep 17, 2025
763b105
Update Python plugin documentation and type safety
marconetsf Sep 17, 2025
d447cc1
moving examples and plugins to respective folder
marconetsf Sep 17, 2025
85ac4cb
deleting unused plugins from config
marconetsf Sep 17, 2025
79bb1b6
Expose plugin mutex helpers and use in PLC cycle
marconetsf Sep 17, 2025
c3c0ac9
Merge branch 'development' into task/RTOP-57-implement-driver-contracts
marconetsf Sep 18, 2025
e5cfc77
Changing pluggins paths to meet exec.sh start path
marconetsf Sep 18, 2025
345fc60
Merge pull request #5 from Autonomy-Logic/task/RTOP-57-implement-driv…
thiagoralves Sep 18, 2025
469fc7b
Rtop 58 plugin modbus slave (#6)
marconetsf Sep 23, 2025
70c560e
Merge branch 'development' into RTOP-52-Standard-Network-Driver
marconetsf Sep 23, 2025
9cf4e5f
fixing plugin's dedicated data retrieval
marconetsf Sep 24, 2025
9bb279c
RTOP 74 adding plugin individual venv usage
marconetsf Sep 25, 2025
2771514
Merge branch 'development' into RTOP-74-Define-venv-environment-for-p…
marconetsf Sep 25, 2025
14301df
Merge branch 'development' into RTOP-52-Standard-Network-Driver
marconetsf Sep 25, 2025
69d1056
Update scripts/manage_plugin_venvs.sh
marconetsf Sep 25, 2025
cefb954
Disabling initial plugin prints and avoiding code insertion
marconetsf Sep 26, 2025
b021601
Merge branch 'RTOP-74-Define-venv-environment-for-python-plugins' of …
marconetsf Sep 26, 2025
f8ab311
install now has support for apt yum and dfs and plc program is being …
marconetsf Sep 26, 2025
e086fea
adding proper build error and success logs
marconetsf Sep 29, 2025
55dfcd2
[RTOP 74][WIP] implementing initialization scripts
marconetsf Sep 29, 2025
7f98748
changing runtime venv from .venv to venvs/runtime/
marconetsf Sep 29, 2025
43e43ac
Merge branch 'development' into RTOP-52-Standard-Network-Driver
marconetsf Sep 29, 2025
950a475
Merge branch 'RTOP-52-Standard-Network-Driver' into RTOP-74-Define-ve…
thiagoralves Sep 29, 2025
659f49b
Merge pull request #11 from Autonomy-Logic/RTOP-74-Define-venv-enviro…
thiagoralves Sep 29, 2025
a919d86
Add requirements.txt to the Modbus driver
thiagoralves Sep 29, 2025
c2acb8c
Add checks on install and start_openplc scripts
Sep 29, 2025
f8974ee
Quick fix on start_openplc.sh
Sep 29, 2025
aad7e53
Fix zip file check to allow generated bash script
lucasbutzke Sep 30, 2025
865c429
[RTOP-72] Parse and log unix socket log messages
lucasbutzke Sep 30, 2025
8791754
[RTOP-72] Additional Exceptions
lucasbutzke Sep 30, 2025
debb9e9
[RTOP-72] Logging format refactor
lucasbutzke Oct 1, 2025
4d65c3d
[RTOP-76] Logging module
lucasbutzke Oct 1, 2025
c1f7aa7
Merge branch 'fix-reastapi-zip-file-check' into RTOP-76-Runtime-logs-…
lucasbutzke Oct 1, 2025
b0ea1fd
[RTOP-76] Logger parser
lucasbutzke Oct 1, 2025
58b5f48
[RTOP-76] Logger in app
lucasbutzke Oct 2, 2025
7287680
Merge branch 'development' into RTOP-76-Runtime-logs-parser
lucasbutzke Oct 2, 2025
7dd4fc3
[RTOP-76] Runtime logging buffer test
lucasbutzke Oct 2, 2025
6651c8c
[RTOP-76] Fix logging instantiation, parser and buffer
lucasbutzke Oct 3, 2025
b8bff1e
[RTOP-76] Runtime Logs parser, buffering and json response
lucasbutzke Oct 3, 2025
4dd1e49
Merge remote-tracking branch 'origin/development' into RTOP-76-Runtim…
lucasbutzke Oct 8, 2025
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: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"dlfcn.h": "c",
"utils.h": "c",
"plc_state_manager.h": "c",
"unix_socket.h": "c"
"unix_socket.h": "c",
"plugin_driver.h": "c"
},
"editor.rulers": [
80
Expand Down
26 changes: 16 additions & 10 deletions webserver/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import logging
import os
import ssl
from pathlib import Path
Expand Down Expand Up @@ -28,12 +27,15 @@
MAX_FILE_SIZE
)

# from logger import get_logger, LogParser


app = flask.Flask(__name__)
app.secret_key = str(os.urandom(16))
login_manager = flask_login.LoginManager()
login_manager.init_app(app)

logger = logging.getLogger(__name__)
# logger = get_logger(use_buffer=True)
Comment thread
marconetsf marked this conversation as resolved.

runtime_manager = RuntimeManager(
runtime_path="./build/plc_main",
Expand Down Expand Up @@ -97,7 +99,7 @@ def restapi_callback_get(argument: str, data: dict) -> dict:
"""
Dispatch GET callbacks by argument.
"""
logger.debug("GET | Received argument: %s, data: %s", argument, data)
# logger.debug("GET | Received argument: %s, data: %s", argument, data)
handler = GET_HANDLERS.get(argument)
if handler:
return handler(data)
Expand Down Expand Up @@ -166,7 +168,7 @@ def restapi_callback_post(argument: str, data: dict) -> dict:
"""
Dispatch POST callbacks by argument.
"""
logger.debug("POST | Received argument: %s, data: %s", argument, data)
# logger.debug("POST | Received argument: %s, data: %s", argument, data)
handler = POST_HANDLERS.get(argument)

if not handler:
Expand All @@ -184,9 +186,10 @@ def run_https():
try:
db.create_all()
db.session.commit()
logger.info("Database tables created successfully.")
# logger.info("Database tables created successfully.")
except Exception as e:
logger.error("Error creating database tables: %s", e)
# logger.error("Error creating database tables: %s", e)
pass

try:
cert_gen = CertGen(hostname=HOSTNAME, ip_addresses=["127.0.0.1"])
Expand All @@ -208,14 +211,17 @@ def run_https():
)

except FileNotFoundError as e:
logger.error("Could not find SSL credentials! %s", e)
# logger.error("Could not find SSL credentials! %s", e)
pass
except ssl.SSLError as e:
logger.error("SSL credentials FAIL! %s", e)
# logger.error("SSL credentials FAIL! %s", e)
pass
except KeyboardInterrupt:
logger.info("HTTP server stopped by KeyboardInterrupt")
# logger.info("HTTP server stopped by KeyboardInterrupt")
pass
finally:
runtime_manager.stop()
logger.info("Runtime manager stopped")
# logger.info("Runtime manager stopped")


if __name__ == "__main__":
Expand Down
8 changes: 1 addition & 7 deletions webserver/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import logging
import os
import re
import secrets
Expand All @@ -11,12 +10,7 @@
DB_PATH = Path(__file__).resolve().parent.parent / "restapi.db"
BASE_DIR = os.path.abspath(os.path.dirname(__file__))

logger = logging.getLogger(__name__)
logging.basicConfig(
level=logging.DEBUG, # Minimum level to capture
format="[%(levelname)s] %(asctime)s - %(message)s",
datefmt="%H:%M:%S",
)
# logger = logging.getLogger("logger")


# Function to validate environment variable values
Expand Down
19 changes: 9 additions & 10 deletions webserver/credentials.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import datetime
import ipaddress
import os
import logging

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID

logger = logging.getLogger(__name__)
# logger = logging.getLogger("logger")


class CertGen:
Expand Down Expand Up @@ -40,7 +39,7 @@ def generate_key(self):
)

def generate_self_signed_cert(self, cert_file, key_file):
logger.debug("Generating self-signed certificate for %s...", self.hostname)
# logger.debug("Generating self-signed certificate for %s...", self.hostname)

self.generate_key()

Expand Down Expand Up @@ -72,13 +71,13 @@ def generate_self_signed_cert(self, cert_file, key_file):
serialization.NoEncryption(),
)
)
logger.debug("Certificate saved to %s", cert_file)
logger.debug("Private key saved to %s", key_file)
# logger.debug("Certificate saved to %s", cert_file)
# logger.debug("Private key saved to %s", key_file)

def is_certificate_valid(self, cert_file):
"""Check if the certificate is valid."""
if not os.path.exists(cert_file):
logger.warning("Certificate file not found: %s", cert_file)
# logger.warning("Certificate file not found: %s", cert_file)
return False

try:
Expand All @@ -90,15 +89,15 @@ def is_certificate_valid(self, cert_file):
now = datetime.datetime.now(datetime.timezone.utc)

if now < cert.not_valid_before_utc:
logger.warning("Certificate is not yet valid. Valid from: %s", cert.not_valid_before_utc)
# logger.warning("Certificate is not yet valid. Valid from: %s", cert.not_valid_before_utc)
return False
if now > cert.not_valid_after_utc:
logger.warning("Certificate has expired. Expired on: %s", cert.not_valid_after_utc)
# logger.warning("Certificate has expired. Expired on: %s", cert.not_valid_after_utc)
return False

logger.info("Certificate is valid. Expires on: %s", cert.not_valid_after_utc)
# logger.info("Certificate is valid. Expires on: %s", cert.not_valid_after_utc)
return True

except Exception as e:
logger.error("Error loading or parsing certificate: %s", e)
# logger.error("Error loading or parsing certificate: %s", e)
return False
4 changes: 4 additions & 0 deletions webserver/__init__.py → webserver/logger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import logging
import logging.config
from .logger import get_logger
from .parser import LogParser
from .bufferhandler import BufferHandler

__all__ = ["get_logger", "LogParser", "BufferHandler"]
__version__ = "0.1"
__author__ = "Autonomy"
__license__ = "MIT"
Expand Down
64 changes: 64 additions & 0 deletions webserver/logger/bufferhandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import logging
from collections import deque
from typing import List, Optional
import json
import re
from datetime import datetime


class BufferHandler(logging.Handler):
"""
Custom logging handler that stores log records in memory (FIFO).
Logs are formatted using the attached formatter (JSON).
"""

def __init__(self, capacity: int = 1000):
super().__init__()
self.buffer = deque(maxlen=capacity)

def emit(self, record: logging.LogRecord) -> None:
try:
self.buffer.append(self.format(record))
except Exception:
self.handleError(record)

def get_logs(self, count: Optional[int] = None) -> List[str]:
"""Retrieve logs from buffer."""
if count is None or count > len(self.buffer):
return list(self.buffer)
return list(self.buffer)[-count:]

def normalize_buffer_logs(self, buffer_records):
"""
Takes a list of log strings from buffer and returns a list of clean JSON dicts.
"""
result = []
json_extract = re.compile(r'(\{.*\})') # match JSON inside log line

for record in buffer_records:
match = json_extract.search(record)
if not match:
continue

try:
raw_json = json.loads(match.group(1))
# Convert unix timestamp → readable datetime
ts = int(raw_json.get("timestamp", 0))
dt = datetime.utcfromtimestamp(ts).isoformat() + "Z"

entry = {
"timestamp": dt,
"level": raw_json.get("level", "INFO"),
"message": raw_json.get("message", "")
}
result.append(entry)
except (json.JSONDecodeError, ValueError):
continue

return result

def clear(self) -> None:
self.buffer.clear()

def __len__(self):
return len(self.buffer)
19 changes: 19 additions & 0 deletions webserver/logger/formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import logging
import time
import json

class JsonFormatter(logging.Formatter):
"""Format log records as JSON strings."""

def format(self, record: logging.LogRecord) -> str:
log_dict = {
"timestamp": str(int(record.created)), # epoch seconds
"level": record.levelname,
"message": record.getMessage()
}

# Include optional fields if present
if hasattr(record, "source"):
log_dict["source"] = record.source

return json.dumps(log_dict, ensure_ascii=False)
38 changes: 38 additions & 0 deletions webserver/logger/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import logging
from .formatter import JsonFormatter
from .bufferhandler import BufferHandler


def get_logger(name: str = "logger",
level: int = logging.INFO,
use_buffer: bool = False):
"""Return a logger instance with custom formatting."""

collector_logger = logging.getLogger(name)
collector_logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
collector_logger.addHandler(handler)

buffer_handler = None

if use_buffer:
# Use buffer handler for log messages
buffer_handler = BufferHandler()
buffer_handler.setFormatter(
logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
collector_logger.addHandler(buffer_handler)

if use_buffer:
Comment thread
lucasbutzke marked this conversation as resolved.
# Find buffer handler again if it already exists
if buffer_handler is None:
for h in collector_logger.handlers:
if isinstance(h, BufferHandler):
buffer_handler = h
break
Comment thread
lucasbutzke marked this conversation as resolved.
return collector_logger, buffer_handler
else:
return collector_logger, None

# return collector_logger
74 changes: 74 additions & 0 deletions webserver/logger/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# logger/parser.py
import logging
import re
import time
import json

LOG_PATTERN = re.compile(r'^\[(?P<level>\w+)\]\s*(?P<message>.*)$')

LEVEL_MAP = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
}


class LogParser:
def __init__(self, collector_logger: logging.Logger):
self.collector_logger = collector_logger

def parse_and_log(self, line: str):
"""Parse incoming log line and re-log it in normalized JSON format."""
sline = line.strip()
if not sline:
return

timestamp = int(time.time())
level_name = "INFO"
level = logging.INFO
message = sline

# Case 1: JSON log already
try:
parsed = json.loads(sline)
if isinstance(parsed, dict) and "message" in parsed:
# Preserve incoming JSON fields, but ensure timestamp is present
parsed.setdefault("timestamp", str(timestamp))
level_name = parsed.get("level", "INFO")
level = LEVEL_MAP.get(level_name, logging.INFO)
log_entry = parsed
else:
raise ValueError("Not a valid log JSON dict")
except (json.JSONDecodeError, ValueError):
# Case 2: Regex log like "[INFO] Something"
match = LOG_PATTERN.match(sline)
if match:
level_name = match["level"]
level = LEVEL_MAP.get(level_name, logging.INFO)
message = match["message"]
else:
message = sline

log_entry = {
"timestamp": str(timestamp),
"level": level_name,
"message": message
}

# Create final JSON string
json_log = json.dumps(log_entry, ensure_ascii=False)

# Push into Python logging
record = self.collector_logger.makeRecord(
name="external",
level=level,
fn="",
lno=0,
msg=json_log,
args=(),
exc_info=None
)
record.source = "external"
self.collector_logger.handle(record)
Loading