Request a Decision from the Universe: Powered by a Gamma-Event True Random Number Generator

16 min read

https://decision.atlane.de

In meiner Kindheit gab es bei einem Freund ein Spielzeug. Es war eine Kugel, die man schütteln musste; daraufhin ertönten Licht- und Soundeffekte, und man erhielt ein „Ja“ oder „Nein“ als Antwort. Ein beliebtes Spiel bestand darin, Fragen zu stellen und schließlich die vermeintliche Antwort aus der Kristallkugel zu erfahren. Natürlich war uns auch damals schon klar, dass es sich hierbei nicht um echte „Antworten“ handelte, sondern dass wohl irgendein Schaltkreis eine vermeintlich zufällige Antwort vermittelte. Trotzdem war es faszinierend, darüber nachzudenken.

Während der Überlegungen zu den TRNG Artikeln kam die Idee auf, einen solchen Zufallsgenerator mit echter Randomisierung umzusetzen. In diesem Beitrag sollen die Technik und die Umsetzung erläutert werden.

Die vorherigen Artikel, die zu diesem Projekt geführt haben, können hier noch einmal nachgelesen werden:

TRNG BASICS: True Random Number Generator mit dem GDK-101 Gamma Strahlen Detektor und dem Raspberry Pi Pico

TRNG II Statistic Testing – Die Zufälligkeit eines Zufallsgenerators: Einsatz der NIST-Test-Suite : RNG – Random Number Generator mit dem GDK-101 Gamma Strahlen Detektor und einem Raspberry Pi Pico

Hardware

Zunächst gilt es das Lesen und Übertragen von Gamma-Ereignisdaten von einem GDK101-Sensor mit Hilfe eines W5500-EVB-Pico, einem robusten Mikrocontroller-Board, das speziell für Netzwerkanwendungen konzipiert wurde zu bewerkstelligen. Wir werden den Aufbau des Codes detailliert durchgehen und erläutern, wie jeder Teil zur Gesamtfunktionalität des Systems beiträgt.

Hardware Setup

Das Herzstück des Systems ist der W5500-EVB-Pico, eine spezielle Version des Raspberry Pi Pico mit einem Ethernet-Netzwerk Modul. Der GDK101 ist der bekannte Gammastrahlungsdetektor, der Gamma-Ereignisse detektiert und als elektronische Signale ausgibt, welche wir auslesen und verarbeiten.

ADC-Konfiguration für den GDK101

Der erste Schritt in unserem Code ist das Einrichten des ADC (Analog-Digital-Umsetzer), um die analogen Signale vom GDK101 zu digitalisieren. Hier definieren wir den ADC auf einem bestimmten Pin, setzen den Schwellenwert fest, und bereiten einen Puffer für die Speicherung der Daten vor:

import machine

# Setup ADC
adc = machine.ADC(26)  # ADC auf Pin 26 initialisieren
threshold_voltage = 120  # Schwellenspannung in mV
buffer_size = 500  # Größe des Puffers
samples = [0] * buffer_size  # Puffer initialisieren
JavaScript

Erkennung von Gamma-Ereignissen

Um zu bestimmen, ob ein gelesener Wert ein Gamma-Ereignis darstellt, überprüfen wir, ob der Spannungswert den Schwellenwert überschreitet:

def check_threshold(sample):
    voltage = (sample * 3.3) / 65535  # Umrechnung des ADC-Werts in Spannung
    return voltage > (threshold_voltage / 1000.0)  # Vergleich mit dem Schwellenwert
JavaScript

Daten sammeln

Die collect_data()-Funktion ist verantwortlich für das kontinuierliche Auslesen des ADC, das Überprüfen des Schwellenwerts und das Speichern der Daten, sobald ein Ereignis erkannt wird:

def collect_data():
    print("Starting Data Collector Thread")
    current_index = 0
    trigger_found = False
    while True:
        sample = adc.read_u16()  # ADC-Wert lesen
        samples[current_index] = sample

        if not trigger_found and check_threshold(sample):
            trigger_time = utime.time()  # Zeitstempel des Ereignisses
            trigger_found = True

        if trigger_found:
            if current_index == buffer_size - 1:
                data_window = {"timestamp": trigger_time, "data": samples[:]}
                with queue_lock:
                    data_queue.append(data_window)
                trigger_found = False
                current_index = -1
                print(data_window)
        current_index = (current_index + 1) % buffer_size
JavaScript

Datenübertragung

Sobald Daten gesammelt sind, muss ein separater Thread sie sicher an einen Server übertragen. Wir benutzen hierfür HTTP-Requests:

def data_uploader():
    while True:
        utime.sleep(1)
        if data_queue:
            with queue_lock:
                data_window = data_queue.pop(0)
            try:
                headers = {"content-type": "application/json", "Authorization": 'secret-token'}
                response = urequests.post(
                    f"https://{HOST}/{ENDPOINT}",
                    json=data_window,
                    headers=headers
                )
                print("Upload successful:", response.text)
            except Exception as e:
                print("Failed to upload data:", e)
JavaScript

Starten des Systems

Zum Abschluss initialisieren wir das Gerät und starten die beiden Threads:

init_device()
_thread.start_new_thread(data_uploader, ())
_thread.start_new_thread(collect_data, ())
JavaScript

Der Gesamte Code für den GDK101-W5500-EVB-Pico

import network
import ntptime
import machine
from machine import Pin, SPI, WDT
import utime
import _thread
import urequests
import uping
import time

# API REST-Endpoint Configuration
HOST = "domain.tld"
ENDPOINT = "upload"
AUTH_TOKEN = 'secret-token'

# Setup ADC
adc = machine.ADC(26)  # ADC auf Pin 26 initialisieren
threshold_voltage = 120  # Schwellenspannung in mV
buffer_size = 500  # Größe des Puffers
samples = [0] * buffer_size  # Puffer initialisieren
data_queue = []
queue_lock = _thread.allocate_lock()  # Create a lock for thread-safe operations on the queue

# Initialize Watchdog Timer with a timeout of 10000 milliseconds (10 seconds)
wdt = WDT(timeout=8000)

def check_threshold(sample):
    voltage = (sample * 3.3) / 65535
    return voltage > (threshold_voltage / 1000.0)

def collect_data():
    print("Starting Data Collector Thread")
    current_index = 0
    trigger_found = False
    while True:
        sample = adc.read_u16()
        samples[current_index] = sample

        if not trigger_found and check_threshold(sample):
            trigger_time = utime.time()
            trigger_found = True

        if trigger_found:
            if current_index == buffer_size - 1:
                data_window = {"timestamp": trigger_time, "data": samples[:]}
                with queue_lock:  # Acquire the lock before adding to queue
                    data_queue.append(data_window)
                trigger_found = False
                current_index = -1
                print(data_window)
        current_index = (current_index + 1) % buffer_size

        wdt.feed()  # Reset the watchdog timer

def data_uploader():
    print("Starting Data Uploader Thread")
    while True:
        utime.sleep(1)
        while data_queue:
            with queue_lock:  # Acquire the lock before accessing the queue
                if data_queue:
                    data_window = data_queue.pop(0)
            try:
                headers = {
                    "content-type": "application/json",
                    "Authorization": AUTH_TOKEN
                }
                response = urequests.post(
                    f"https://{HOST}/{ENDPOINT}",
                    json=data_window,
                    headers=headers
                )
                print("Upload successful:", response.text)
            except Exception as e:
                print("Failed to upload data:", e)
            
            wdt.feed()  # Reset the watchdog timer
            utime.sleep(1)  # Throttle uploads

def w5x00_init():
    spi = SPI(0, 2_000_000, mosi=Pin(19), miso=Pin(16), sck=Pin(18))
    nic = network.WIZNET5K(spi, Pin(17), Pin(20))
    nic.active(True)
    nic.ifconfig(('192.168.178.56', '255.255.255.0', '192.168.178.1', '8.8.8.8'))

    attempts = 0
    while not nic.isconnected() and attempts < 10:
        time.sleep(1)
        attempts += 1
        print("Attempting to connect:", attempts)

    if not nic.isconnected():
        raise Exception("Failed to connect to the network after multiple attempts")
    print(nic.ifconfig())

def init_device():
    w5x00_init()
    uping.ping(HOST, count=1)
    attempts = 0
    while attempts < 5:
        wdt.feed()  # Reset the watchdog timer
        try:
            ntptime.host = "pool.ntp.org"
            ntptime.settime()
            print("NTP time synchronized")
            return
        except Exception as e:
            print("Failed to synchronize NTP time:", e)
            attempts += 1
            time.sleep(2 ** attempts)

    raise Exception("NTP time could not be synchronized after several attempts")

init_device()
_thread.start_new_thread(data_uploader, ())
collect_data()
Python

Die Flask WebApplication

Das Interface der Genie Application wird mittels Python-Flask bereitgestellt.

Diese Anwendung nutzt Flask-SocketIO für Echtzeit-Kommunikation und SQLAlchemy für die Datenverwaltung.

Die Flask-Anwendung wird zunächst mit einer Standardkonfiguration eingerichtet, einschließlich eines statischen Ordners für die Webressourcen und einer SQLite-Datenbank für die Speicherung der Daten. Wir konfigurieren Flask-SocketIO mit spezifischen CORS-Einstellungen, um sichere Cross-Origin-Anfragen zu ermöglichen.

Das Datenmodell besteht aus zwei Hauptklassen: GammaEventEntry für die Aufzeichnung der Gamma-Ereignisse und ChatMessage für die Verwaltung der Chat-Nachrichten. GammaEventEntry speichert jedes Ereignis mit einem Zeitstempel, den Rohdaten als JSON und einem Bild des Datenplots als Textblob. ChatMessage enthält Informationen zu jeder Chat-Nachricht, einschließlich des Benutzernamens und Zeitstempels.

Die Datenerfassung erfolgt über einen REST-Endpoint, der POST-Anfragen aufnimmt. Dieser Endpoint prüft das Authorization-Token im Header, um sicherzustellen, dass nur autorisierte Benutzer Daten senden können. Wenn die Daten im JSON-Format vorliegen, werden sie zusammen mit einem generierten Plot in die Datenbank eingefügt. Bei erfolgreicher Verarbeitung wird eine Erfolgsmeldung zurückgesendet, ansonsten eine Fehlermeldung.

Die Anwendung nutzt SocketIO, um Daten in Echtzeit an alle verbundenen Clients zu senden. Sobald ein neuer Datenpunkt eingefügt wird, werden die neuesten 40 Einträge aus der Datenbank abgerufen und an die Clients gesendet. Dies ermöglicht eine dynamische und interaktive Benutzererfahrung, da Benutzer sofortige Updates sehen können.

Die Visualisierung der Daten erfolgt durch Matplotlib, das zur Erstellung von Plots verwendet wird. Diese Plots werden als PNG-Bilder gerendert, in Bytes umgewandelt und dann in einen Base64-String kodiert, der direkt über Web-Sockets gesendet werden kann.

Ein integriertes Chat-System ermöglicht es Benutzern, Nachrichten zu senden, die das System validiert und an alle verbundenen Clients übermittelt. Dieses System umfasst Funktionen zur Überprüfung auf Profanität, HTML-Escaping zur Verhinderung von Cross-Site-Scripting-Angriffen und eine Zeichenbegrenzung, um die Datenverwaltung zu vereinfachen.

Diese Flask-Anwendung stellt ein umfassendes System dar, das nicht nur Daten effizient sammelt und speichert, sondern diese auch in Echtzeit verarbeitet und visualisiert.

Der vollständige Code

from flask import Flask, request, jsonify, send_from_directory
from flask_sqlalchemy import SQLAlchemy
from flask_socketio import SocketIO, emit
from datetime import datetime
import logging
from random import choice
from random_username.generate import generate_username
from datetime import datetime, timedelta
import threading
import time
import io
import base64
import matplotlib.pyplot as plt
import matplotlib
from PIL import Image
import time
from html import escape
from better_profanity import profanity

matplotlib.use("Agg")

# map IP addresses to their last message's timestamp
last_message_time_by_ip = {}

app = Flask(__name__, static_folder="static")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
socketio = SocketIO(
    app,
    cors_allowed_origins=["https://decision.atlane.de"],
)


# Model definition
class GammaEventEntry(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    data = db.Column(db.JSON, nullable=False)
    image_plot = db.Column(db.Text)  # Storing the image as a binary blob


class ChatMessage(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    question = db.Column(db.String, nullable=False)
    answer = db.Column(db.String, default="")
    answer_image_plot = db.Column(db.Text)
    username = db.Column(db.String, default="")
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)


# Secret token for validation, should be kept secret and complex
SECRET_TOKEN = "secret-token"


def create_and_save_plot(data_dict):
    plt.axis("off")
    plt.plot(data_dict["data"], color="black")
    buf = io.BytesIO()
    plt.savefig(buf, format="png", transparent=True)
    buf.seek(0)
    plt.close()
    image_base64 = base64.b64encode(buf.getvalue()).decode("utf-8")
    return image_base64


def check_for_answers():
    # Query for all chat messages with an empty answer
    empty_answer_messages = ChatMessage.query.filter(ChatMessage.answer == "")

    # Now check for each message if there are at least four corresponding GammaEventEntries
    # with a timestamp greater than the timestamp of the message
    for message in empty_answer_messages:
        GammaEventEntries = (
            GammaEventEntry.query.filter(GammaEventEntry.timestamp > message.timestamp)
            .limit(4)
            .all()
        )

        if len(GammaEventEntries) >= 4:
            print("The answer is ready")
            # Calculate Boolean Answer
            answer = ""
            timeValues = []
            for entry in GammaEventEntries:
                timeValues.append(entry.timestamp)

            timeDiff1 = time.mktime(timeValues[1].timetuple()) - time.mktime(
                timeValues[0].timetuple()
            )
            timeDiff2 = time.mktime(timeValues[3].timetuple()) - time.mktime(
                timeValues[2].timetuple()
            )

            timeDiffRes = timeDiff1 - timeDiff2

            if timeDiffRes < 0:
                answer = "Nope..."
            else:
                answer = "Yeah!"

            # Process images if there are at least four gamma event entries
            images = []
            for entry in GammaEventEntries:
                image_data = entry.image_plot
                if image_data:
                    # Convert base64 string to Image
                    image_bytes = base64.b64decode(image_data)
                    image = Image.open(io.BytesIO(image_bytes))
                    images.append(image)

            if images:
                # Combine images horizontally
                total_width = sum(img.width for img in images)
                max_height = max(img.height for img in images)

                # Create a new image with transparency (note the 'RGBA')
                combined_image = Image.new("RGBA", (total_width, max_height))

                x_offset = 0
                for img in images:
                    # Ensure images have an alpha layer
                    img = img.convert("RGBA")
                    combined_image.paste(img, (x_offset, 0), img)
                    x_offset += img.width

                # Convert combined image to base64
                buffered = io.BytesIO()
                combined_image.save(buffered, format="PNG")
                img_str = base64.b64encode(buffered.getvalue()).decode()

            message.answer = answer
            message.answer_image_plot = img_str
            db.session.commit()  # Commit the update to the database

            # Broadcast client message
            with app.app_context():
                socketio.emit(
                    "receive_message",
                    {
                        "id": message.id,
                        "message": message.question,
                        "username": message.username,
                        "answer": message.answer,
                        "answer_image_plot": message.answer_image_plot,
                        "timestamp": message.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
                    },
                )


@app.route("/upload", methods=["POST"])
def upload_data():
    token = request.headers.get("Authorization")

    if token != SECRET_TOKEN:
        logging.warning("Unauthorized access attempt.")
        return jsonify({"status": "error", "message": "Unauthorized"}), 401

    if request.is_json:
        data = request.get_json()
        new_entry = GammaEventEntry(data=data)
        # Create plot and store it as a binary blob
        new_entry.image_plot = create_and_save_plot(data)

        db.session.add(new_entry)
        db.session.commit()

        # Emit new data to all connected clients
        emit_data = {
            "id": new_entry.id,
            "timestamp": new_entry.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
            "data": new_entry.data,
            "image_plot": new_entry.image_plot,
        }
        socketio.emit("new_data", emit_data)

        check_for_answers()

        return jsonify({"status": "success", "message": "Data received"}), 200
    else:
        return jsonify({"status": "error", "message": "Request body must be JSON"}), 400


@app.route("/")
def index():
    return send_from_directory(app.static_folder, "index.html")


@socketio.on("connect")
def handle_connect():
    initial_data = (
        GammaEventEntry.query.order_by(GammaEventEntry.timestamp.desc()).limit(40).all()
    )
    data_list = [
        {
            "id": data.id,
            "timestamp": data.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
            "data": data.data,
            "image_plot": data.image_plot,
        }
        for data in initial_data
    ]
    emit("initial_data", data_list)


"""
    Chat System
"""

@socketio.on("send_message")
def handle_message(data):
    current_time = datetime.utcnow()
    ip_address = request.remote_addr

    # Retrieve and sanitize the message
    message_content = escape(data)  # Escapes HTML special characters  

    if len(message_content) == 0:
        emit(
            "message_error",
            {"error": "Empty message is not allowed."},
            room=request.sid,
        )
        return     

    # Character limit check
    if len(message_content) > 1000:
        emit(
            "message_error",
            {"error": "Message too long. Limit is 1000 characters."},
            room=request.sid,
        )
        return

    # Profanity and code injection checks
    if profanity.contains_profanity(message_content):
        emit(
            "message_error",
            {"error": "Message contains inappropriate content."},
            room=request.sid,
        )
        return

    # Rate limiting check
    if ip_address in last_message_time_by_ip and (
        current_time - last_message_time_by_ip[ip_address]
    ) < timedelta(seconds=30):
        emit(
            "message_error",
            {"error": "You can only send a message every 30 seconds."},
            room=request.sid,
        )
        return

    last_message_time_by_ip[ip_address] = current_time

    new_message = ChatMessage(
        question=message_content, username=generate_username(1)[0]
    )
    db.session.add(new_message)
    db.session.commit()

    # Broadcast server response
    socketio.emit(
        "message_added",
        {
            "id": new_message.id,
            "message": new_message.question,
            "username": new_message.username,
            "answer": new_message.answer,
            "answer_image_plot": new_message.answer_image_plot,
            "timestamp": new_message.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
        },
        room=request.sid,
    )


@app.route("/latest_messages", methods=["GET"])
def get_latest_messages():
    messages = ChatMessage.query.order_by(ChatMessage.timestamp.asc()).limit(100).all()
    messages = [
        {
            "id": msg.id,
            "message": msg.question,
            "username": msg.username,
            "answer": msg.answer,
            "answer_image_plot": msg.answer_image_plot,
            "timestamp": msg.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
        }
        for msg in messages
    ]
    return jsonify(messages)


@app.route("/older_messages", methods=["GET"])
def get_older_messages():
    last_id = request.args.get(
        "last_id", type=int
    )  # ID of the oldest message currently displayed
    if last_id:
        messages = (
            ChatMessage.query.filter(ChatMessage.id < last_id)
            .order_by(ChatMessage.timestamp.asc())
            .limit(20)
            .all()
        )
        messages = [
            {
                "id": msg.id,
                "message": msg.question,
                "username": msg.username,
                "answer": msg.answer,
                "answer_image_plot": msg.answer_image_plot,
                "timestamp": msg.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
            }
            for msg in reversed(messages)
        ]
        return jsonify(messages)
    return jsonify([])


def cleanup_ip_timestamps():
    while True:
        current_time = datetime.utcnow()
        # Acquire a lock to ensure thread safety
        with threading.Lock():
            keys_to_remove = [
                ip
                for ip, last_time in last_message_time_by_ip.items()
                if (current_time - last_time) > timedelta(seconds=30)
            ]
            for ip in keys_to_remove:
                del last_message_time_by_ip[ip]
        # Sleep for a specific interval before checking again
        time.sleep(10)  # Check every 10 seconds


if __name__ == "__main__":
    with app.app_context():
        db.create_all()

    cleanup_thread = threading.Thread(target=cleanup_ip_timestamps)
    cleanup_thread.daemon = (
        True  # This makes the thread exit when the main program exits
    )
    cleanup_thread.start()

    socketio.run(app)
Python

Die index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
    <title>Decision - ATLANE</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Fuzzy+Bubbles:wght@400;700&display=swap');

        HTML,
        BODY {
            background: url("/static/background.png") no-repeat center center fixed;
            background-size: cover;
            height: 100%;
            width: 100%;
            height: 100vh;
            width: 100vw;
            margin: 0;
            padding: 0;
            /* required to prevent rogue scrollbars */
            overflow: hidden;
            /* cosmetic stuff: */
            background-color: black;
        }

        .responsive-div {
            margin-left: 2.5vw;
            margin-top: 5vw;
            display: flex;
            flex-direction: column;
            height: 85vh;
            width: 90vw;
            background-color: #f8f9fa;
            opacity: 0.9;
            box-sizing: border-box;
            border-radius: 1em;
            overflow: hidden;
            /* Prevent any overflow beyond this point */
        }

        .chat-messages {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            overflow-y: auto;
            /* Allow this element to scroll */
            padding: 10px;
        }

        .message {
            background-color: #f0f0f0;
            padding: 10px 20px;
            border-radius: 20px;
            margin-bottom: 10px;
            max-width: 90%;
        }

        .sent {
            align-self: flex-start;
            background-color: #DCF8C6;
            position: relative;
        }

        .received {
            font-family: "Fuzzy Bubbles", sans-serif;
            font-weight: 700;
            font-size: 1.5em;
            align-self: flex-end;
            right: 5vw;
            background-color: #f0f0f0;
        }

        #chartsContainerAnswer * {
            max-width: 100%;
            height: auto;
        }

        #chartsContainerAnswer iframe,
        #chartsContainerAnswer embed,
        #chartsContainerAnswer object {
            max-width: 45%;
            height: auto;
        }
    </style>
</head>

<body>
    <div class="container d-flex" style="height: 100vh;">
        <div class="responsive-div">
            <div class="text-center p-3" style="border-bottom: lightgray;">
                <img style="height: 2.5vw;" src="https://atlane.de/wp-content/uploads/2024/05/atlane-logo-v3.jpg">
                </br>
                <span class="h3">Request a Decision from the Universe: </span><br>
                <span class="h5">Powered by a Gamma-Event True Random Number Generator</span>
            </div>

            <div id="messages" class="chat-messages d-flex flex-grow-1"></div>
            <div id="chartsContainer" class="d-flex flex-grow-1" style="visibility: hidden;"></div>

            <!-- Input -->
            <div class="text-center p-3">
                <input type="text" id="chatInput" class="form-control" rows="3" maxlength="1000">
                <button class="btn btn-primary mt-3" onclick="sendMessage()">Ask the univers for a "Yeah!" or
                    "Nope..."</button>
            </div>
        </div>
    </div>

    <!-- Modal -->
    <div class="modal fade" id="rateLimitModal" tabindex="-1" aria-labelledby="rateLimitModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="rateLimitModalLabel">Message Sending Error</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <div id="error-message"></div>
                </div>
                <div class="modal-footer">
                </div>
            </div>
        </div>
    </div>

    <!-- Modal -->
    <div class="modal fade" id="sendMessageModal" tabindex="-1" aria-labelledby="rateLimitModalLabel"
        aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="rateLimitModalLabel">Question added to RandomAnswer Queue</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    Your message was added to the RandomAnswer Queue. Please wait until the Univers sends an
                    answer to your question: </br>
                    <div id="messages-dialog" class="mb-3 chat-messages"></div>
                    Needed 4 Gamma-Events:
                    <div class="progress">
                        <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
                            aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div>
                    </div>

                    <div id="chartsContainerAnswer"></div>
                    <!-- Container for new charts after question -->
                </div>
                <div class="modal-footer">
                </div>
            </div>
        </div>
    </div>

    <script>
        const socket = io();


        var awaiting_answer = false;
        var awaiting_answer_gamma_events = 0;
        var awaiting_answer_message_id = 0;

        document.addEventListener('DOMContentLoaded', function () {

            socket.on('message_error', function (data) {
                // Show error modal for rate limit or other errors
                var rateLimitModalElement = document.getElementById('rateLimitModal');
                var rateLimitModalInstance = new bootstrap.Modal(rateLimitModalElement);

                console.log(data);

                const errormessagediv = document.getElementById('error-message');
                errormessagediv.textContent = data.error;

                rateLimitModalInstance.show();
            });
            socket.on('message_added', function (data) {
                awaiting_answer = true;
                awaiting_answer_gamma_events = 0;
                awaiting_answer_message_id = data.id;

                const progressBar = document.querySelector('.progress-bar');
                const maxValue = parseInt(progressBar.style.width); // Get the maximum value from the width style
                progressBar.setAttribute('aria-valuenow', 0);
                progressBar.style.width = 0 + '%';
                document.getElementById('chartsContainerAnswer').replaceChildren();
                var modalElement = document.getElementById('sendMessageModal');
                var modalInstance = new bootstrap.Modal(modalElement);

                const messagesContainerDialgo = document.getElementById('messages-dialog');

                const messageDiv = document.createElement('div');
                messageDiv.classList.add('message');
                messageDiv.classList.add('sent');
                messageDiv.textContent = data.message + " (" + data.timestamp + ") - " + data.username;
                messagesContainerDialgo.replaceChildren();
                messagesContainerDialgo.appendChild(messageDiv); // This appends the message at the end.
                modalInstance.show();
            });
        });

        function createChart(timestamp, data, plot_image, question_dialog = false) {
            const chartContainer = document.createElement('div');
            chartContainer.className = 'chart-container my-4 p-3 shadow-sm border';

            const title = document.createElement('h5');
            title.textContent = `Event: ${timestamp}`;
            chartContainer.appendChild(title);

            const image = document.createElement('img')
            image.src = `data:image/jpeg;base64,${plot_image}`;
            image.style = "background-color: transparent; max-height: 25vw"

            chartContainer.appendChild(image);

            //document.getElementById('chartsContainer').appendChild(chartContainer);
            if (question_dialog) {
                document.getElementById('chartsContainerAnswer').appendChild(chartContainer);
            }
        }

        socket.on('initial_data', function (entries) {
            entries.forEach(entry => {
                createChart(entry.timestamp, entry.data.data, entry.image_plot);
            });
        });

        socket.on('new_data', function (entry) {

            if (awaiting_answer == false) {

            } else {

                const progressBar = document.querySelector('.progress-bar');
                const maxValue = parseInt(progressBar.style.width); // Get the maximum value from the width style

                if (awaiting_answer_gamma_events < 4) {
                    createChart(entry.timestamp, entry.data.data, entry.image_plot, question_dialog = true);

                    awaiting_answer_gamma_events += 1;

                    let currentValue = parseInt(progressBar.getAttribute('aria-valuenow')); // Current value starts at 0

                    currentValue += 25; // Increment the current value
                    progressBar.setAttribute('aria-valuenow', currentValue);
                    progressBar.textContent = awaiting_answer_gamma_events + "/4";
                    progressBar.style.width = currentValue + '%';
                } else {
                    awaiting_answer = false;
                }

            }

        });

        function sendMessage() {
            const message = document.getElementById('chatInput').value;
            socket.emit('send_message', message);
            document.getElementById('chatInput').value = '';
        }

        function loadInitialMessages() {
            $.get('/latest_messages', function (messages) {
                messages.forEach(msg => displayMessage(msg));
            });
        }

        loadInitialMessages();

        function displayMessage(msg) {
            console.log("Display Message called");
            console.log(msg);
            const messagesContainer = document.getElementById('messages');

            const messageDiv = document.createElement('div');
            messageDiv.classList.add('message');
            messageDiv.classList.add('sent');

            messageDiv.innerHTML = msg.message + " </br><em><small style='color:gray,'> (" + msg.timestamp + ") - " + msg.username + "</small></em>";

            const answerDiv = document.createElement('div');
            answerDiv.classList.add('message');
            answerDiv.classList.add('received');
            answerDiv.textContent = msg.answer;

            const image = document.createElement('img')
            image.src = `data:image/jpeg;base64,${msg.answer_image_plot}`;
            image.style = "background-color: transparent; max-width: 10vw; padding-top:10px"
            answerDiv.appendChild(document.createElement('br'));
            answerDiv.appendChild(image);

            messagesContainer.appendChild(messageDiv); // This appends the message at the end.
            messagesContainer.appendChild(answerDiv); // This appends the message at the end.

            messagesContainer.scrollTop = messagesContainer.scrollHeight + 300;

            let answerDiv2 = answerDiv;
            let messageDiv2 = messageDiv;

            if (msg.id == awaiting_answer_message_id) {
                const messagesContainerDialog = document.getElementById('messages-dialog');
                messagesContainerDialog.replaceChildren();

                const messageDiv2 = document.createElement('div');
                messageDiv2.classList.add('message');
                messageDiv2.classList.add('sent');

                messageDiv2.innerHTML = msg.message + " </br><em><small style='color:gray,'> (" + msg.timestamp + ") - " + msg.username + "</small></em>";

                const answerDiv2 = document.createElement('div');
                answerDiv2.classList.add('message');
                answerDiv2.classList.add('received');
                answerDiv2.textContent = msg.answer;

                const image = document.createElement('img')
                image.src = `data:image/jpeg;base64,${msg.answer_image_plot}`;
                image.style = "background-color: transparent; max-width: 10vw; padding-top:10px"
                answerDiv2.appendChild(document.createElement('br'));
                answerDiv2.appendChild(image);

                messagesContainerDialog.appendChild(messageDiv2);
                messagesContainerDialog.appendChild(answerDiv2);
            }
        }


        socket.on('receive_message', function (msg) {
            displayMessage(msg);
        });

        let lastLoadedMessageId;

        window.addEventListener('scroll', function () {
            if (window.scrollY === 0) {  // User has scrolled to the top of the window
                loadOlderMessages();
            }
        });

        function loadOlderMessages() {
            if (lastLoadedMessageId) {
                $.get(`/older_messages?last_id=${lastLoadedMessageId}`, function (messages) {
                    if (messages.length > 0) {
                        messages.forEach(msg => {
                            const messageDiv = document.createElement('div');
                            messageDiv.textContent = msg.message + " (" + msg.timestamp + ") - " + msg.username;
                            const messagesContainer = document.getElementById('messages');
                            messagesContainer.insertBefore(messageDiv, messagesContainer.firstChild);
                            lastLoadedMessageId = messages[messages.length - 1].id;
                        });
                    }
                });
            }
        }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>

</html>
HTML

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.