Wednesday, December 31, 2025

A Happy New Year 2026



Dear readers,


Thanks for reading my Blog so frequently in 2025. As a present, I‘ll provide you a HTML file created by Gemini 2.5 Flash.

Please, save and open the following HTML-page. It is safe to do so!



<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Happy New Year! - Dynamic Scenery</title>

    <style>

        body {

            margin: 0;

            overflow: hidden; /* Hide scrollbars */

            background: linear-gradient(to bottom, #0a0a2a 0%, #2c0a4e 100%); /* Dark night sky */

            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

            display: flex;

            justify-content: center;

            align-items: center;

            min-height: 100vh;

            position: relative;

            color: #fff; /* Default text color */

        }


        /* Canvas for fireworks */

        #fireworksCanvas {

            position: absolute;

            top: 0;

            left: 0;

            width: 100%;

            height: 100%;

            z-index: 1; /* Above city, below text */

        }


        /* "Happy New Year!" Text */

        #new-year-text {

            position: absolute;

            top: 20%; /* Adjust vertical position */

            left: 50%;

            transform: translate(-50%, -50%);

            font-size: 5em; /* Large font size */

            font-weight: bold;

            text-shadow: 0 0 15px rgba(255, 255, 255, 0.8), 0 0 30px rgba(255, 255, 255, 0.6), 0 0 45px rgba(255, 255, 255, 0.4);

            letter-spacing: 5px;

            z-index: 2; /* On top of everything */

            animation: pulseText 3s infinite alternate; /* Subtle breathing effect */

            white-space: nowrap; /* Prevent text wrapping */

        }


        #new-year-text span {

            display: block; /* Ensures text shadow applies properly */

            background: linear-gradient(45deg, #ffd700, #ff8c00, #ff4500); /* Gold/Orange gradient */

            -webkit-background-clip: text;

            -webkit-text-fill-color: transparent;

            background-clip: text; /* Standard property */

            color: transparent; /* Fallback for browsers without -webkit-text-fill-color */

        }


        @keyframes pulseText {

            0% {

                transform: translate(-50%, -50%) scale(1);

                opacity: 0.9;

            }

            100% {

                transform: translate(-50%, -50%) scale(1.02);

                opacity: 1;

            }

        }


        /* City Silhouette at the bottom */

        #city-silhouette {

            position: absolute;

            bottom: 0;

            left: 0;

            width: 100%;

            height: 150px; /* Height of the silhouette */

            background: #000; /* Solid black silhouette */

            z-index: 0; /* Behind fireworks */

            /* Using a simple SVG mask for a jagged city skyline effect */

            mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="black" d="M0 100 L0 50 L10 50 L10 70 L20 70 L20 40 L30 40 L30 80 L40 80 L40 60 L50 60 L50 90 L60 90 L60 70 L70 70 L70 50 L80 50 L80 80 L90 80 L90 60 L100 60 L100 100 Z"/></svg>');

            mask-size: 100% 100%;

            mask-repeat: no-repeat;

            -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="black" d="M0 100 L0 50 L10 50 L10 70 L20 70 L20 40 L30 40 L30 80 L40 80 L40 60 L50 60 L50 90 L60 90 L60 70 L70 70 L70 50 L80 50 L80 80 L90 80 L90 60 L100 60 L100 100 Z"/></svg>');

            -webkit-mask-size: 100% 100%;

            -webkit-mask-repeat: no-repeat;

        }


        /* Twinkling Stars (added via JavaScript) */

        .star {

            position: absolute;

            background-color: white;

            border-radius: 50%;

            animation: twinkle 2s infinite alternate;

            opacity: 0; /* Start invisible */

            z-index: 0; /* Behind fireworks */

        }


        @keyframes twinkle {

            0% { opacity: 0; transform: scale(0.5); }

            50% { opacity: 1; transform: scale(1); }

            100% { opacity: 0; transform: scale(0.5); }

        }

    </style>

</head>

<body>

    <div id="new-year-text">

        <span>Happy New Year!</span>

    </div>

    <canvas id="fireworksCanvas"></canvas>

    <div id="city-silhouette"></div>


    <script>

        const canvas = document.getElementById('fireworksCanvas');

        const ctx = canvas.getContext('2d');


        let fireworks = [];

        let particles = [];


        // Set canvas dimensions to full window size

        function setCanvasSize() {

            canvas.width = window.innerWidth;

            canvas.height = window.innerHeight;

        }

        setCanvasSize();

        window.addEventListener('resize', setCanvasSize);


        // Utility function to get a random vibrant color

        function getRandomColor() {

            const hue = Math.floor(Math.random() * 360);

            return `hsl(${hue}, 100%, 70%)`; // High saturation, medium lightness for vibrancy

        }


        // Particle class for firework trails and explosion fragments

        class Particle {

            constructor(x, y, vx, vy, color, alphaDecay, size) {

                this.x = x;

                this.y = y;

                this.vx = vx;

                this.vy = vy;

                this.color = color;

                this.alpha = 1;

                this.alphaDecay = alphaDecay; // How fast the particle fades

                this.size = size;

                this.gravity = 0.05; // Affects how particles fall

                this.friction = 0.99; // Slows down particles over time

            }


            update() {

                this.vx *= this.friction;

                this.vy *= this.friction;

                this.vy += this.gravity; // Apply gravity

                this.x += this.vx;

                this.y += this.vy;

                this.alpha -= this.alphaDecay; // Fade out

            }


            draw() {

                ctx.save();

                ctx.globalAlpha = Math.max(0, this.alpha); // Ensure alpha doesn't go below 0

                ctx.beginPath();

                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);

                ctx.fillStyle = this.color;

                ctx.shadowBlur = this.size * 2; // Create a glow effect

                ctx.shadowColor = this.color;

                ctx.fill();

                ctx.restore();

            }

        }


        // Firework class for the rocket and its explosion logic

        class Firework {

            constructor(startX, startY, targetX, targetY) {

                this.x = startX;

                this.y = startY;

                this.targetX = targetX;

                this.targetY = targetY;


                // Calculate velocity to reach target

                const angle = Math.atan2(targetY - startY, targetX - startX);

                this.speed = 5; // Initial rocket speed

                this.vx = Math.cos(angle) * this.speed;

                this.vy = Math.sin(angle) * this.speed;


                this.color = getRandomColor();

                this.trail = []; // Particles forming the rocket's trail

                this.exploded = false;

            }


            update() {

                if (!this.exploded) {

                    // Check if firework has reached its target

                    const distanceToTarget = Math.sqrt((this.x - this.targetX)**2 + (this.y - this.targetY)**2);

                    if (distanceToTarget <= this.speed) {

                        this.exploded = true;

                        this.explode(); // Trigger explosion

                    } else {

                        this.x += this.vx;

                        this.y += this.vy;

                        // Add particles to the rocket's trail

                        this.trail.push(new Particle(this.x, this.y, 0, 0, this.color, 0.02, 1.5));

                        if (this.trail.length > 20) { // Limit trail length for performance

                            this.trail.shift();

                        }

                    }

                }

                // Update and remove faded trail particles

                for (let i = this.trail.length - 1; i >= 0; i--) {

                    this.trail[i].update();

                    if (this.trail[i].alpha <= 0.05) {

                        this.trail.splice(i, 1);

                    }

                }

            }


            draw() {

                if (!this.exploded) {

                    // Draw the rocket itself

                    ctx.save();

                    ctx.beginPath();

                    ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);

                    ctx.fillStyle = this.color;

                    ctx.shadowBlur = 10;

                    ctx.shadowColor = this.color;

                    ctx.fill();

                    ctx.restore();

                }

                // Draw the rocket's trail

                this.trail.forEach(p => p.draw());

            }


            explode() {

                const numParticles = 80 + Math.random() * 50; // Random number of explosion particles

                for (let i = 0; i < numParticles; i++) {

                    const angle = Math.random() * Math.PI * 2; // Random direction

                    const speed = Math.random() * 8 + 2; // Random speed

                    const vx = Math.cos(angle) * speed;

                    const vy = Math.sin(angle) * speed;

                    const size = Math.random() * 3 + 1; // Random size

                    particles.push(new Particle(this.x, this.y, vx, vy, this.color, 0.02, size));

                }

            }

        }


        // Main animation loop

        function animate() {

            requestAnimationFrame(animate); // Call animate again on next frame


            // Clear canvas with a fading effect to create trails

            ctx.globalCompositeOperation = 'destination-out';

            ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; // Fades out previous frames

            ctx.fillRect(0, 0, canvas.width, canvas.height);

            ctx.globalCompositeOperation = 'lighter'; // Blend mode for glowing particles


            // Update and draw fireworks

            for (let i = fireworks.length - 1; i >= 0; i--) {

                fireworks[i].update();

                fireworks[i].draw();


                // Remove firework if it exploded and its trail has faded

                if (fireworks[i].exploded && fireworks[i].trail.length === 0) {

                    fireworks.splice(i, 1);

                }

            }


            // Update and draw explosion particles

            for (let i = particles.length - 1; i >= 0; i--) {

                particles[i].update();

                particles[i].draw();


                // Remove particle if it has faded out

                if (particles[i].alpha <= 0.05) {

                    particles.splice(i, 1);

                }

            }

        }


        // Function to launch a new firework from the bottom center

        function launchFirework() {

            const startX = canvas.width / 2;

            const startY = canvas.height; // Start from bottom

            const targetX = Math.random() * canvas.width * 0.8 + canvas.width * 0.1; // Random target X (within 10%-90% of width)

            const targetY = Math.random() * canvas.height * 0.5 + canvas.height * 0.1; // Random target Y (within 10%-60% of height)

            fireworks.push(new Firework(startX, startY, targetX, targetY));

        }


        // Launch fireworks periodically

        setInterval(launchFirework, 1000); // Launch a new firework every 1 second


        // Initial launch to start the show

        launchFirework();


        // Start the animation loop

        animate();


        // Optional: Add twinkling stars to the background using JS

        function createStars(numStars) {

            const starsContainer = document.body;

            for (let i = 0; i < numStars; i++) {

                const star = document.createElement('div');

                star.classList.add('star');

                star.style.width = star.style.height = `${Math.random() * 2 + 1}px`; // Random size 1-3px

                star.style.top = `${Math.random() * 100}%`;

                star.style.left = `${Math.random() * 100}%`;

                star.style.animationDelay = `${Math.random() * 2}s`; // Stagger animation start

                star.style.animationDuration = `${Math.random() * 1.5 + 1.5}s`; // Random animation duration

                starsContainer.appendChild(star);

            }

        }

        createStars(100); // Create 100 twinkling stars

    </script>

</body>

</html>


Tuesday, December 30, 2025

Building a Complete DOS Emulator in Go

 




The code of the dos-simulator can be found here:

https://github.com/ms1963/dos-emulator


Introduction and Rationale

When I started my university studies, the most used operating system had been Microsoft‘s MS-DOS - though some had serious doubts whether DOS (Disk Operating System) really was an operating system or not. In that era memory capacity was calculated in Kilobytes instead of Gigabytes. It was hard to live and work with these constraints, but somehow we managed to write ressource-efficient programs. As a homage to these ancient times I wrote a DOS emulator.

The development of a DOS emulator serves multiple important purposes in modern computing. First and foremost, it provides a means to preserve and run legacy software that was developed during the DOS era, which spans from the early 1980s through the mid-1990s. Many businesses, educational institutions, and individuals still possess valuable software written for DOS that cannot run on modern operating systems. By creating a DOS emulator, we enable these programs to execute on contemporary hardware without requiring vintage computers or complex virtualization setups. Additionally, building a DOS emulator serves as an excellent educational project for understanding low-level computer architecture, operating system concepts, and the x86 instruction set. It provides hands-on experience with CPU emulation, memory management, interrupt handling, and file system operations. Furthermore, a DOS emulator written in a modern language like Go offers portability across different platforms, allowing DOS programs to run on Windows, Linux, macOS, and other operating systems without modification. The project also demonstrates how modern programming languages can be used to implement complex system-level software while maintaining readability and maintainability.


Understanding the DOS Environment

Before diving into implementation details, it is essential to understand what DOS actually is and how it operates. DOS, which stands for Disk Operating System, is a single-tasking operating system that provides a command-line interface and basic services for running programs. The most common version was MS-DOS, developed by Microsoft, though compatible versions like PC-DOS and FreeDOS also existed. DOS programs run in real mode on x86 processors, which means they have direct access to memory and hardware without the protection mechanisms found in modern operating systems. The DOS environment consists of several key components that our emulator must replicate. These include the BIOS, which provides basic input and output services through software interrupts, the DOS kernel itself, which offers file management and program execution services, and the command interpreter, typically COMMAND.COM, which provides the user interface. Programs interact with DOS and the BIOS primarily through software interrupts, which are essentially function calls to operating system services. Understanding this interrupt-based architecture is crucial for building an accurate emulator.


Core Components Overview


A complete DOS emulator requires several major components working together in harmony. The first and most fundamental component is the CPU emulator, which must accurately simulate the behavior of an Intel 8086 or compatible processor. This includes implementing all registers, flags, and the instruction set. The second major component is the memory subsystem, which provides the one megabyte address space that DOS programs expect, organized into segments and offsets. The third component is the BIOS emulation layer, which handles hardware-related interrupts for video output, keyboard input, disk operations, and system services. The fourth component is the DOS kernel emulation, which implements file system operations, program loading, and process management through DOS interrupts. The fifth component is the file system interface, which bridges between DOS file operations and the host operating system's file system. Finally, we need a user interface component that provides either a command-line shell or a way to directly execute DOS programs. Each of these components must be carefully designed and implemented to create a functional emulator.


CPU Emulation Architecture

The CPU emulator forms the heart of our DOS emulator and requires meticulous attention to detail. The Intel 8086 processor, which we are emulating, is a sixteen-bit processor with a segmented memory architecture. We must implement all of the processor's registers, which include four general-purpose registers that can be accessed as sixteen-bit values or split into eight-bit high and low bytes. These registers are AX, BX, CX, and DX, where AX serves as the accumulator, BX as the base register, CX as the counter, and DX as the data register. Each of these can be accessed as AL and AH for AX, BL and BH for BX, CL and CH for CX, and DL and DH for DX. Additionally, we need four index and pointer registers: SI for source index, DI for destination index, BP for base pointer, and SP for stack pointer. The processor also has four segment registers: CS for code segment, DS for data segment, ES for extra segment, and SS for stack segment. The instruction pointer IP tracks the current position in the code being executed. Finally, we must implement the FLAGS register, which contains individual bits representing the state of various conditions such as carry, zero, sign, overflow, parity, auxiliary carry, direction, interrupt enable, and trap flags.


Implementing the Instruction Set

The instruction set implementation is perhaps the most labor-intensive part of building a CPU emulator. The 8086 processor supports over one hundred fifty different instructions, each of which must be accurately implemented. These instructions fall into several categories. Data movement instructions include MOV for moving data between registers and memory, PUSH and POP for stack operations, XCHG for exchanging values, and LEA for loading effective addresses. Arithmetic instructions include ADD, SUB, MUL, DIV, INC, and DEC for basic mathematical operations, as well as ADC and SBB for operations with carry. Logical instructions include AND, OR, XOR, NOT, and TEST for bitwise operations. Shift and rotate instructions include SHL, SHR, SAL, SAR, ROL, ROR, RCL, and RCR for bit manipulation. Control flow instructions include JMP for unconditional jumps, various conditional jumps like JZ, JNZ, JE, JNE, JG, JL, and others, as well as CALL and RET for subroutine operations and LOOP instructions for iteration. String instructions include MOVSB, MOVSW, STOSB, STOSW, LODSB, LODSW, CMPSB, CMPSW, and SCASB for efficient string and memory block operations. Finally, we have interrupt instructions like INT, IRET, and special instructions like HLT, NOP, and flag manipulation instructions. Each instruction must be decoded from its binary representation and executed with proper flag updates.


Memory Management Implementation

The memory subsystem must provide the segmented memory model that DOS programs expect. In the 8086 architecture, memory addresses are calculated using a segment and offset pair. The physical address is computed by shifting the segment value left by four bits and adding the offset, which allows addressing up to one megabyte of memory despite using sixteen-bit registers. Our emulator allocates a byte array of one megabyte to represent the entire addressable memory space. We implement functions to read and write both bytes and words from memory, handling the little-endian byte ordering that x86 processors use. The memory subsystem must also support the Program Segment Prefix, or PSP, which is a 256-byte data structure that DOS creates at the beginning of each program's memory space. The PSP contains important information such as the command line arguments, file handles, and pointers to the environment. When loading a program, we must set up the PSP correctly and initialize the segment registers to point to appropriate locations. The stack grows downward from high memory, and we must ensure that stack operations correctly update the stack pointer and handle stack overflow conditions gracefully.


BIOS Interrupt Implementation

BIOS interrupts provide low-level hardware services that DOS programs rely upon. The most important BIOS interrupt for our purposes is INT 10h, which handles video services. This interrupt supports numerous functions identified by the value in the AH register. Function 0Eh provides teletype output, which writes a character to the screen and advances the cursor. This is the most commonly used function for simple text output. Function 00h sets the video mode, function 02h sets the cursor position, function 03h gets the cursor position, function 06h scrolls the screen up, function 09h writes a character with attributes multiple times, and function 0Fh gets the current video mode. Our implementation maintains a virtual video buffer and cursor position, updating them as programs call these functions and outputting the results to the host system's console. INT 16h provides keyboard services, with function 00h waiting for a keypress and returning the character, function 01h checking if a key is available without waiting, and function 02h getting the keyboard shift status. INT 13h handles disk services, though for our emulator we can provide minimal implementations since DOS handles most file operations. INT 1Ah provides time and date services, returning the system time in various formats. We implement function 00h to get the tick count since midnight, function 02h to get the real-time clock time, and function 04h to get the date.


DOS Interrupt Implementation

DOS services are accessed through INT 21h, which is the most complex interrupt to implement as it provides dozens of functions. The function number is specified in the AH register, and parameters are passed in other registers. Function 01h reads a character from standard input with echo, function 02h writes a character to standard output from the DL register, function 06h provides direct console I/O with the ability to check for input without waiting, function 07h and 08h read characters without echo, function 09h writes a string terminated by a dollar sign to standard output, and function 0Ah reads a buffered line of input. Function 0Eh selects the current drive, and function 19h gets the current drive. Function 25h sets an interrupt vector, and function 35h gets an interrupt vector. Function 2Ah gets the system date, function 2Ch gets the system time, and function 30h gets the DOS version number. File operations include function 3Ch to create a file, function 3Dh to open a file, function 3Eh to close a file, function 3Fh to read from a file, function 40h to write to a file, function 41h to delete a file, function 42h to seek within a file, function 43h to get or set file attributes, function 47h to get the current directory, function 4Eh to find the first matching file, function 4Fh to find the next matching file, and function 56h to rename a file. Directory operations include function 39h to create a directory, function 3Ah to remove a directory, and function 3Bh to change the current directory. Function 4Ch terminates a program with a return code, and functions 51h and 62h get the Program Segment Prefix address. Each of these functions must be carefully implemented to match DOS behavior while interfacing with the host operating system's file system and console.


Program Loading Mechanisms

Loading DOS programs requires understanding two different executable formats: COM files and EXE files. COM files are the simpler format, consisting of raw machine code with no header or relocation information. A COM file is loaded at offset 0100h within its segment, immediately following the PSP. All segment registers are set to point to the same segment, creating a unified 64KB address space. The instruction pointer is set to 0100h, and the stack pointer is set to FFFEh at the top of the segment. COM files are limited to approximately 64KB in size due to this single-segment architecture. EXE files are more complex and begin with a header containing metadata about the program. The header includes a signature of 4D5Ah or 5A4Dh, the number of bytes in the last page, the total number of 512-byte pages, the number of relocation entries, the size of the header in paragraphs, minimum and maximum memory allocation requirements, initial stack segment and pointer values, a checksum, initial instruction pointer and code segment values, the offset to the relocation table, and an overlay number. When loading an EXE file, we must read this header, calculate the actual program size, load the program data into memory at an appropriate segment, process any relocation entries by adjusting segment references to account for where the program was actually loaded, and set the segment registers and instruction pointer according to the header values. The relocation table contains pairs of segment and offset values pointing to locations in the code that contain segment references that must be adjusted.


File System Integration

Integrating with the host operating system's file system presents interesting challenges because DOS file operations must be translated to modern file system calls. DOS uses a handle-based approach where files are opened and assigned a numeric handle, which is then used for all subsequent operations on that file. We maintain a map of DOS file handles to host operating system file objects. Handles zero, one, and two are reserved for standard input, standard output, and standard error respectively. When a DOS program opens a file, we translate the DOS filename to a host path, open the file using the host operating system's file API, assign it a handle number, and return that handle to the program. Read and write operations translate the DOS handle to the host file object and perform the operation. Seeking within files requires converting between DOS seek modes and host seek modes. Directory operations must translate between DOS directory structures and host directory structures. The Directory Transfer Area, or DTA, is a memory structure that DOS uses to return file information during directory searches. We must populate this structure with file attributes, modification time and date, file size, and filename when programs search for files. DOS filenames follow the eight dot three format, so we may need to truncate or convert long filenames from the host system.


Building the Interactive Shell

The interactive shell provides a user-friendly interface for running DOS programs and managing files. Our shell implements a command prompt that displays the current drive and directory, reads user input, parses commands, and executes them. Built-in commands include DIR to list directory contents in a DOS-style format showing filename, size, date, and time, CD to change the current directory, MD and MKDIR to create directories, RD and RMDIR to remove directories, DEL and ERASE to delete files, TYPE to display file contents, COPY to copy files, REN and RENAME to rename files, ECHO to display messages, DATE to show the current date, TIME to show the current time, MEM to display memory information, CLS to clear the screen, VER to show the emulator version, and EXIT or QUIT to terminate the emulator. Additionally, we implement emulator-specific commands such as DEBUG to toggle debug mode which shows detailed execution information, STEP to enable single-step execution, TRACE to show each instruction as it executes, REGS to display CPU register contents, DUMP to show memory contents in hexadecimal, STACK to display stack contents, STATS to show execution statistics like instruction count and execution time, and DISASM to disassemble memory contents into assembly language. When a user types a filename with a COM or EXE extension, the shell attempts to load and execute that program. The shell must handle errors gracefully and provide helpful error messages when files are not found or operations fail.


Debugging and Testing Infrastructure

Building a robust DOS emulator requires extensive testing and debugging capabilities. We implement several debugging features to help diagnose problems. Debug mode prints detailed information about each instruction being executed, including the address, instruction name, and register values. Step mode pauses execution after each instruction and waits for user input, allowing careful examination of program behavior. Trace mode logs execution flow without pausing, useful for understanding program behavior over longer sequences. The register display shows all CPU registers and flags in a formatted manner, making it easy to see the current processor state. Memory dump functionality displays memory contents in both hexadecimal and ASCII, allowing inspection of data structures and code. Stack display shows the most recent values pushed onto the stack, helpful for debugging function calls and returns. The disassembler converts machine code back into assembly language mnemonics, making it easier to understand what a program is doing. Statistics tracking counts instructions executed, measures execution time, and calculates instructions per second, providing performance insights. We also implement breakpoint support, allowing execution to pause when reaching specific addresses. Comprehensive logging helps track interrupt calls and their parameters, making it easier to see how programs interact with DOS and BIOS services.


Handling Edge Cases and Compatibility

Achieving good compatibility with real DOS programs requires handling numerous edge cases and subtle behaviors. Flag updates must be precise, as some programs rely on specific flag states after operations. The parity flag, for example, must correctly reflect whether the low byte of a result has an even number of set bits. Arithmetic operations must properly set carry, overflow, auxiliary carry, zero, and sign flags according to x86 specifications. String operations with the REP prefix must correctly decrement CX and check for zero, and REPZ and REPNZ must also check the zero flag. Segment wraparound must be handled correctly when addresses exceed 64KB boundaries. Stack operations must properly handle stack overflow and underflow conditions. Interrupt handling must save and restore flags correctly, and IRET must pop flags, code segment, and instruction pointer in the correct order. File operations must handle DOS-style line endings with carriage return and line feed, and text mode versus binary mode distinctions. Path handling must support both forward slashes and backslashes, handle drive letters correctly, and support relative and absolute paths. Case insensitivity must be maintained for filenames and commands, as DOS is case-insensitive. Memory allocation must prevent programs from accessing memory outside their allocated space while still allowing the freedom that DOS programs expect.


Performance Optimization Strategies

While correctness is paramount, performance is also important for a usable emulator. Several optimization strategies can significantly improve execution speed. Instruction decoding can be optimized by caching decoded instructions rather than re-decoding the same code repeatedly. A simple instruction cache indexed by memory address can provide substantial speedups for loops and frequently executed code. Register access can be optimized by using native integer types rather than constantly masking and shifting bits. Memory access patterns can be optimized by using direct array indexing rather than function calls for simple reads and writes. Interrupt dispatch can use a jump table or switch statement rather than a long chain of if-else statements. Flag calculations can be optimized by only computing flags that are actually used, though this requires careful analysis of which instructions affect which flags and which subsequent instructions test those flags. String operations can be optimized by implementing them with native loops rather than simulating individual instructions. The REP prefix can execute multiple iterations in a single step rather than decoding the same instruction repeatedly. Just-in-time compilation techniques could theoretically translate x86 code to native code, though this adds significant complexity. Profiling the emulator helps identify bottlenecks and guides optimization efforts toward the areas that will provide the most benefit.


Error Handling and Robustness

A production-quality emulator must handle errors gracefully and provide useful feedback. Invalid opcodes should be detected and reported rather than causing crashes or undefined behavior. Memory access violations should be caught and reported with the address and context. Stack overflow and underflow should be detected and handled appropriately. File operation errors from the host operating system should be translated into appropriate DOS error codes and returned to programs. Division by zero should trigger the appropriate interrupt rather than crashing the emulator. Invalid interrupt numbers should be handled gracefully. Malformed EXE headers should be detected during program loading and reported clearly. Resource exhaustion, such as running out of file handles, should be managed properly. The emulator should never crash due to program behavior, as DOS programs expect to have full control and may intentionally or accidentally perform invalid operations. Logging and error reporting should provide enough context to diagnose problems, including the instruction address, register state, and operation being attempted. Recovery mechanisms should allow the emulator to continue running or shut down cleanly even when errors occur.


Extending and Enhancing the Emulator

Once the basic emulator is functional, numerous enhancements can improve its capabilities and usability. Graphics support could be added by implementing VGA or EGA graphics modes, allowing graphical DOS programs to run. Sound support could emulate the PC speaker or Sound Blaster, enabling games and multimedia applications. Mouse support through INT 33h would allow programs that use a mouse to function properly. Extended memory and expanded memory support would enable programs that require more than 640KB of conventional memory. Disk image support could allow mounting floppy disk images or hard disk images, providing a more authentic DOS environment. Network support could enable DOS networking applications to function. Save state functionality could allow saving and restoring the entire emulator state, useful for debugging or preserving program state. Scripting support could automate testing or program execution. Configuration files could allow customizing emulator behavior, memory size, or available drives. Plugin architecture could allow extending the emulator with custom interrupt handlers or hardware emulation. Performance profiling could identify hot spots in emulated programs. Code coverage analysis could help with testing. Integration with modern development tools could enable using the emulator as part of a development workflow.


Conclusion and Future Directions

Building a DOS emulator is a substantial undertaking that provides deep insights into computer architecture, operating systems, and software engineering. The project demonstrates how modern programming languages like Go can be used to implement complex system software while maintaining clarity and portability. A functional DOS emulator preserves access to legacy software, provides an educational tool for learning about computer systems, and offers a platform for retro computing enthusiasts. The implementation requires careful attention to detail in CPU emulation, accurate interrupt handling, proper file system integration, and robust error handling. While the basic emulator provides core functionality, numerous opportunities exist for enhancement and optimization. The skills developed while building such an emulator transfer to many other domains, including virtual machine implementation, operating system development, compiler construction, and embedded systems programming. As computing continues to evolve, emulators like this ensure that the software heritage of earlier eras remains accessible and functional, allowing future generations to experience and learn from the programs that shaped the development of personal computing. The complete source code and documentation for this emulator serve as both a functional tool and an educational resource for anyone interested in low-level programming, computer architecture, or software preservation.

michael@intelligent-gagarin:~/code/go/ms-dos$ 

Building an AI Distributed IoT Air Pollution Monitoring Network



Introduction and System Overview


Air pollution monitoring has become increasingly critical in urban environments where industrial activities, vehicle emissions, and other sources contribute to deteriorating air quality. Traditional monitoring systems often rely on expensive, centralized equipment that provides limited spatial coverage. This article presents a comprehensive solution for building a distributed Internet of Things (IoT) network that enables cost-effective, wide-area air pollution monitoring using multiple remote sensor devices.

The proposed system architecture consists of three primary components working in harmony. First, numerous ESP32-based sensor nodes equipped with air quality sensors continuously collect environmental data from their respective locations. Second, a central web server receives, processes, and stores data from all registered devices while maintaining their geographical information. Third, an intelligent data processing system that utilizes artificial intelligence algorithms to analyze collected data and provide meaningful insights to end users.

The fundamental operation principle involves each sensor device measuring air pollution parameters at regular intervals and transmitting this data along with its unique identifier to the central server. The server then processes incoming data by grouping measurements from devices within configurable geographical regions and computing average pollution levels for those areas. Users can query the system to obtain real-time air quality information for any specified location and radius.

This distributed approach offers several advantages over traditional monitoring systems. The network provides higher spatial resolution by deploying multiple low-cost sensors across a wide area. The system demonstrates excellent scalability, allowing new devices to be added without significant infrastructure changes. Additionally, the redundancy inherent in multiple sensors improves data reliability and system robustness.


Hardware Architecture and Component Selection


ESP32 Microcontroller Platform


The ESP32 serves as the foundation for each sensor node due to its integrated WiFi capabilities, sufficient processing power, and extensive peripheral support. The ESP32 features a dual-core Tensilica Xtensa LX6 microprocessor running at up to 240 MHz, providing adequate computational resources for sensor data processing and network communication. The integrated 802.11 b/g/n WiFi transceiver eliminates the need for additional networking hardware, reducing both cost and complexity.

The ESP32 development board includes essential components such as voltage regulators, crystal oscillators, and USB-to-serial converters that simplify the development process. The board provides numerous GPIO pins for sensor interfacing, including analog-to-digital converters, I2C, SPI, and UART communication interfaces. The built-in flash memory stores the firmware program, while the RAM provides sufficient space for data buffering and network operations.


Air Quality Sensor Selection


For air pollution measurement, the MQ-135 gas sensor provides a cost-effective solution for detecting various air pollutants including ammonia, nitrogen oxides, benzene, smoke, and carbon dioxide. This semiconductor-based sensor operates by measuring changes in electrical conductivity when exposed to target gases. The sensor output varies proportionally to the concentration of detected pollutants, providing an analog voltage signal that the ESP32 can easily process.

The MQ-135 sensor requires a heating element to maintain optimal operating temperature, which consumes approximately 150 milliwatts of power. The sensor includes both analog and digital outputs, though the analog output provides more precise measurements suitable for quantitative analysis. The response time typically ranges from 10 to 60 seconds, making it suitable for continuous monitoring applications.


Circuit Design and Connections


The hardware circuit connects the MQ-135 sensor to the ESP32 through a simple interface that requires minimal external components. The sensor's VCC pin connects to the ESP32's 3.3V power supply, while the ground pin connects to the common ground. The analog output pin connects to one of the ESP32's ADC-capable GPIO pins, specifically GPIO34 in this implementation.

A pull-up resistor of 10 kilohms connects between the sensor's analog output and the power supply to ensure stable readings. Additionally, a bypass capacitor of 100 microfarads connects between power and ground near the sensor to filter power supply noise. These components ensure reliable sensor operation and accurate measurements.

The complete circuit requires minimal external components, making it suitable for compact, low-cost deployment. The ESP32's built-in voltage regulation eliminates the need for additional power conditioning circuits when powered from a 5V USB source or appropriate wall adapter.


ESP32 Air Quality Sensor Circuit Diagram:


                    +3.3V

                      |

                     +-+

                     | | 10kΩ

                     | |

                     +-+

                      |

    ESP32             |              MQ-135

   +-------+          |             +-------+

   |       |          +-------------|VCC    |

   |GPIO34 |<---------------------- |A0     |

   |       |                        |       |

   |GND    |------------------------|GND    |

   |       |                        |       |

   |3.3V   |------------------------|VCC    |

   +-------+                        +-------+

                                         |

                                       -----

                                       ----- 100µF

                                         |

                                        GND


ESP32 Firmware Implementation


Core System Architecture


The ESP32 firmware follows a modular architecture that separates concerns into distinct functional units. The main program loop coordinates between sensor reading, data processing, network communication, and power management functions. This separation ensures maintainable code and facilitates future enhancements or modifications.

The firmware implements a state machine approach where the device cycles through different operational states including initialization, sensor reading, data transmission, and sleep modes. This approach optimizes power consumption while ensuring reliable data collection and transmission.


WiFi Network Management


The WiFi connection management system handles network connectivity with automatic reconnection capabilities. The system stores network credentials in non-volatile memory and attempts to connect to the configured network during startup. If the connection fails, the system implements an exponential backoff strategy to avoid overwhelming the network infrastructure.


#include <WiFi.h>

#include <HTTPClient.h>

#include <ArduinoJson.h>

#include <EEPROM.h>

#include <esp_system.h>


// Network configuration constants

const char* WIFI_SSID = "YourNetworkName";

const char* WIFI_PASSWORD = "YourNetworkPassword";

const char* SERVER_URL = "http://your-server.com/api/sensor-data";


// Device configuration

const int SENSOR_PIN = 34;

const int MEASUREMENT_INTERVAL = 600000; // 10 minutes in milliseconds

const int MAX_RETRY_ATTEMPTS = 3;

const int WIFI_TIMEOUT = 10000; // 10 seconds


// Global variables for device operation

String deviceUUID;

unsigned long lastMeasurementTime = 0;

bool wifiConnected = false;


class WiFiManager {

private:

    int connectionAttempts;

    unsigned long lastConnectionAttempt;

    

public:

    WiFiManager() : connectionAttempts(0), lastConnectionAttempt(0) {}

    

    bool initializeConnection() {

        WiFi.mode(WIFI_STA);

        WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

        

        Serial.println("Connecting to WiFi network...");

        

        unsigned long startTime = millis();

        while (WiFi.status() != WL_CONNECTED && 

               (millis() - startTime) < WIFI_TIMEOUT) {

            delay(500);

            Serial.print(".");

        }

        

        if (WiFi.status() == WL_CONNECTED) {

            Serial.println("\nWiFi connected successfully");

            Serial.print("IP address: ");

            Serial.println(WiFi.localIP());

            connectionAttempts = 0;

            return true;

        } else {

            Serial.println("\nWiFi connection failed");

            connectionAttempts++;

            return false;

        }

    }

    

    bool isConnected() {

        return WiFi.status() == WL_CONNECTED;

    }

    

    bool reconnectIfNeeded() {

        if (!isConnected()) {

            Serial.println("WiFi disconnected, attempting reconnection...");

            return initializeConnection();

        }

        return true;

    }

    

    void handleConnectionFailure() {

        if (connectionAttempts >= MAX_RETRY_ATTEMPTS) {

            Serial.println("Maximum connection attempts reached, entering deep sleep");

            esp_deep_sleep(MEASUREMENT_INTERVAL * 1000);

        }

    }

};


The WiFiManager class encapsulates all network-related functionality, providing clean interfaces for connection management. The class implements retry logic with configurable timeouts to handle temporary network issues gracefully. When maximum retry attempts are reached, the device enters deep sleep mode to conserve power and avoid continuous failed connection attempts.


Sensor Data Acquisition System


The sensor data acquisition system implements calibrated readings from the MQ-135 air quality sensor. The system performs multiple readings and applies statistical filtering to reduce noise and improve measurement accuracy. The implementation includes temperature compensation and baseline correction to account for environmental variations.


class AirQualitySensor {

private:

    int sensorPin;

    float baselineResistance;

    int warmupTime;

    bool isWarmedUp;

    

    // Calibration constants for MQ-135 sensor

    static constexpr float RLOAD = 10.0; // Load resistance in kΩ

    static constexpr float RZERO = 76.63; // Sensor resistance in clean air

    static constexpr float PARA = 116.6020682; // Calibration parameter

    static constexpr float PARB = 2.769034857; // Calibration parameter

    

public:

    AirQualitySensor(int pin) : sensorPin(pin), isWarmedUp(false), warmupTime(20000) {

        pinMode(sensorPin, INPUT);

        baselineResistance = 0.0;

    }

    

    void initialize() {

        Serial.println("Initializing air quality sensor...");

        

        // Allow sensor to warm up

        unsigned long startTime = millis();

        while ((millis() - startTime) < warmupTime) {

            delay(1000);

            Serial.print("Warming up sensor... ");

            Serial.print((millis() - startTime) / 1000);

            Serial.println(" seconds");

        }

        

        // Calculate baseline resistance in clean air

        baselineResistance = calculateResistance();

        isWarmedUp = true;

        

        Serial.print("Sensor initialization complete. Baseline resistance: ");

        Serial.print(baselineResistance);

        Serial.println(" kΩ");

    }

    

    float readRawValue() {

        if (!isWarmedUp) {

            Serial.println("Warning: Sensor not properly warmed up");

            return -1.0;

        }

        

        // Take multiple readings for averaging

        const int numReadings = 10;

        float totalValue = 0.0;

        

        for (int i = 0; i < numReadings; i++) {

            int analogValue = analogRead(sensorPin);

            float voltage = (analogValue / 4095.0) * 3.3; // Convert to voltage

            float resistance = calculateResistanceFromVoltage(voltage);

            totalValue += resistance;

            delay(100); // Small delay between readings

        }

        

        return totalValue / numReadings;

    }

    

    float calculatePPM() {

        float resistance = readRawValue();

        if (resistance < 0) {

            return -1.0; // Error condition

        }

        

        // Calculate ratio of current resistance to baseline

        float ratio = resistance / RZERO;

        

        // Convert resistance ratio to PPM using calibration curve

        float ppm = PARA * pow(ratio, -PARB);

        

        return ppm;

    }

    

private:

    float calculateResistance() {

        int analogValue = analogRead(sensorPin);

        float voltage = (analogValue / 4095.0) * 3.3;

        return calculateResistanceFromVoltage(voltage);

    }

    

    float calculateResistanceFromVoltage(float voltage) {

        if (voltage <= 0.0) {

            return 0.0;

        }

        

        // Calculate sensor resistance using voltage divider formula

        float resistance = ((3.3 - voltage) / voltage) * RLOAD;

        return resistance;

    }

};


The AirQualitySensor class implements comprehensive sensor management including initialization, calibration, and measurement functions. The class performs statistical averaging across multiple readings to reduce measurement noise and improve data quality. The implementation includes proper sensor warm-up procedures and resistance-to-PPM conversion using calibrated parameters specific to the MQ-135 sensor.


Data Transmission and Communication Protocol


The data transmission system implements a robust HTTP-based communication protocol for sending sensor measurements to the central server. The system formats data in JSON format and includes error handling for network failures and server responses.


class DataTransmissionManager {

private:

    String serverURL;

    String deviceID;

    HTTPClient httpClient;

    

public:

    DataTransmissionManager(const String& url, const String& id) 

        : serverURL(url), deviceID(id) {}

    

    bool transmitSensorData(float ppmValue, float latitude, float longitude) {

        if (ppmValue < 0) {

            Serial.println("Invalid sensor reading, skipping transmission");

            return false;

        }

        

        // Create JSON payload

        DynamicJsonDocument jsonDoc(1024);

        jsonDoc["device_id"] = deviceID;

        jsonDoc["timestamp"] = getCurrentTimestamp();

        jsonDoc["air_quality_ppm"] = ppmValue;

        jsonDoc["latitude"] = latitude;

        jsonDoc["longitude"] = longitude;

        jsonDoc["sensor_type"] = "MQ135";

        jsonDoc["firmware_version"] = "1.0.0";

        

        String jsonString;

        serializeJson(jsonDoc, jsonString);

        

        Serial.println("Transmitting sensor data:");

        Serial.println(jsonString);

        

        // Configure HTTP client

        httpClient.begin(serverURL);

        httpClient.addHeader("Content-Type", "application/json");

        httpClient.addHeader("User-Agent", "ESP32-AirQuality-Sensor/1.0");

        httpClient.setTimeout(15000); // 15 second timeout

        

        // Send POST request

        int httpResponseCode = httpClient.POST(jsonString);

        

        if (httpResponseCode > 0) {

            String response = httpClient.getString();

            Serial.print("HTTP Response Code: ");

            Serial.println(httpResponseCode);

            Serial.print("Server Response: ");

            Serial.println(response);

            

            httpClient.end();

            return (httpResponseCode >= 200 && httpResponseCode < 300);

        } else {

            Serial.print("HTTP Error: ");

            Serial.println(httpClient.errorToString(httpResponseCode));

            httpClient.end();

            return false;

        }

    }

    

private:

    unsigned long getCurrentTimestamp() {

        // In a production system, this would sync with NTP server

        // For simplicity, using millis() as relative timestamp

        return millis();

    }

};


Device Registration and UUID Management


The device registration system ensures each sensor node has a unique identifier and properly registers with the central server during initial setup. The system stores the UUID in non-volatile memory to maintain consistency across power cycles.


class DeviceManager {

private:

    String uuid;

    bool isRegistered;

    

public:

    DeviceManager() : isRegistered(false) {}

    

    void initialize() {

        // Try to load existing UUID from EEPROM

        EEPROM.begin(512);

        uuid = loadUUIDFromEEPROM();

        

        if (uuid.length() == 0) {

            // Generate new UUID if none exists

            uuid = generateUUID();

            saveUUIDToEEPROM(uuid);

            Serial.println("Generated new device UUID: " + uuid);

        } else {

            Serial.println("Loaded existing UUID: " + uuid);

        }

    }

    

    String getDeviceUUID() {

        return uuid;

    }

    

    bool registerWithServer(float latitude, float longitude) {

        HTTPClient httpClient;

        String registrationURL = "http://your-server.com/api/register-device";

        

        // Create registration payload

        DynamicJsonDocument jsonDoc(512);

        jsonDoc["device_id"] = uuid;

        jsonDoc["device_type"] = "air_quality_sensor";

        jsonDoc["latitude"] = latitude;

        jsonDoc["longitude"] = longitude;

        jsonDoc["sensor_model"] = "MQ135";

        jsonDoc["firmware_version"] = "1.0.0";

        

        String jsonString;

        serializeJson(jsonDoc, jsonString);

        

        httpClient.begin(registrationURL);

        httpClient.addHeader("Content-Type", "application/json");

        

        int responseCode = httpClient.POST(jsonString);

        

        if (responseCode >= 200 && responseCode < 300) {

            Serial.println("Device registered successfully with server");

            isRegistered = true;

            httpClient.end();

            return true;

        } else {

            Serial.print("Device registration failed. Response code: ");

            Serial.println(responseCode);

            httpClient.end();

            return false;

        }

    }

    

private:

    String generateUUID() {

        // Simple UUID generation based on ESP32 chip ID and random values

        uint64_t chipId = ESP.getEfuseMac();

        String uuid = String((uint32_t)(chipId >> 32), HEX) + 

                     String((uint32_t)chipId, HEX) + 

                     String(random(0x10000000, 0xFFFFFFFF), HEX);

        uuid.toUpperCase();

        return uuid;

    }

    

    void saveUUIDToEEPROM(const String& uuid) {

        for (int i = 0; i < uuid.length() && i < 32; i++) {

            EEPROM.write(i, uuid[i]);

        }

        EEPROM.write(uuid.length(), '\0');

        EEPROM.commit();

    }

    

    String loadUUIDFromEEPROM() {

        String uuid = "";

        char ch;

        for (int i = 0; i < 32; i++) {

            ch = EEPROM.read(i);

            if (ch == '\0') break;

            uuid += ch;

        }

        return uuid;

    }

};


Main Program Structure and Execution Flow


The main program coordinates all system components and implements the primary execution loop. The program handles initialization, periodic sensor readings, data transmission, and power management.


// Global object instances

WiFiManager wifiManager;

AirQualitySensor airSensor(SENSOR_PIN);

DataTransmissionManager dataManager(SERVER_URL, "");

DeviceManager deviceManager;


void setup() {

    Serial.begin(115200);

    Serial.println("ESP32 Air Quality Sensor Starting...");

    

    // Initialize random number generator

    randomSeed(analogRead(0));

    

    // Initialize device manager and get UUID

    deviceManager.initialize();

    String deviceUUID = deviceManager.getDeviceUUID();

    

    // Update data manager with device UUID

    dataManager = DataTransmissionManager(SERVER_URL, deviceUUID);

    

    // Initialize WiFi connection

    if (!wifiManager.initializeConnection()) {

        Serial.println("Failed to connect to WiFi, entering deep sleep");

        esp_deep_sleep(MEASUREMENT_INTERVAL * 1000);

    }

    

    // Register device with server (with hardcoded coordinates for this example)

    // In production, GPS module would provide actual coordinates

    float deviceLatitude = 40.7128;  // Example: New York City

    float deviceLongitude = -74.0060;

    

    if (!deviceManager.registerWithServer(deviceLatitude, deviceLongitude)) {

        Serial.println("Device registration failed, continuing with measurements");

    }

    

    // Initialize air quality sensor

    airSensor.initialize();

    

    Serial.println("System initialization complete");

    lastMeasurementTime = millis();

}


void loop() {

    unsigned long currentTime = millis();

    

    // Check if it's time for next measurement

    if (currentTime - lastMeasurementTime >= MEASUREMENT_INTERVAL) {

        

        // Ensure WiFi connection is active

        if (!wifiManager.reconnectIfNeeded()) {

            wifiManager.handleConnectionFailure();

            return;

        }

        

        // Read sensor data

        Serial.println("Taking air quality measurement...");

        float ppmValue = airSensor.calculatePPM();

        

        if (ppmValue >= 0) {

            Serial.print("Air quality reading: ");

            Serial.print(ppmValue);

            Serial.println(" PPM");

            

            // Transmit data to server

            float deviceLatitude = 40.7128;  // In production, get from GPS

            float deviceLongitude = -74.0060;

            

            bool transmissionSuccess = dataManager.transmitSensorData(

                ppmValue, deviceLatitude, deviceLongitude);

            

            if (transmissionSuccess) {

                Serial.println("Data transmitted successfully");

            } else {

                Serial.println("Data transmission failed");

            }

        } else {

            Serial.println("Invalid sensor reading, skipping transmission");

        }

        

        lastMeasurementTime = currentTime;

    }

    

    // Small delay to prevent excessive CPU usage

    delay(1000);

}


Central Web Server Implementation


Server Architecture and Technology Stack


The central web server implements a scalable architecture capable of handling multiple concurrent sensor devices while providing real-time data access to end users. The server utilizes Node.js with Express framework for HTTP request handling, MongoDB for data persistence, and implements RESTful API endpoints for device communication and user queries.

The server architecture follows a modular design pattern with separate modules for device management, data processing, geographical calculations, and artificial intelligence-based data analysis. This separation ensures maintainable code and facilitates future enhancements or scaling requirements.


Database Schema and Data Models


The database schema accommodates device registration information, sensor measurements, and user access patterns. The design optimizes for both write-heavy operations from sensor devices and read-heavy operations from user queries.

Javascript


// server.js - Main server application

const express = require('express');

const mongoose = require('mongoose');

const cors = require('cors');

const helmet = require('helmet');

const rateLimit = require('express-rate-limit');

const compression = require('compression');


// Import custom modules

const deviceRoutes = require('./routes/devices');

const dataRoutes = require('./routes/data');

const analyticsRoutes = require('./routes/analytics');

const { connectDatabase } = require('./config/database');

const { logger } = require('./utils/logger');


// Express application setup

const app = express();

const PORT = process.env.PORT || 3000;


// Security middleware

app.use(helmet());

app.use(cors({

    origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],

    credentials: true

}));


// Rate limiting configuration

const limiter = rateLimit({

    windowMs: 15 * 60 * 1000, // 15 minutes

    max: 100, // Limit each IP to 100 requests per windowMs

    message: 'Too many requests from this IP, please try again later.'

});

app.use('/api/', limiter);


// Compression and parsing middleware

app.use(compression());

app.use(express.json({ limit: '10mb' }));

app.use(express.urlencoded({ extended: true }));


// Request logging middleware

app.use((req, res, next) => {

    logger.info(`${req.method} ${req.path} - ${req.ip}`);

    next();

});


// API routes

app.use('/api/devices', deviceRoutes);

app.use('/api/data', dataRoutes);

app.use('/api/analytics', analyticsRoutes);


// Health check endpoint

app.get('/health', (req, res) => {

    res.status(200).json({

        status: 'healthy',

        timestamp: new Date().toISOString(),

        uptime: process.uptime()

    });

});


// Error handling middleware

app.use((error, req, res, next) => {

    logger.error('Unhandled error:', error);

    res.status(500).json({

        error: 'Internal server error',

        message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong'

    });

});


// 404 handler

app.use('*', (req, res) => {

    res.status(404).json({

        error: 'Not found',

        message: 'The requested resource was not found'

    });

});


// Database connection and server startup

async function startServer() {

    try {

        await connectDatabase();

        logger.info('Database connected successfully');

        

        app.listen(PORT, () => {

            logger.info(`Air Quality Monitoring Server running on port ${PORT}`);

            logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);

        });

    } catch (error) {

        logger.error('Failed to start server:', error);

        process.exit(1);

    }

}


// Graceful shutdown handling

process.on('SIGTERM', () => {

    logger.info('SIGTERM received, shutting down gracefully');

    process.exit(0);

});


process.on('SIGINT', () => {

    logger.info('SIGINT received, shutting down gracefully');

    process.exit(0);

});


startServer();


Database Models and Schema Definitions


The database models define the structure for storing device information, sensor measurements, and analytical results. The schema includes proper indexing for efficient geographical queries and time-based data retrieval.


Javascript


// models/Device.js - Device registration model

const mongoose = require('mongoose');


const deviceSchema = new mongoose.Schema({

    deviceId: {

        type: String,

        required: true,

        unique: true,

        index: true,

        trim: true,

        maxlength: 64

    },

    deviceType: {

        type: String,

        required: true,

        enum: ['air_quality_sensor', 'weather_station', 'noise_monitor'],

        default: 'air_quality_sensor'

    },

    location: {

        type: {

            type: String,

            enum: ['Point'],

            required: true,

            default: 'Point'

        },

        coordinates: {

            type: [Number],

            required: true,

            validate: {

                validator: function(coords) {

                    return coords.length === 2 && 

                           coords[0] >= -180 && coords[0] <= 180 && // longitude

                           coords[1] >= -90 && coords[1] <= 90;    // latitude

                },

                message: 'Invalid coordinates format'

            }

        }

    },

    sensorModel: {

        type: String,

        required: true,

        trim: true,

        maxlength: 50

    },

    firmwareVersion: {

        type: String,

        required: true,

        trim: true,

        maxlength: 20

    },

    registrationDate: {

        type: Date,

        default: Date.now,

        index: true

    },

    lastActivity: {

        type: Date,

        default: Date.now,

        index: true

    },

    isActive: {

        type: Boolean,

        default: true,

        index: true

    },

    calibrationData: {

        baselineResistance: Number,

        calibrationDate: Date,

        calibrationCoefficients: [Number]

    }

}, {

    timestamps: true

});


// Create geospatial index for location-based queries

deviceSchema.index({ location: '2dsphere' });


// Index for efficient device lookup

deviceSchema.index({ deviceId: 1, isActive: 1 });


// Update last activity timestamp on save

deviceSchema.pre('save', function(next) {

    this.lastActivity = new Date();

    next();

});


// Instance methods

deviceSchema.methods.updateActivity = function() {

    this.lastActivity = new Date();

    return this.save();

};


deviceSchema.methods.deactivate = function() {

    this.isActive = false;

    return this.save();

};


// Static methods

deviceSchema.statics.findActiveDevices = function() {

    return this.find({ isActive: true });

};


deviceSchema.statics.findDevicesInRadius = function(longitude, latitude, radiusMeters) {

    return this.find({

        location: {

            $geoWithin: {

                $centerSphere: [[longitude, latitude], radiusMeters / 6378100]

            }

        },

        isActive: true

    });

};


module.exports = mongoose.model('Device', deviceSchema);


Javascript


// models/SensorData.js - Sensor measurement model

const mongoose = require('mongoose');


const sensorDataSchema = new mongoose.Schema({

    deviceId: {

        type: String,

        required: true,

        index: true,

        ref: 'Device'

    },

    timestamp: {

        type: Date,

        required: true,

        index: true,

        default: Date.now

    },

    location: {

        type: {

            type: String,

            enum: ['Point'],

            required: true,

            default: 'Point'

        },

        coordinates: {

            type: [Number],

            required: true

        }

    },

    measurements: {

        airQualityPPM: {

            type: Number,

            required: true,

            min: 0,

            max: 10000,

            validate: {

                validator: function(value) {

                    return !isNaN(value) && isFinite(value);

                },

                message: 'Air quality PPM must be a valid number'

            }

        },

        temperature: {

            type: Number,

            min: -50,

            max: 100

        },

        humidity: {

            type: Number,

            min: 0,

            max: 100

        },

        pressure: {

            type: Number,

            min: 800,

            max: 1200

        }

    },

    sensorType: {

        type: String,

        required: true,

        enum: ['MQ135', 'MQ7', 'BME280', 'DHT22'],

        default: 'MQ135'

    },

    firmwareVersion: {

        type: String,

        required: true

    },

    dataQuality: {

        type: String,

        enum: ['excellent', 'good', 'fair', 'poor'],

        default: 'good'

    },

    processed: {

        type: Boolean,

        default: false,

        index: true

    }

}, {

    timestamps: true

});


// Compound indexes for efficient queries

sensorDataSchema.index({ deviceId: 1, timestamp: -1 });

sensorDataSchema.index({ timestamp: -1, processed: 1 });

sensorDataSchema.index({ location: '2dsphere', timestamp: -1 });


// Time-based partitioning index

sensorDataSchema.index({ 

    timestamp: -1 

}, { 

    expireAfterSeconds: 60 * 60 * 24 * 365 // 1 year retention

});


// Static methods for data analysis

sensorDataSchema.statics.getAverageInRadius = async function(longitude, latitude, radiusMeters, startTime, endTime) {

    const pipeline = [

        {

            $match: {

                location: {

                    $geoWithin: {

                        $centerSphere: [[longitude, latitude], radiusMeters / 6378100]

                    }

                },

                timestamp: {

                    $gte: startTime,

                    $lte: endTime

                }

            }

        },

        {

            $group: {

                _id: null,

                averageAirQuality: { $avg: '$measurements.airQualityPPM' },

                averageTemperature: { $avg: '$measurements.temperature' },

                averageHumidity: { $avg: '$measurements.humidity' },

                sampleCount: { $sum: 1 },

                deviceCount: { $addToSet: '$deviceId' }

            }

        },

        {

            $project: {

                _id: 0,

                averageAirQuality: { $round: ['$averageAirQuality', 2] },

                averageTemperature: { $round: ['$averageTemperature', 2] },

                averageHumidity: { $round: ['$averageHumidity', 2] },

                sampleCount: 1,

                deviceCount: { $size: '$deviceCount' }

            }

        }

    ];

    

    const result = await this.aggregate(pipeline);

    return result[0] || null;

};


sensorDataSchema.statics.getTimeSeriesData = function(deviceId, startTime, endTime, interval = '1h') {

    const groupBy = {

        '1m': { $dateToString: { format: '%Y-%m-%d %H:%M', date: '$timestamp' } },

        '1h': { $dateToString: { format: '%Y-%m-%d %H:00', date: '$timestamp' } },

        '1d': { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } }

    };

    

    return this.aggregate([

        {

            $match: {

                deviceId: deviceId,

                timestamp: { $gte: startTime, $lte: endTime }

            }

        },

        {

            $group: {

                _id: groupBy[interval] || groupBy['1h'],

                averageAirQuality: { $avg: '$measurements.airQualityPPM' },

                minAirQuality: { $min: '$measurements.airQualityPPM' },

                maxAirQuality: { $max: '$measurements.airQualityPPM' },

                sampleCount: { $sum: 1 }

            }

        },

        {

            $sort: { _id: 1 }

        }

    ]);

};


module.exports = mongoose.model('SensorData', sensorDataSchema);


Device Registration and Management API


The device registration system handles new device onboarding, location updates, and device status management. The API validates device information and maintains an active device registry.


Javascript


// routes/devices.js - Device management routes

const express = require('express');

const Device = require('../models/Device');

const { logger } = require('../utils/logger');

const { validateDeviceRegistration } = require('../middleware/validation');


const router = express.Router();


// Register new device

router.post('/register', validateDeviceRegistration, async (req, res) => {

    try {

        const {

            device_id,

            device_type,

            latitude,

            longitude,

            sensor_model,

            firmware_version

        } = req.body;


        // Check if device already exists

        const existingDevice = await Device.findOne({ deviceId: device_id });

        if (existingDevice) {

            // Update existing device information

            existingDevice.location.coordinates = [longitude, latitude];

            existingDevice.sensorModel = sensor_model;

            existingDevice.firmwareVersion = firmware_version;

            existingDevice.isActive = true;

            

            await existingDevice.save();

            

            logger.info(`Device ${device_id} updated registration`);

            return res.status(200).json({

                success: true,

                message: 'Device registration updated successfully',

                deviceId: device_id

            });

        }


        // Create new device registration

        const newDevice = new Device({

            deviceId: device_id,

            deviceType: device_type || 'air_quality_sensor',

            location: {

                type: 'Point',

                coordinates: [longitude, latitude]

            },

            sensorModel: sensor_model,

            firmwareVersion: firmware_version,

            registrationDate: new Date(),

            isActive: true

        });


        await newDevice.save();

        

        logger.info(`New device registered: ${device_id}`);

        res.status(201).json({

            success: true,

            message: 'Device registered successfully',

            deviceId: device_id

        });


    } catch (error) {

        logger.error('Device registration error:', error);

        res.status(500).json({

            success: false,

            error: 'Device registration failed',

            message: error.message

        });

    }

});


// Get device information

router.get('/:deviceId', async (req, res) => {

    try {

        const { deviceId } = req.params;

        

        const device = await Device.findOne({ deviceId: deviceId });

        if (!device) {

            return res.status(404).json({

                success: false,

                error: 'Device not found'

            });

        }


        res.json({

            success: true,

            device: {

                deviceId: device.deviceId,

                deviceType: device.deviceType,

                location: device.location,

                sensorModel: device.sensorModel,

                firmwareVersion: device.firmwareVersion,

                registrationDate: device.registrationDate,

                lastActivity: device.lastActivity,

                isActive: device.isActive

            }

        });


    } catch (error) {

        logger.error('Get device error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to retrieve device information'

        });

    }

});


// List all active devices

router.get('/', async (req, res) => {

    try {

        const { page = 1, limit = 50, type } = req.query;

        

        const query = { isActive: true };

        if (type) {

            query.deviceType = type;

        }


        const devices = await Device.find(query)

            .select('deviceId deviceType location sensorModel lastActivity')

            .limit(limit * 1)

            .skip((page - 1) * limit)

            .sort({ lastActivity: -1 });


        const totalDevices = await Device.countDocuments(query);


        res.json({

            success: true,

            devices: devices,

            pagination: {

                currentPage: parseInt(page),

                totalPages: Math.ceil(totalDevices / limit),

                totalDevices: totalDevices,

                hasNextPage: page * limit < totalDevices,

                hasPrevPage: page > 1

            }

        });


    } catch (error) {

        logger.error('List devices error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to retrieve device list'

        });

    }

});


// Update device location

router.put('/:deviceId/location', async (req, res) => {

    try {

        const { deviceId } = req.params;

        const { latitude, longitude } = req.body;


        if (!latitude || !longitude) {

            return res.status(400).json({

                success: false,

                error: 'Latitude and longitude are required'

            });

        }


        const device = await Device.findOne({ deviceId: deviceId });

        if (!device) {

            return res.status(404).json({

                success: false,

                error: 'Device not found'

            });

        }


        device.location.coordinates = [longitude, latitude];

        await device.save();


        logger.info(`Device ${deviceId} location updated`);

        res.json({

            success: true,

            message: 'Device location updated successfully'

        });


    } catch (error) {

        logger.error('Update device location error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to update device location'

        });

    }

});


// Deactivate device

router.delete('/:deviceId', async (req, res) => {

    try {

        const { deviceId } = req.params;

        

        const device = await Device.findOne({ deviceId: deviceId });

        if (!device) {

            return res.status(404).json({

                success: false,

                error: 'Device not found'

            });

        }


        await device.deactivate();

        

        logger.info(`Device ${deviceId} deactivated`);

        res.json({

            success: true,

            message: 'Device deactivated successfully'

        });


    } catch (error) {

        logger.error('Deactivate device error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to deactivate device'

        });

    }

});


module.exports = router;


Sensor Data Ingestion API


The data ingestion system receives sensor measurements from ESP32 devices, validates the data, and stores it in the database with proper indexing for efficient retrieval.


Javascript


// routes/data.js - Sensor data ingestion and retrieval routes

const express = require('express');

const SensorData = require('../models/SensorData');

const Device = require('../models/Device');

const { logger } = require('../utils/logger');

const { validateSensorData } = require('../middleware/validation');

const { calculateAirQualityIndex } = require('../utils/airQuality');


const router = express.Router();


// Receive sensor data from devices

router.post('/sensor-data', validateSensorData, async (req, res) => {

    try {

        const {

            device_id,

            timestamp,

            air_quality_ppm,

            latitude,

            longitude,

            sensor_type,

            firmware_version,

            temperature,

            humidity

        } = req.body;


        // Verify device exists and is active

        const device = await Device.findOne({ 

            deviceId: device_id, 

            isActive: true 

        });

        

        if (!device) {

            return res.status(404).json({

                success: false,

                error: 'Device not found or inactive'

            });

        }


        // Update device last activity

        await device.updateActivity();


        // Create sensor data record

        const sensorData = new SensorData({

            deviceId: device_id,

            timestamp: timestamp ? new Date(timestamp) : new Date(),

            location: {

                type: 'Point',

                coordinates: [longitude, latitude]

            },

            measurements: {

                airQualityPPM: air_quality_ppm,

                temperature: temperature,

                humidity: humidity

            },

            sensorType: sensor_type || 'MQ135',

            firmwareVersion: firmware_version,

            dataQuality: calculateDataQuality(air_quality_ppm, temperature, humidity)

        });


        await sensorData.save();


        logger.info(`Sensor data received from device ${device_id}: ${air_quality_ppm} PPM`);

        

        res.status(201).json({

            success: true,

            message: 'Sensor data received successfully',

            dataId: sensorData._id,

            airQualityIndex: calculateAirQualityIndex(air_quality_ppm)

        });


    } catch (error) {

        logger.error('Sensor data ingestion error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to process sensor data',

            message: error.message

        });

    }

});


// Get aggregated data for a geographical area

router.get('/area-average', async (req, res) => {

    try {

        const {

            latitude,

            longitude,

            radius = 1000, // Default 1km radius

            start_time,

            end_time,

            interval = '1h'

        } = req.query;


        if (!latitude || !longitude) {

            return res.status(400).json({

                success: false,

                error: 'Latitude and longitude are required'

            });

        }


        const lat = parseFloat(latitude);

        const lng = parseFloat(longitude);

        const radiusMeters = parseInt(radius);


        // Default time range: last 24 hours

        const endTime = end_time ? new Date(end_time) : new Date();

        const startTime = start_time ? new Date(start_time) : 

                         new Date(endTime.getTime() - 24 * 60 * 60 * 1000);


        // Get average data for the area

        const averageData = await SensorData.getAverageInRadius(

            lng, lat, radiusMeters, startTime, endTime

        );


        if (!averageData) {

            return res.json({

                success: true,

                message: 'No data available for the specified area and time range',

                data: null

            });

        }


        // Calculate air quality index and health recommendations

        const airQualityIndex = calculateAirQualityIndex(averageData.averageAirQuality);

        const healthRecommendation = getHealthRecommendation(airQualityIndex);


        res.json({

            success: true,

            data: {

                location: {

                    latitude: lat,

                    longitude: lng,

                    radius: radiusMeters

                },

                timeRange: {

                    startTime: startTime,

                    endTime: endTime

                },

                measurements: {

                    averageAirQuality: averageData.averageAirQuality,

                    averageTemperature: averageData.averageTemperature,

                    averageHumidity: averageData.averageHumidity,

                    airQualityIndex: airQualityIndex,

                    healthRecommendation: healthRecommendation

                },

                statistics: {

                    sampleCount: averageData.sampleCount,

                    deviceCount: averageData.deviceCount

                }

            }

        });


    } catch (error) {

        logger.error('Area average calculation error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to calculate area average'

        });

    }

});


// Get time series data for a specific device

router.get('/device/:deviceId/timeseries', async (req, res) => {

    try {

        const { deviceId } = req.params;

        const {

            start_time,

            end_time,

            interval = '1h'

        } = req.query;


        // Verify device exists

        const device = await Device.findOne({ deviceId: deviceId });

        if (!device) {

            return res.status(404).json({

                success: false,

                error: 'Device not found'

            });

        }


        // Default time range: last 7 days

        const endTime = end_time ? new Date(end_time) : new Date();

        const startTime = start_time ? new Date(start_time) : 

                         new Date(endTime.getTime() - 7 * 24 * 60 * 60 * 1000);


        const timeSeriesData = await SensorData.getTimeSeriesData(

            deviceId, startTime, endTime, interval

        );


        res.json({

            success: true,

            deviceId: deviceId,

            timeRange: {

                startTime: startTime,

                endTime: endTime,

                interval: interval

            },

            data: timeSeriesData

        });


    } catch (error) {

        logger.error('Time series data error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to retrieve time series data'

        });

    }

});


// Helper functions

function calculateDataQuality(airQuality, temperature, humidity) {

    let qualityScore = 100;

    

    // Penalize extreme or unrealistic values

    if (airQuality < 0 || airQuality > 1000) qualityScore -= 50;

    if (temperature && (temperature < -40 || temperature > 60)) qualityScore -= 30;

    if (humidity && (humidity < 0 || humidity > 100)) qualityScore -= 30;

    

    if (qualityScore >= 90) return 'excellent';

    if (qualityScore >= 70) return 'good';

    if (qualityScore >= 50) return 'fair';

    return 'poor';

}


function getHealthRecommendation(airQualityIndex) {

    if (airQualityIndex <= 50) {

        return 'Air quality is good. Ideal for outdoor activities.';

    } else if (airQualityIndex <= 100) {

        return 'Air quality is moderate. Sensitive individuals should consider limiting outdoor activities.';

    } else if (airQualityIndex <= 150) {

        return 'Air quality is unhealthy for sensitive groups. Limit outdoor activities if you are sensitive to air pollution.';

    } else if (airQualityIndex <= 200) {

        return 'Air quality is unhealthy. Everyone should limit outdoor activities.';

    } else {

        return 'Air quality is very unhealthy. Avoid outdoor activities.';

    }

}


module.exports = router;


Artificial Intelligence Data Analysis System


The AI analysis system processes collected sensor data to provide intelligent insights, trend analysis, and predictive capabilities. The system implements machine learning algorithms for pattern recognition and anomaly detection.


Javascript


// routes/analytics.js - AI-powered data analysis routes

const express = require('express');

const SensorData = require('../models/SensorData');

const Device = require('../models/Device');

const { logger } = require('../utils/logger');

const { performAIAnalysis } = require('../services/aiAnalysis');


const router = express.Router();


// AI-powered area analysis

router.get('/ai-analysis', async (req, res) => {

    try {

        const {

            latitude,

            longitude,

            radius = 2000,

            analysis_type = 'comprehensive'

        } = req.query;


        if (!latitude || !longitude) {

            return res.status(400).json({

                success: false,

                error: 'Latitude and longitude are required'

            });

        }


        const lat = parseFloat(latitude);

        const lng = parseFloat(longitude);

        const radiusMeters = parseInt(radius);


        // Get recent data for AI analysis

        const endTime = new Date();

        const startTime = new Date(endTime.getTime() - 7 * 24 * 60 * 60 * 1000); // Last 7 days


        // Retrieve raw sensor data for analysis

        const sensorData = await SensorData.find({

            location: {

                $geoWithin: {

                    $centerSphere: [[lng, lat], radiusMeters / 6378100]

                }

            },

            timestamp: {

                $gte: startTime,

                $lte: endTime

            }

        }).sort({ timestamp: -1 }).limit(1000);


        if (sensorData.length === 0) {

            return res.json({

                success: true,

                message: 'Insufficient data for AI analysis',

                analysis: null

            });

        }


        // Perform AI analysis

        const aiAnalysis = await performAIAnalysis(sensorData, {

            analysisType: analysis_type,

            location: { latitude: lat, longitude: lng },

            radius: radiusMeters

        });


        res.json({

            success: true,

            analysis: aiAnalysis,

            dataPoints: sensorData.length,

            analysisTimestamp: new Date()

        });


    } catch (error) {

        logger.error('AI analysis error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to perform AI analysis'

        });

    }

});


// Trend analysis for specific location

router.get('/trends', async (req, res) => {

    try {

        const {

            latitude,

            longitude,

            radius = 1000,

            period = '30d'

        } = req.query;


        if (!latitude || !longitude) {

            return res.status(400).json({

                success: false,

                error: 'Latitude and longitude are required'

            });

        }


        const lat = parseFloat(latitude);

        const lng = parseFloat(longitude);

        const radiusMeters = parseInt(radius);


        // Calculate time range based on period

        const endTime = new Date();

        let startTime;

        switch (period) {

            case '7d':

                startTime = new Date(endTime.getTime() - 7 * 24 * 60 * 60 * 1000);

                break;

            case '30d':

                startTime = new Date(endTime.getTime() - 30 * 24 * 60 * 60 * 1000);

                break;

            case '90d':

                startTime = new Date(endTime.getTime() - 90 * 24 * 60 * 60 * 1000);

                break;

            default:

                startTime = new Date(endTime.getTime() - 30 * 24 * 60 * 60 * 1000);

        }


        // Aggregate data by day for trend analysis

        const trendData = await SensorData.aggregate([

            {

                $match: {

                    location: {

                        $geoWithin: {

                            $centerSphere: [[lng, lat], radiusMeters / 6378100]

                        }

                    },

                    timestamp: {

                        $gte: startTime,

                        $lte: endTime

                    }

                }

            },

            {

                $group: {

                    _id: {

                        $dateToString: { format: '%Y-%m-%d', date: '$timestamp' }

                    },

                    averageAirQuality: { $avg: '$measurements.airQualityPPM' },

                    minAirQuality: { $min: '$measurements.airQualityPPM' },

                    maxAirQuality: { $max: '$measurements.airQualityPPM' },

                    sampleCount: { $sum: 1 }

                }

            },

            {

                $sort: { _id: 1 }

            }

        ]);


        // Calculate trend statistics

        const trendAnalysis = calculateTrendStatistics(trendData);


        res.json({

            success: true,

            location: {

                latitude: lat,

                longitude: lng,

                radius: radiusMeters

            },

            period: period,

            trendData: trendData,

            trendAnalysis: trendAnalysis

        });


    } catch (error) {

        logger.error('Trend analysis error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to perform trend analysis'

        });

    }

});


// Anomaly detection

router.get('/anomalies', async (req, res) => {

    try {

        const {

            latitude,

            longitude,

            radius = 1000,

            sensitivity = 'medium'

        } = req.query;


        if (!latitude || !longitude) {

            return res.status(400).json({

                success: false,

                error: 'Latitude and longitude are required'

            });

        }


        const lat = parseFloat(latitude);

        const lng = parseFloat(longitude);

        const radiusMeters = parseInt(radius);


        // Get recent data for anomaly detection

        const endTime = new Date();

        const startTime = new Date(endTime.getTime() - 24 * 60 * 60 * 1000); // Last 24 hours


        const recentData = await SensorData.find({

            location: {

                $geoWithin: {

                    $centerSphere: [[lng, lat], radiusMeters / 6378100]

                }

            },

            timestamp: {

                $gte: startTime,

                $lte: endTime

            }

        }).sort({ timestamp: -1 });


        // Detect anomalies using statistical methods

        const anomalies = detectAnomalies(recentData, sensitivity);


        res.json({

            success: true,

            location: {

                latitude: lat,

                longitude: lng,

                radius: radiusMeters

            },

            timeRange: {

                startTime: startTime,

                endTime: endTime

            },

            anomalies: anomalies,

            sensitivity: sensitivity

        });


    } catch (error) {

        logger.error('Anomaly detection error:', error);

        res.status(500).json({

            success: false,

            error: 'Failed to detect anomalies'

        });

    }

});


// Helper functions for analysis

function calculateTrendStatistics(trendData) {

    if (trendData.length < 2) {

        return {

            trend: 'insufficient_data',

            changeRate: 0,

            volatility: 0

        };

    }


    const values = trendData.map(d => d.averageAirQuality);

    const firstValue = values[0];

    const lastValue = values[values.length - 1];

    

    // Calculate overall trend

    const changeRate = ((lastValue - firstValue) / firstValue) * 100;

    

    // Calculate volatility (standard deviation)

    const mean = values.reduce((sum, val) => sum + val, 0) / values.length;

    const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;

    const volatility = Math.sqrt(variance);

    

    let trend;

    if (Math.abs(changeRate) < 5) {

        trend = 'stable';

    } else if (changeRate > 0) {

        trend = 'increasing';

    } else {

        trend = 'decreasing';

    }

    

    return {

        trend: trend,

        changeRate: Math.round(changeRate * 100) / 100,

        volatility: Math.round(volatility * 100) / 100,

        mean: Math.round(mean * 100) / 100

    };

}


function detectAnomalies(data, sensitivity) {

    if (data.length < 10) {

        return [];

    }


    const values = data.map(d => d.measurements.airQualityPPM);

    const mean = values.reduce((sum, val) => sum + val, 0) / values.length;

    const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;

    const stdDev = Math.sqrt(variance);

    

    // Set threshold based on sensitivity

    let threshold;

    switch (sensitivity) {

        case 'low':

            threshold = 3;

            break;

        case 'medium':

            threshold = 2.5;

            break;

        case 'high':

            threshold = 2;

            break;

        default:

            threshold = 2.5;

    }

    

    const anomalies = [];

    

    data.forEach((point, index) => {

        const value = point.measurements.airQualityPPM;

        const zScore = Math.abs((value - mean) / stdDev);

        

        if (zScore > threshold) {

            anomalies.push({

                timestamp: point.timestamp,

                deviceId: point.deviceId,

                value: value,

                zScore: Math.round(zScore * 100) / 100,

                severity: zScore > 3 ? 'high' : 'medium',

                location: point.location

            });

        }

    });

    

    return anomalies.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

}


module.exports = router;


AI Analysis Service Implementation


The AI analysis service implements machine learning algorithms for pattern recognition, prediction, and intelligent data interpretation.


Javascript


// services/aiAnalysis.js - AI analysis implementation

const tf = require('@tensorflow/tfjs-node');

const { logger } = require('../utils/logger');


class AIAnalysisService {

    constructor() {

        this.model = null;

        this.isModelLoaded = false;

    }


    async initializeModel() {

        try {

            // In a production environment, load a pre-trained model

            // For this example, we'll create a simple neural network

            this.model = tf.sequential({

                layers: [

                    tf.layers.dense({ inputShape: [4], units: 16, activation: 'relu' }),

                    tf.layers.dense({ units: 8, activation: 'relu' }),

                    tf.layers.dense({ units: 1, activation: 'linear' })

                ]

            });


            this.model.compile({

                optimizer: 'adam',

                loss: 'meanSquaredError',

                metrics: ['mae']

            });


            this.isModelLoaded = true;

            logger.info('AI model initialized successfully');

        } catch (error) {

            logger.error('Failed to initialize AI model:', error);

        }

    }


    async performAIAnalysis(sensorData, options = {}) {

        try {

            if (!this.isModelLoaded) {

                await this.initializeModel();

            }


            const analysis = {

                summary: await this.generateSummary(sensorData),

                patterns: await this.detectPatterns(sensorData),

                predictions: await this.generatePredictions(sensorData),

                recommendations: await this.generateRecommendations(sensorData),

                riskAssessment: await this.assessRisk(sensorData)

            };


            return analysis;

        } catch (error) {

            logger.error('AI analysis failed:', error);

            throw error;

        }

    }


    async generateSummary(sensorData) {

        const values = sensorData.map(d => d.measurements.airQualityPPM);

        const timestamps = sensorData.map(d => new Date(d.timestamp));

        

        const summary = {

            totalDataPoints: values.length,

            timeSpan: {

                start: Math.min(...timestamps),

                end: Math.max(...timestamps)

            },

            statistics: {

                mean: this.calculateMean(values),

                median: this.calculateMedian(values),

                standardDeviation: this.calculateStandardDeviation(values),

                minimum: Math.min(...values),

                maximum: Math.max(...values)

            },

            airQualityLevel: this.categorizeAirQuality(this.calculateMean(values))

        };


        return summary;

    }


    async detectPatterns(sensorData) {

        const patterns = {

            dailyPattern: await this.analyzeDailyPattern(sensorData),

            weeklyPattern: await this.analyzeWeeklyPattern(sensorData),

            seasonalTrend: await this.analyzeSeasonalTrend(sensorData),

            correlations: await this.analyzeCorrelations(sensorData)

        };


        return patterns;

    }


    async analyzeDailyPattern(sensorData) {

        const hourlyData = {};

        

        sensorData.forEach(point => {

            const hour = new Date(point.timestamp).getHours();

            if (!hourlyData[hour]) {

                hourlyData[hour] = [];

            }

            hourlyData[hour].push(point.measurements.airQualityPPM);

        });


        const hourlyAverages = {};

        for (let hour = 0; hour < 24; hour++) {

            if (hourlyData[hour] && hourlyData[hour].length > 0) {

                hourlyAverages[hour] = this.calculateMean(hourlyData[hour]);

            } else {

                hourlyAverages[hour] = null;

            }

        }


        // Identify peak pollution hours

        const validHours = Object.entries(hourlyAverages)

            .filter(([hour, avg]) => avg !== null)

            .map(([hour, avg]) => ({ hour: parseInt(hour), average: avg }));


        const peakHour = validHours.reduce((max, current) => 

            current.average > max.average ? current : max, validHours[0]);


        const lowHour = validHours.reduce((min, current) => 

            current.average < min.average ? current : min, validHours[0]);


        return {

            hourlyAverages: hourlyAverages,

            peakPollutionHour: peakHour,

            lowestPollutionHour: lowHour,

            dailyVariation: peakHour.average - lowHour.average

        };

    }


    async analyzeWeeklyPattern(sensorData) {

        const weeklyData = {};

        const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

        

        sensorData.forEach(point => {

            const dayOfWeek = new Date(point.timestamp).getDay();

            if (!weeklyData[dayOfWeek]) {

                weeklyData[dayOfWeek] = [];

            }

            weeklyData[dayOfWeek].push(point.measurements.airQualityPPM);

        });


        const weeklyAverages = {};

        for (let day = 0; day < 7; day++) {

            if (weeklyData[day] && weeklyData[day].length > 0) {

                weeklyAverages[dayNames[day]] = this.calculateMean(weeklyData[day]);

            }

        }


        return {

            weeklyAverages: weeklyAverages,

            weekendVsWeekday: this.compareWeekendVsWeekday(weeklyData)

        };

    }


    async analyzeSeasonalTrend(sensorData) {

        // Group data by month

        const monthlyData = {};

        

        sensorData.forEach(point => {

            const month = new Date(point.timestamp).getMonth();

            if (!monthlyData[month]) {

                monthlyData[month] = [];

            }

            monthlyData[month].push(point.measurements.airQualityPPM);

        });


        const monthlyAverages = {};

        const monthNames = [

            'January', 'February', 'March', 'April', 'May', 'June',

            'July', 'August', 'September', 'October', 'November', 'December'

        ];


        for (let month = 0; month < 12; month++) {

            if (monthlyData[month] && monthlyData[month].length > 0) {

                monthlyAverages[monthNames[month]] = this.calculateMean(monthlyData[month]);

            }

        }


        return {

            monthlyAverages: monthlyAverages,

            seasonalVariation: this.calculateSeasonalVariation(monthlyAverages)

        };

    }


    async analyzeCorrelations(sensorData) {

        // Analyze correlations between air quality and other factors

        const correlations = {};

        

        if (sensorData.some(d => d.measurements.temperature !== undefined)) {

            correlations.temperatureCorrelation = this.calculateCorrelation(

                sensorData.map(d => d.measurements.airQualityPPM),

                sensorData.map(d => d.measurements.temperature).filter(t => t !== undefined)

            );

        }


        if (sensorData.some(d => d.measurements.humidity !== undefined)) {

            correlations.humidityCorrelation = this.calculateCorrelation(

                sensorData.map(d => d.measurements.airQualityPPM),

                sensorData.map(d => d.measurements.humidity).filter(h => h !== undefined)

            );

        }


        return correlations;

    }


    async generatePredictions(sensorData) {

        if (sensorData.length < 10) {

            return {

                error: 'Insufficient data for predictions',

                predictions: null

            };

        }


        // Simple time series prediction using moving average

        const values = sensorData.map(d => d.measurements.airQualityPPM);

        const recentValues = values.slice(-10); // Last 10 values

        const movingAverage = this.calculateMean(recentValues);

        

        // Calculate trend

        const firstHalf = recentValues.slice(0, 5);

        const secondHalf = recentValues.slice(5);

        const trend = this.calculateMean(secondHalf) - this.calculateMean(firstHalf);


        const predictions = [];

        for (let i = 1; i <= 6; i++) { // Predict next 6 hours

            const predictedValue = movingAverage + (trend * i);

            predictions.push({

                hoursAhead: i,

                predictedValue: Math.max(0, predictedValue),

                confidence: Math.max(0.1, 1 - (i * 0.15)) // Decreasing confidence

            });

        }


        return {

            method: 'moving_average_with_trend',

            predictions: predictions,

            baseValue: movingAverage,

            trend: trend

        };

    }


    async generateRecommendations(sensorData) {

        const currentLevel = this.calculateMean(

            sensorData.slice(-5).map(d => d.measurements.airQualityPPM)

        );

        

        const recommendations = [];


        if (currentLevel > 150) {

            recommendations.push({

                priority: 'high',

                category: 'health',

                message: 'Air quality is unhealthy. Avoid outdoor activities and keep windows closed.'

            });

            recommendations.push({

                priority: 'high',

                category: 'action',

                message: 'Consider using air purifiers indoors and wearing masks when going outside.'

            });

        } else if (currentLevel > 100) {

            recommendations.push({

                priority: 'medium',

                category: 'health',

                message: 'Air quality is moderate. Sensitive individuals should limit outdoor activities.'

            });

        } else {

            recommendations.push({

                priority: 'low',

                category: 'health',

                message: 'Air quality is good. Safe for all outdoor activities.'

            });

        }


        // Add monitoring recommendations

        if (sensorData.length < 50) {

            recommendations.push({

                priority: 'medium',

                category: 'monitoring',

                message: 'More data collection recommended for better analysis accuracy.'

            });

        }


        return recommendations;

    }


    async assessRisk(sensorData) {

        const values = sensorData.map(d => d.measurements.airQualityPPM);

        const currentLevel = this.calculateMean(values.slice(-5));

        const maxLevel = Math.max(...values);

        const volatility = this.calculateStandardDeviation(values);


        let riskLevel;

        let riskScore = 0;


        // Base risk on current level

        if (currentLevel > 200) riskScore += 40;

        else if (currentLevel > 150) riskScore += 30;

        else if (currentLevel > 100) riskScore += 20;

        else if (currentLevel > 50) riskScore += 10;


        // Add risk based on maximum observed level

        if (maxLevel > 300) riskScore += 30;

        else if (maxLevel > 200) riskScore += 20;

        else if (maxLevel > 150) riskScore += 10;


        // Add risk based on volatility

        if (volatility > 50) riskScore += 20;

        else if (volatility > 30) riskScore += 10;


        // Determine risk level

        if (riskScore >= 70) riskLevel = 'very_high';

        else if (riskScore >= 50) riskLevel = 'high';

        else if (riskScore >= 30) riskLevel = 'medium';

        else if (riskScore >= 15) riskLevel = 'low';

        else riskLevel = 'very_low';


        return {

            riskLevel: riskLevel,

            riskScore: riskScore,

            factors: {

                currentLevel: currentLevel,

                maxLevel: maxLevel,

                volatility: volatility

            }

        };

    }


    // Utility methods

    calculateMean(values) {

        return values.reduce((sum, val) => sum + val, 0) / values.length;

    }


    calculateMedian(values) {

        const sorted = values.slice().sort((a, b) => a - b);

        const middle = Math.floor(sorted.length / 2);

        

        if (sorted.length % 2 === 0) {

            return (sorted[middle - 1] + sorted[middle]) / 2;

        } else {

            return sorted[middle];

        }

    }


    calculateStandardDeviation(values) {

        const mean = this.calculateMean(values);

        const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;

        return Math.sqrt(variance);

    }


    calculateCorrelation(x, y) {

        if (x.length !== y.length || x.length === 0) return 0;

        

        const meanX = this.calculateMean(x);

        const meanY = this.calculateMean(y);

        

        let numerator = 0;

        let denomX = 0;

        let denomY = 0;

        

        for (let i = 0; i < x.length; i++) {

            const deltaX = x[i] - meanX;

            const deltaY = y[i] - meanY;

            

            numerator += deltaX * deltaY;

            denomX += deltaX * deltaX;

            denomY += deltaY * deltaY;

        }

        

        const denominator = Math.sqrt(denomX * denomY);

        return denominator === 0 ? 0 : numerator / denominator;

    }


    categorizeAirQuality(ppm) {

        if (ppm <= 50) return 'Good';

        if (ppm <= 100) return 'Moderate';

        if (ppm <= 150) return 'Unhealthy for Sensitive Groups';

        if (ppm <= 200) return 'Unhealthy';

        if (ppm <= 300) return 'Very Unhealthy';

        return 'Hazardous';

    }


    compareWeekendVsWeekday(weeklyData) {

        const weekdayData = [];

        const weekendData = [];

        

        for (let day = 0; day < 7; day++) {

            if (weeklyData[day]) {

                if (day === 0 || day === 6) { // Sunday or Saturday

                    weekendData.push(...weeklyData[day]);

                } else {

                    weekdayData.push(...weeklyData[day]);

                }

            }

        }

        

        return {

            weekdayAverage: weekdayData.length > 0 ? this.calculateMean(weekdayData) : null,

            weekendAverage: weekendData.length > 0 ? this.calculateMean(weekendData) : null,

            difference: weekdayData.length > 0 && weekendData.length > 0 ? 

                       this.calculateMean(weekdayData) - this.calculateMean(weekendData) : null

        };

    }


    calculateSeasonalVariation(monthlyAverages) {

        const values = Object.values(monthlyAverages).filter(v => v !== undefined);

        if (values.length === 0) return null;

        

        return {

            highestMonth: Object.entries(monthlyAverages).reduce((max, [month, avg]) => 

                avg > max.average ? { month, average: avg } : max, 

                { month: '', average: -Infinity }),

            lowestMonth: Object.entries(monthlyAverages).reduce((min, [month, avg]) => 

                avg < min.average ? { month, average: avg } : min, 

                { month: '', average: Infinity }),

            seasonalRange: Math.max(...values) - Math.min(...values)

        };

    }

}


// Export singleton instance

const aiAnalysisService = new AIAnalysisService();


async function performAIAnalysis(sensorData, options) {

    return await aiAnalysisService.performAIAnalysis(sensorData, options);

}


module.exports = {

    performAIAnalysis,

    AIAnalysisService

};


System Configuration and Deployment


Database Configuration


The database configuration establishes connection parameters, indexing strategies, and performance optimization settings for MongoDB.


Javascript


// config/database.js - Database configuration

const mongoose = require('mongoose');

const { logger } = require('../utils/logger');


const connectDatabase = async () => {

    try {

        const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/air_quality_monitoring';

        

        const options = {

            useNewUrlParser: true,

            useUnifiedTopology: true,

            maxPoolSize: 10,

            serverSelectionTimeoutMS: 5000,

            socketTimeoutMS: 45000,

            family: 4,

            bufferCommands: false,

            bufferMaxEntries: 0

        };


        await mongoose.connect(mongoURI, options);

        

        // Set up connection event handlers

        mongoose.connection.on('connected', () => {

            logger.info('MongoDB connected successfully');

        });


        mongoose.connection.on('error', (error) => {

            logger.error('MongoDB connection error:', error);

        });


        mongoose.connection.on('disconnected', () => {

            logger.warn('MongoDB disconnected');

        });


        // Graceful shutdown

        process.on('SIGINT', async () => {

            await mongoose.connection.close();

            logger.info('MongoDB connection closed through app termination');

            process.exit(0);

        });


    } catch (error) {

        logger.error('Database connection failed:', error);

        throw error;

    }

};


module.exports = { connectDatabase };


Validation Middleware


The validation middleware ensures data integrity and security by validating incoming requests from sensor devices and user queries.


Javascript


// middleware/validation.js - Request validation middleware

const { body, query, validationResult } = require('express-validator');

const { logger } = require('../utils/logger');


// Device registration validation

const validateDeviceRegistration = [

    body('device_id')

        .isLength({ min: 8, max: 64 })

        .matches(/^[A-Za-z0-9]+$/)

        .withMessage('Device ID must be 8-64 alphanumeric characters'),

    

    body('device_type')

        .optional()

        .isIn(['air_quality_sensor', 'weather_station', 'noise_monitor'])

        .withMessage('Invalid device type'),

    

    body('latitude')

        .isFloat({ min: -90, max: 90 })

        .withMessage('Latitude must be between -90 and 90'),

    

    body('longitude')

        .isFloat({ min: -180, max: 180 })

        .withMessage('Longitude must be between -180 and 180'),

    

    body('sensor_model')

        .isLength({ min: 2, max: 50 })

        .withMessage('Sensor model must be 2-50 characters'),

    

    body('firmware_version')

        .matches(/^\d+\.\d+\.\d+$/)

        .withMessage('Firmware version must be in format x.y.z'),

    

    (req, res, next) => {

        const errors = validationResult(req);

        if (!errors.isEmpty()) {

            logger.warn('Device registration validation failed:', errors.array());

            return res.status(400).json({

                success: false,

                error: 'Validation failed',

                details: errors.array()

            });

        }

        next();

    }

];


// Sensor data validation

const validateSensorData = [

    body('device_id')

        .isLength({ min: 8, max: 64 })

        .matches(/^[A-Za-z0-9]+$/)

        .withMessage('Invalid device ID'),

    

    body('air_quality_ppm')

        .isFloat({ min: 0, max: 10000 })

        .withMessage('Air quality PPM must be between 0 and 10000'),

    

    body('latitude')

        .isFloat({ min: -90, max: 90 })

        .withMessage('Invalid latitude'),

    

    body('longitude')

        .isFloat({ min: -180, max: 180 })

        .withMessage('Invalid longitude'),

    

    body('timestamp')

        .optional()

        .isISO8601()

        .withMessage('Invalid timestamp format'),

    

    body('temperature')

        .optional()

        .isFloat({ min: -50, max: 100 })

        .withMessage('Temperature must be between -50 and 100 Celsius'),

    

    body('humidity')

        .optional()

        .isFloat({ min: 0, max: 100 })

        .withMessage('Humidity must be between 0 and 100 percent'),

    

    body('sensor_type')

        .optional()

        .isIn(['MQ135', 'MQ7', 'BME280', 'DHT22'])

        .withMessage('Invalid sensor type'),

    

    (req, res, next) => {

        const errors = validationResult(req);

        if (!errors.isEmpty()) {

            logger.warn('Sensor data validation failed:', errors.array());

            return res.status(400).json({

                success: false,

                error: 'Invalid sensor data',

                details: errors.array()

            });

        }

        next();

    }

];


module.exports = {

    validateDeviceRegistration,

    validateSensorData

};


Utility Functions


The utility functions provide common functionality for air quality calculations, logging, and system operations.


Javascript


// utils/airQuality.js - Air quality calculation utilities

function calculateAirQualityIndex(ppm) {

    // Convert PPM to AQI using EPA standards (simplified)

    if (ppm <= 12) return Math.round((50 / 12) * ppm);

    if (ppm <= 35.4) return Math.round(((100 - 51) / (35.4 - 12.1)) * (ppm - 12.1) + 51);

    if (ppm <= 55.4) return Math.round(((150 - 101) / (55.4 - 35.5)) * (ppm - 35.5) + 101);

    if (ppm <= 150.4) return Math.round(((200 - 151) / (150.4 - 55.5)) * (ppm - 55.5) + 151);

    if (ppm <= 250.4) return Math.round(((300 - 201) / (250.4 - 150.5)) * (ppm - 150.5) + 201);

    return Math.round(((500 - 301) / (500.4 - 250.5)) * (ppm - 250.5) + 301);

}


function getAirQualityCategory(aqi) {

    if (aqi <= 50) return 'Good';

    if (aqi <= 100) return 'Moderate';

    if (aqi <= 150) return 'Unhealthy for Sensitive Groups';

    if (aqi <= 200) return 'Unhealthy';

    if (aqi <= 300) return 'Very Unhealthy';

    return 'Hazardous';

}


function getHealthRecommendation(aqi) {

    if (aqi <= 50) {

        return 'Air quality is satisfactory, and air pollution poses little or no risk.';

    } else if (aqi <= 100) {

        return 'Air quality is acceptable. However, there may be a risk for some people, particularly those who are unusually sensitive to air pollution.';

    } else if (aqi <= 150) {

        return 'Members of sensitive groups may experience health effects. The general public is less likely to be affected.';

    } else if (aqi <= 200) {

        return 'Some members of the general public may experience health effects; members of sensitive groups may experience more serious health effects.';

    } else if (aqi <= 300) {

        return 'Health alert: The risk of health effects is increased for everyone.';

    } else {

        return 'Health warning of emergency conditions: everyone is more likely to be affected.';

    }

}


module.exports = {

    calculateAirQualityIndex,

    getAirQualityCategory,

    getHealthRecommendation

};



Javascript


// utils/logger.js - Logging utility

const winston = require('winston');


const logger = winston.createLogger({

    level: process.env.LOG_LEVEL || 'info',

    format: winston.format.combine(

        winston.format.timestamp(),

        winston.format.errors({ stack: true }),

        winston.format.json()

    ),

    defaultMeta: { service: 'air-quality-server' },

    transports: [

        new winston.transports.File({ 

            filename: 'logs/error.log', 

            level: 'error',

            maxsize: 5242880, // 5MB

            maxFiles: 5

        }),

        new winston.transports.File({ 

            filename: 'logs/combined.log',

            maxsize: 5242880, // 5MB

            maxFiles: 5

        })

    ]

});


if (process.env.NODE_ENV !== 'production') {

    logger.add(new winston.transports.Console({

        format: winston.format.combine(

            winston.format.colorize(),

            winston.format.simple()

        )

    }));

}


module.exports = { logger };



Complete Working Example


This section provides a complete, deployable example of the IoT air pollution monitoring system including all necessary configuration files and deployment scripts.


ESP32 Complete Firmware (C++)


// complete_esp32_firmware.ino - Complete ESP32 air quality sensor firmware

#include <WiFi.h>

#include <HTTPClient.h>

#include <ArduinoJson.h>

#include <EEPROM.h>

#include <esp_system.h>


// Configuration constants - Update these for your deployment

const char* WIFI_SSID = "YourWiFiNetwork";

const char* WIFI_PASSWORD = "YourWiFiPassword";

const char* SERVER_URL = "http://your-server.com/api/data/sensor-data";

const char* REGISTRATION_URL = "http://your-server.com/api/devices/register";


// Hardware configuration

const int SENSOR_PIN = 34;

const int LED_PIN = 2;

const int MEASUREMENT_INTERVAL = 600000; // 10 minutes

const int MAX_RETRY_ATTEMPTS = 3;

const int WIFI_TIMEOUT = 15000;

const int SENSOR_WARMUP_TIME = 30000;


// Device location (in production, use GPS module)

const float DEVICE_LATITUDE = 40.7128;   // New York City example

const float DEVICE_LONGITUDE = -74.0060;


// Global variables

String deviceUUID;

unsigned long lastMeasurementTime = 0;

bool systemInitialized = false;


// Sensor calibration constants

const float RLOAD = 10.0;

const float RZERO = 76.63;

const float PARA = 116.6020682;

const float PARB = 2.769034857;


class SystemManager {

private:

    bool wifiConnected;

    int connectionAttempts;

    

public:

    SystemManager() : wifiConnected(false), connectionAttempts(0) {}

    

    void initialize() {

        Serial.begin(115200);

        Serial.println("\n=== ESP32 Air Quality Sensor System ===");

        Serial.println("Firmware Version: 1.0.0");

        Serial.println("Initializing system components...");

        

        // Initialize hardware

        pinMode(SENSOR_PIN, INPUT);

        pinMode(LED_PIN, OUTPUT);

        digitalWrite(LED_PIN, LOW);

        

        // Initialize EEPROM

        EEPROM.begin(512);

        

        // Generate or load device UUID

        deviceUUID = getOrCreateDeviceUUID();

        Serial.println("Device UUID: " + deviceUUID);

        

        // Initialize WiFi

        if (initializeWiFi()) {

            Serial.println("WiFi initialization successful");

            registerDevice();

            warmupSensor();

            systemInitialized = true;

            digitalWrite(LED_PIN, HIGH); // Indicate ready state

        } else {

            Serial.println("WiFi initialization failed - entering deep sleep");

            enterDeepSleep();

        }

    }

    

    bool initializeWiFi() {

        WiFi.mode(WIFI_STA);

        WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

        

        Serial.print("Connecting to WiFi");

        unsigned long startTime = millis();

        

        while (WiFi.status() != WL_CONNECTED && 

               (millis() - startTime) < WIFI_TIMEOUT) {

            delay(500);

            Serial.print(".");

        }

        

        if (WiFi.status() == WL_CONNECTED) {

            Serial.println("\nWiFi connected successfully");

            Serial.print("IP Address: ");

            Serial.println(WiFi.localIP());

            Serial.print("Signal Strength: ");

            Serial.print(WiFi.RSSI());

            Serial.println(" dBm");

            wifiConnected = true;

            connectionAttempts = 0;

            return true;

        } else {

            Serial.println("\nWiFi connection failed");

            connectionAttempts++;

            wifiConnected = false;

            return false;

        }

    }

    

    bool ensureWiFiConnection() {

        if (WiFi.status() != WL_CONNECTED) {

            Serial.println("WiFi disconnected, attempting reconnection...");

            return initializeWiFi();

        }

        return true;

    }

    

    void registerDevice() {

        Serial.println("Registering device with server...");

        

        HTTPClient http;

        http.begin(REGISTRATION_URL);

        http.addHeader("Content-Type", "application/json");

        http.setTimeout(15000);

        

        DynamicJsonDocument doc(512);

        doc["device_id"] = deviceUUID;

        doc["device_type"] = "air_quality_sensor";

        doc["latitude"] = DEVICE_LATITUDE;

        doc["longitude"] = DEVICE_LONGITUDE;

        doc["sensor_model"] = "MQ135";

        doc["firmware_version"] = "1.0.0";

        

        String jsonString;

        serializeJson(doc, jsonString);

        

        int httpResponseCode = http.POST(jsonString);

        

        if (httpResponseCode >= 200 && httpResponseCode < 300) {

            Serial.println("Device registration successful");

        } else {

            Serial.print("Device registration failed. HTTP Code: ");

            Serial.println(httpResponseCode);

            Serial.println("Continuing with measurements...");

        }

        

        http.end();

    }

    

    void warmupSensor() {

        Serial.println("Warming up air quality sensor...");

        unsigned long startTime = millis();

        

        while ((millis() - startTime) < SENSOR_WARMUP_TIME) {

            unsigned long elapsed = (millis() - startTime) / 1000;

            Serial.print("Sensor warmup: ");

            Serial.print(elapsed);

            Serial.print("/");

            Serial.print(SENSOR_WARMUP_TIME / 1000);

            Serial.println(" seconds");

            

            // Blink LED during warmup

            digitalWrite(LED_PIN, !digitalRead(LED_PIN));

            delay(1000);

        }

        

        digitalWrite(LED_PIN, HIGH);

        Serial.println("Sensor warmup complete");

    }

    

    void enterDeepSleep() {

        Serial.println("Entering deep sleep mode...");

        digitalWrite(LED_PIN, LOW);

        esp_deep_sleep(MEASUREMENT_INTERVAL * 1000ULL);

    }

    

    bool isSystemReady() {

        return systemInitialized && wifiConnected;

    }

    

private:

    String getOrCreateDeviceUUID() {

        String uuid = loadUUIDFromEEPROM();

        if (uuid.length() == 0) {

            uuid = generateUUID();

            saveUUIDToEEPROM(uuid);

            Serial.println("Generated new device UUID");

        } else {

            Serial.println("Loaded existing UUID from EEPROM");

        }

        return uuid;

    }

    

    String generateUUID() {

        uint64_t chipId = ESP.getEfuseMac();

        String uuid = String((uint32_t)(chipId >> 32), HEX) + 

                     String((uint32_t)chipId, HEX) + 

                     String(random(0x10000000, 0xFFFFFFFF), HEX);

        uuid.toUpperCase();

        return uuid;

    }

    

    void saveUUIDToEEPROM(const String& uuid) {

        for (int i = 0; i < uuid.length() && i < 32; i++) {

            EEPROM.write(i, uuid[i]);

        }

        EEPROM.write(uuid.length(), '\0');

        EEPROM.commit();

    }

    

    String loadUUIDFromEEPROM() {

        String uuid = "";

        char ch;

        for (int i = 0; i < 32; i++) {

            ch = EEPROM.read(i);

            if (ch == '\0') break;

            uuid += ch;

        }

        return uuid;

    }

};


class AirQualitySensor {

private:

    int pin;

    bool isWarmedUp;

    

public:

    AirQualitySensor(int sensorPin) : pin(sensorPin), isWarmedUp(true) {}

    

    float readAirQualityPPM() {

        const int numReadings = 10;

        float totalResistance = 0.0;

        

        // Take multiple readings for averaging

        for (int i = 0; i < numReadings; i++) {

            int analogValue = analogRead(pin);

            float voltage = (analogValue / 4095.0) * 3.3;

            float resistance = calculateResistance(voltage);

            

            if (resistance > 0) {

                totalResistance += resistance;

            }

            

            delay(100);

        }

        

        float avgResistance = totalResistance / numReadings;

        float ratio = avgResistance / RZERO;

        float ppm = PARA * pow(ratio, -PARB);

        

        // Sanity check

        if (ppm < 0 || ppm > 1000) {

            Serial.println("Warning: Sensor reading out of expected range");

            return -1.0;

        }

        

        return ppm;

    }

    

private:

    float calculateResistance(float voltage) {

        if (voltage <= 0.0 || voltage >= 3.3) {

            return 0.0;

        }

        return ((3.3 - voltage) / voltage) * RLOAD;

    }

};


class DataTransmitter {

private:

    String serverURL;

    

public:

    DataTransmitter(const String& url) : serverURL(url) {}

    

    bool transmitData(const String& deviceId, float ppm, float lat, float lng) {

        if (ppm < 0) {

            Serial.println("Invalid sensor reading - skipping transmission");

            return false;

        }

        

        HTTPClient http;

        http.begin(serverURL);

        http.addHeader("Content-Type", "application/json");

        http.addHeader("User-Agent", "ESP32-AirQuality/1.0");

        http.setTimeout(20000);

        

        DynamicJsonDocument doc(1024);

        doc["device_id"] = deviceId;

        doc["timestamp"] = getCurrentTimestamp();

        doc["air_quality_ppm"] = ppm;

        doc["latitude"] = lat;

        doc["longitude"] = lng;

        doc["sensor_type"] = "MQ135";

        doc["firmware_version"] = "1.0.0";

        

        String jsonString;

        serializeJson(doc, jsonString);

        

        Serial.println("Transmitting data to server:");

        Serial.println(jsonString);

        

        int httpResponseCode = http.POST(jsonString);

        

        if (httpResponseCode >= 200 && httpResponseCode < 300) {

            String response = http.getString();

            Serial.print("Transmission successful. Response: ");

            Serial.println(response);

            http.end();

            return true;

        } else {

            Serial.print("Transmission failed. HTTP Code: ");

            Serial.println(httpResponseCode);

            if (httpResponseCode > 0) {

                Serial.print("Error response: ");

                Serial.println(http.getString());

            }

            http.end();

            return false;

        }

    }

    

private:

    String getCurrentTimestamp() {

        // In production, sync with NTP server

        return String(millis());

    }

};


// Global objects

SystemManager systemManager;

AirQualitySensor airSensor(SENSOR_PIN);

DataTransmitter dataTransmitter(SERVER_URL);


void setup() {

    // Initialize random seed

    randomSeed(analogRead(0));

    

    // Initialize system

    systemManager.initialize();

    

    // Record first measurement time

    lastMeasurementTime = millis();

    

    Serial.println("=== System Ready ===");

}


void loop() {

    unsigned long currentTime = millis();

    

    // Check if it's time for next measurement

    if (currentTime - lastMeasurementTime >= MEASUREMENT_INTERVAL) {

        

        if (!systemManager.isSystemReady()) {

            Serial.println("System not ready, skipping measurement");

            delay(5000);

            return;

        }

        

        // Ensure WiFi connection

        if (!systemManager.ensureWiFiConnection()) {

            Serial.println("WiFi connection failed, entering deep sleep");

            systemManager.enterDeepSleep();

            return;

        }

        

        // Indicate measurement in progress

        digitalWrite(LED_PIN, LOW);

        

        Serial.println("\n--- Taking Air Quality Measurement ---");

        

        // Read sensor data

        float ppmValue = airSensor.readAirQualityPPM();

        

        if (ppmValue >= 0) {

            Serial.print("Air Quality Reading: ");

            Serial.print(ppmValue, 2);

            Serial.println(" PPM");

            

            // Transmit data

            bool success = dataTransmitter.transmitData(

                deviceUUID, ppmValue, DEVICE_LATITUDE, DEVICE_LONGITUDE);

            

            if (success) {

                Serial.println("Data transmission successful");

                // Quick blink to indicate success

                for (int i = 0; i < 3; i++) {

                    digitalWrite(LED_PIN, HIGH);

                    delay(100);

                    digitalWrite(LED_PIN, LOW);

                    delay(100);

                }

            } else {

                Serial.println("Data transmission failed");

                // Longer blink to indicate failure

                for (int i = 0; i < 5; i++) {

                    digitalWrite(LED_PIN, HIGH);

                    delay(200);

                    digitalWrite(LED_PIN, LOW);

                    delay(200);

                }

            }

        } else {

            Serial.println("Invalid sensor reading - measurement skipped");

        }

        

        // Return LED to ready state

        digitalWrite(LED_PIN, HIGH);

        

        // Update last measurement time

        lastMeasurementTime = currentTime;

        

        Serial.print("Next measurement in ");

        Serial.print(MEASUREMENT_INTERVAL / 60000);

        Serial.println(" minutes");

    }

    

    // Small delay to prevent excessive CPU usage

    delay(1000);

    

    // Watchdog-style check - restart if system becomes unresponsive

    if (millis() - lastMeasurementTime > MEASUREMENT_INTERVAL * 2) {

        Serial.println("System appears unresponsive - restarting");

        ESP.restart();

    }

}



Complete Server Package Configuration


Json

// package.json - Node.js dependencies

{

  "name": "air-quality-monitoring-server",

  "version": "1.0.0",

  "description": "IoT Air Quality Monitoring System Server",

  "main": "server.js",

  "scripts": {

    "start": "node server.js",

    "dev": "nodemon server.js",

    "test": "jest",

    "lint": "eslint .",

    "setup": "node scripts/setup.js"

  },

  "dependencies": {

    "express": "^4.18.2",

    "mongoose": "^7.5.0",

    "cors": "^2.8.5",

    "helmet": "^7.0.0",

    "express-rate-limit": "^6.8.1",

    "compression": "^1.7.4",

    "express-validator": "^7.0.1",

    "winston": "^3.10.0",

    "@tensorflow/tfjs-node": "^4.10.0",

    "dotenv": "^16.3.1"

  },

  "devDependencies": {

    "nodemon": "^3.0.1",

    "jest": "^29.6.2",

    "eslint": "^8.47.0"

  },

  "engines": {

    "node": ">=16.0.0"

  }

}


Bash


#!/bin/bash

# deploy.sh - Deployment script


echo "=== Air Quality Monitoring System Deployment ==="


# Create necessary directories

mkdir -p logs

mkdir -p data

mkdir -p config


# Install dependencies

echo "Installing Node.js dependencies..."

npm install


# Set up environment variables

if [ ! -f .env ]; then

    echo "Creating environment configuration..."

    cat > .env << EOF

NODE_ENV=production

PORT=3000

MONGODB_URI=mongodb://localhost:27017/air_quality_monitoring

LOG_LEVEL=info

ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com

EOF

fi


# Create systemd service file

echo "Creating systemd service..."

sudo tee /etc/systemd/system/air-quality-server.service > /dev/null << EOF

[Unit]

Description=Air Quality Monitoring Server

After=network.target


[Service]

Type=simple

User=ubuntu

WorkingDirectory=/path/to/your/project

ExecStart=/usr/bin/node server.js

Restart=on-failure

RestartSec=10

Environment=NODE_ENV=production


[Install]

WantedBy=multi-user.target

EOF


# Enable and start service

sudo systemctl daemon-reload

sudo systemctl enable air-quality-server

sudo systemctl start air-quality-server


echo "Deployment complete!"

echo "Server status: $(sudo systemctl is-active air-quality-server)"

echo "View logs: sudo journalctl -u air-quality-server -f"


This comprehensive IoT air pollution monitoring system provides a complete solution for distributed environmental monitoring. The ESP32-based sensor nodes continuously collect air quality data and transmit it to a central server that processes the information using artificial intelligence algorithms. The system offers scalable architecture, real-time data analysis, and intelligent insights for environmental monitoring applications. Users can access aggregated data for any geographical area with configurable radius parameters, enabling effective air quality management and public health protection.