RedMist
Search Results for

    Show / Hide Table of Contents

    SignalR Real-Time Communication

    RedMist uses SignalR for real-time, bidirectional communication between servers and clients.

    SignalR is an open-source library that simplifies adding real-time web functionality to applications. It enables server-side code to push content to connected clients instantly as events happen, rather than having clients poll the server for updates.

    Key Benefits

    Real-Time Updates

    • Instant data delivery with sub-second latency
    • Live timing data, positions, and lap times update in real-time
    • No polling overhead or delays

    Automatic Transport Selection

    • WebSockets (preferred for best performance)
    • Server-Sent Events (SSE)
    • Long Polling (fallback for older browsers)
    • Automatically negotiates the best available transport

    Built-In Reconnection

    • Automatic reconnection with exponential backoff
    • Seamless recovery from network interruptions
    • State preservation across reconnections

    Scalability

    • Redis backplane for multi-server deployments
    • Horizontal scaling support
    • Connection state management

    Cross-Platform Support

    • JavaScript/TypeScript clients
    • .NET clients (C#, F#)
    • Python clients
    • Java clients
    • Native mobile apps (iOS, Android)

    Bi-Directional Communication

    • Server-to-client push notifications
    • Client-to-server method invocation
    • Strongly-typed hubs for type safety

    Hub Overview

    StatusHub

    URL: wss://api.redmist.racing/status/event-status
    Authentication: Required (Bearer token)

    The StatusHub provides real-time event updates, timing data, and race information.

    Connection Setup

    JavaScript/TypeScript

    import * as signalR from '@microsoft/signalr';
    
    // Create connection
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("https://api.redmist.racing/status/event-status", {
            accessTokenFactory: () => getAccessToken() // Your token function
        })
        .withAutomaticReconnect({
            nextRetryDelayInMilliseconds: retryContext => {
                // Exponential backoff: 0, 2, 10, 30 seconds, then 30 seconds
                if (retryContext.previousRetryCount === 0) return 0;
                if (retryContext.previousRetryCount === 1) return 2000;
                if (retryContext.previousRetryCount === 2) return 10000;
                return 30000;
            }
        })
        .configureLogging(signalR.LogLevel.Information)
        .build();
    
    // Handle reconnection
    connection.onreconnecting(error => {
        console.log('Connection lost. Reconnecting...', error);
    });
    
    connection.onreconnected(connectionId => {
        console.log('Reconnected with connection ID:', connectionId);
        // Re-subscribe to events
        resubscribe();
    });
    
    connection.onclose(error => {
        console.log('Connection closed.', error);
    });
    
    // Start connection
    try {
        await connection.start();
        console.log('Connected to StatusHub');
    } catch (err) {
        console.error('Connection failed:', err);
    }
    

    Python

    from signalrcore.hub_connection_builder import HubConnectionBuilder
    import logging
    
    # Enable logging
    logging.basicConfig(level=logging.INFO)
    
    # Create connection
    hub = HubConnectionBuilder()\
        .with_url(
            "https://api.redmist.racing/status/event-status",
            options={
                "access_token_factory": lambda: get_access_token(),
                "headers": {
                    "User-Agent": "RedMist-Python-Client/1.0"
                }
            }
        )\
        .configure_logging(logging.INFO)\
        .with_automatic_reconnect({
            "type": "interval",
            "keep_alive_interval": 10,
            "intervals": [0, 2, 10, 30]
        })\
        .build()
    
    # Start connection
    hub.start()
    

    C#

    using Microsoft.AspNetCore.SignalR.Client;
    
    var connection = new HubConnectionBuilder()
        .WithUrl("https://api.redmist.racing/status/event-status", options =>
        {
            options.AccessTokenProvider = async () => await GetAccessTokenAsync();
        })
        .WithAutomaticReconnect(new[] {
            TimeSpan.Zero,
            TimeSpan.FromSeconds(2),
            TimeSpan.FromSeconds(10),
            TimeSpan.FromSeconds(30)
        })
        .Build();
    
    connection.Reconnecting += error =>
    {
        Console.WriteLine($"Connection lost. Reconnecting... {error}");
        return Task.CompletedTask;
    };
    
    connection.Reconnected += connectionId =>
    {
        Console.WriteLine($"Reconnected: {connectionId}");
        return ResubscribeAsync();
    };
    
    await connection.StartAsync();
    

    Hub Methods

    Event Subscriptions

    SubscribeToEventV2 (V2)

    Enhanced subscription with improved data structures.

    await connection.invoke("SubscribeToEventV2", eventId);
    

    Features:

    • Optimized data format
    • Better compression
    • Improved update frequency

    UnsubscribeFromEvent / UnsubscribeFromEventV2

    Stop receiving updates for an event.

    await connection.invoke("UnsubscribeFromEventV2", eventId);
    

    Control Log Subscriptions

    SubscribeToControlLogs

    Receive control log updates for an event.

    await connection.invoke("SubscribeToControlLogs", eventId);
    

    SubscribeToCarControlLogs

    Receive control log updates for a specific car.

    await connection.invoke("SubscribeToCarControlLogs", eventId, carNumber);
    

    Use Case: Drivers/teams who only want their car's penalties.

    // Example: Subscribe to car #42's control logs
    await connection.invoke("SubscribeToCarControlLogs", 123, "42");
    

    In-Car Driver Mode

    SubscribeToInCarDriverEvent (V1)

    Subscribe to in-car driver display data.

    await connection.invoke("SubscribeToInCarDriverEvent", eventId, carNumber);
    

    SubscribeToInCarDriverEventV2 (V2)

    Enhanced in-car data with better update frequency.

    await connection.invoke("SubscribeToInCarDriverEventV2", eventId, carNumber);
    

    Data Included:

    • Current position
    • Gap to car ahead
    • Gap to car behind
    • Best lap comparison
    • Current lap time
    • Flag status

    Receiving Messages

    ReceiveMessage Event

    All updates are sent via the ReceiveMessage event.

    connection.on("ReceiveMessage", (message) => {
        // Check if message is gzipped
        if (message.startsWith('H4sI')) {
            // Decompress gzip data
            const decompressed = pako.inflate(
                atob(message), 
                { to: 'string' }
            );
            const status = JSON.parse(decompressed);
            handleStatus(status);
        } else {
            // Parse JSON directly
            const status = JSON.parse(message);
            handleStatus(status);
        }
    });
    
    function handleStatus(status) {
        console.log('Event Status:', status);
        
        // Update UI with car positions
        updateCarPositions(status.cps);
        
        // Update flags
        updateFlags(status.fd);
        
        // Update event info
        updateEventInfo(status.es);
    }
    

    Complete Example

    Real-Time Dashboard

    import * as signalR from '@microsoft/signalr';
    import pako from 'pako';
    
    class RedMistDashboard {
        constructor(eventId) {
            this.eventId = eventId;
            this.connection = null;
            this.currentStatus = null;
        }
    
        async connect() {
            this.connection = new signalR.HubConnectionBuilder()
                .withUrl("https://api.redmist.racing/status/event-status", {
                    accessTokenFactory: () => this.getToken()
                })
                .withAutomaticReconnect()
                .build();
    
            // Handle messages
            this.connection.on("ReceiveMessage", (message) => {
                const status = this.decompressMessage(message);
                this.updateStatus(status);
            });
    
            // Handle reconnection
            this.connection.onreconnected(async () => {
                await this.subscribe();
            });
    
            // Start connection
            await this.connection.start();
            await this.subscribe();
        }
    
        async subscribe() {
            await this.connection.invoke("SubscribeToEventV2", this.eventId);
            console.log(`Subscribed to event ${this.eventId}`);
        }
    
        async unsubscribe() {
            await this.connection.invoke("UnsubscribeFromEventV2", this.eventId);
            console.log(`Unsubscribed from event ${this.eventId}`);
        }
    
        decompressMessage(message) {
            if (message.startsWith('H4sI')) {
                const compressed = atob(message);
                const decompressed = pako.inflate(compressed, { to: 'string' });
                return JSON.parse(decompressed);
            }
            return JSON.parse(message);
        }
    
        updateStatus(status) {
            if (status.t === 'patch') {
                // Apply JSON patch
                this.applyPatches(status.patches);
            } else {
                // Full update
                this.currentStatus = status;
            }
    
            this.render();
        }
    
        applyPatches(patches) {
            // Apply JSON patches to current status
            patches.forEach(patch => {
                const path = patch.path.split('/').slice(1);
                let obj = this.currentStatus;
                
                for (let i = 0; i < path.length - 1; i++) {
                    obj = obj[path[i]];
                }
                
                if (patch.op === 'replace') {
                    obj[path[path.length - 1]] = patch.value;
                }
            });
        }
    
        render() {
            // Update UI with current status
            document.getElementById('event-name').textContent = 
                this.currentStatus.n;
            
            // Update car positions
            const tbody = document.getElementById('positions-tbody');
            tbody.innerHTML = '';
            
            this.currentStatus.cps?.forEach(car => {
                const row = tbody.insertRow();
                row.innerHTML = `
                    <td>${car.ovp}</td>
                    <td>${car.n}</td>
                    <td>${car.bt}</td>
                    <td>${car.og}</td>
                `;
            });
        }
    
        async getToken() {
            // Your token retrieval logic
            return localStorage.getItem('access_token');
        }
    
        async disconnect() {
            await this.unsubscribe();
            await this.connection.stop();
        }
    }
    
    // Usage
    const dashboard = new RedMistDashboard(123);
    dashboard.connect();
    
    // Clean up on page unload
    window.addEventListener('beforeunload', async () => {
        await dashboard.disconnect();
    });
    

    Best Practices

    1. Always Handle Reconnection

    connection.onreconnected(async () => {
        // Re-subscribe to all events using V2
        for (const eventId of subscribedEvents) {
            await connection.invoke("SubscribeToEventV2", eventId);
        }
    });
    

    2. Implement Exponential Backoff

    .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: retryContext => {
            return Math.min(
                1000 * Math.pow(2, retryContext.previousRetryCount),
                30000
            );
        }
    })
    

    3. Handle Token Refresh

    let tokenExpiry = Date.now() + 300000; // 5 minutes
    
    connection.onclose(async () => {
        if (Date.now() > tokenExpiry) {
            // Token expired, get new one
            await refreshToken();
        }
        await connection.start();
    });
    

    4. Unsubscribe When Done

    window.addEventListener('beforeunload', async () => {
        await connection.invoke("UnsubscribeFromEventV2", eventId);
        await connection.stop();
    });
    

    5. Handle Errors Gracefully

    connection.on("ReceiveMessage", (message) => {
        try {
            const status = decompressMessage(message);
            updateUI(status);
        } catch (error) {
            console.error('Failed to process message:', error);
            // Don't crash, log and continue
        }
    });
    

    Troubleshooting

    Connection Fails

    • Check token is valid and not expired
    • Verify URL is correct
    • Ensure HTTPS is used
    • Check CORS settings

    No Messages Received

    • Verify subscription was successful
    • Check token has required permissions
    • Ensure event is live
    • Check browser console for errors

    High Latency

    • Check network connection
    • Verify server load
    • Consider using V2 endpoints
    • Check compression is working

    Related Documentation

    • Getting Started
    • Authentication
    • Data Models
    • Code Examples
    • Edit this page
    In this article
    Back to top © 2025 Big Mission Motorsports, LLC. Red Mist Timing & Scoring