# ============================================================================= # Kynsight — Circular Buffer Kit (Improved Readability) # # Author: Kerem Yollu (improved with Claude) # License: MIT # Company: Kynsight # Created: 2025-10-24 # # WHAT IS A CIRCULAR BUFFER? # =========================== # Think of it like a tape recorder with limited tape that loops back: # # Write Position (wraps around) # ↓ # [D][E][F][A][B][C] ← Buffer (capacity = 6 bytes) # ↑ ↑ # oldest newest # # When full and you write more → oldest data gets overwritten (drop-oldest) # # KEY FEATURE: Absolute Write Offset (w_abs) # ========================================== # Unlike typical circular buffers that only track position (0-5 above), # we ALSO track total bytes ever written: # # w_abs = 0: [] (empty) # w_abs = 3: [A][B][C][ ][ ][ ] (3 bytes written) # w_abs = 6: [A][B][C][D][E][F] (6 bytes, full) # w_abs = 9: [G][H][I][D][E][F] (9 bytes total, overwrote A,B,C) # ↑ newest ↑ oldest # # WHY? To copy data spans unambiguously: # - "Give me bytes from w_abs=5 to w_abs=9" → [F][G][H][I] # - Works even if buffer wrapped around multiple times # # C EQUIVALENT: # ============= # In C, you'd use: # - uint8_t* buffer (malloc'd array) # - size_t write_pos = w_abs % capacity (for indexing) # - pthread_mutex_t lock (for thread safety) # # In Python: # - bytearray (like uint8_t* but managed memory) # - % operator works same as C # - threading.Lock (like pthread_mutex_t) # # ============================================================================= from __future__ import annotations from dataclasses import dataclass, field from enum import Enum from typing import Tuple, Optional import threading # ============================================================================= # STATUS CODES (like errno in C) # ============================================================================= class Status(Enum): """ Return codes for all operations. C equivalent: enum Status { OK=0, BAD_CONFIG=1, NOT_READY=2, OVERFLOW=3 }; Python advantage: Enum names are more readable than raw ints """ OK = 0 # Operation succeeded BAD_CONFIG = 1 # Invalid parameter (e.g., size <= 0) NOT_READY = 2 # Buffer not initialized OVERFLOW = 3 # Data larger than capacity (was truncated) # ============================================================================= # BUFFER HANDLE (like a struct in C) # ============================================================================= @dataclass class CircularBufferHandle: """ Opaque handle for the circular buffer state. C equivalent: typedef struct { size_t capacity; uint8_t* buffer; size_t write_absolute; int overflow_count; pthread_mutex_t lock; } CircularBuffer; Python @dataclass: Automatically generates __init__, __repr__, etc. Like a C struct but with methods and automatic constructor. Fields explained: ----------------- capacity: Total size in bytes (like malloc(capacity) in C) buffer: Actual storage (bytearray = Python's managed uint8_t array) write_absolute: Total bytes written since creation (monotonic, never wraps) To get position in buffer: write_absolute % capacity overflow_count: How many times we wrote more data than capacity at once (these writes get truncated to keep newest data) lock: For thread safety (like pthread_mutex_t in C) 'with h.lock:' in Python = pthread_mutex_lock/unlock in C """ capacity: int buffer: bytearray = field(default_factory=bytearray) write_absolute: int = 0 overflow_count: int = 0 lock: threading.Lock = field(default_factory=threading.Lock) # ============================================================================= # INITIALIZATION / CLEANUP # ============================================================================= def cb_init(capacity_bytes: int) -> Tuple[Status, Optional[CircularBufferHandle]]: """ Create and initialize a circular buffer. C equivalent: CircularBuffer* cb_init(size_t capacity) { if (capacity <= 0) return NULL; CircularBuffer* cb = malloc(sizeof(CircularBuffer)); cb->buffer = malloc(capacity); cb->capacity = capacity; cb->write_absolute = 0; cb->overflow_count = 0; pthread_mutex_init(&cb->lock, NULL); return cb; } Args: capacity_bytes: Size of buffer in bytes (must be > 0) Returns: (Status.OK, handle) if successful (Status.BAD_CONFIG, None) if capacity_bytes <= 0 Example: status, buffer = cb_init(1024) # 1KB buffer if status == Status.OK: # use buffer... """ # Validation if capacity_bytes <= 0: return (Status.BAD_CONFIG, None) # Create handle (like malloc + init in C) handle = CircularBufferHandle( capacity=capacity_bytes, buffer=bytearray(capacity_bytes) # Pre-allocated, zero-filled ) return (Status.OK, handle) def cb_deinit(handle: CircularBufferHandle) -> Status: """ Clean up buffer resources. C equivalent: void cb_deinit(CircularBuffer* cb) { free(cb->buffer); pthread_mutex_destroy(&cb->lock); free(cb); } Python note: Not strictly needed (garbage collector handles cleanup), but included for API consistency and explicit resource management. Args: handle: Buffer to clean up Returns: Status.OK always """ # In Python, just returning is enough (GC will clean up) # But we could explicitly clear if needed: # handle.buffer = bytearray() return Status.OK def cb_reset(handle: CircularBufferHandle) -> Status: """ Reset buffer to empty state (zero bytes written). Like flushing or clearing the buffer. C equivalent: void cb_reset(CircularBuffer* cb) { pthread_mutex_lock(&cb->lock); memset(cb->buffer, 0, cb->capacity); cb->write_absolute = 0; cb->overflow_count = 0; pthread_mutex_unlock(&cb->lock); } Effects: - Zeros all bytes in buffer - Resets write_absolute to 0 - Resets overflow_count to 0 Thread-safe: Uses internal lock Args: handle: Buffer to reset Returns: Status.OK always """ with handle.lock: # ← Python's 'with' = pthread_mutex_lock/unlock in C # Zero out the buffer handle.buffer[:] = b"\x00" * handle.capacity # Reset counters handle.write_absolute = 0 handle.overflow_count = 0 return Status.OK # ============================================================================= # WRITE OPERATION # ============================================================================= def cb_write(handle: CircularBufferHandle, data: bytes) -> Tuple[Status, int]: """ Write bytes to the buffer (drop-oldest on overflow). BEHAVIOR: --------- If data fits: Write normally If data > capacity: Keep only the NEWEST 'capacity' bytes Example with capacity=4: Initial: [ ][ ][ ][ ] Write ABC: [A][B][C][ ] → w_abs=3 Write DE: [A][B][C][D] → w_abs=4 (full) Write FG: [F][G][C][D] → w_abs=6 (overwrote A,B) ↑ newest ↑ oldest C equivalent: int cb_write(CircularBuffer* cb, const uint8_t* data, size_t len) { pthread_mutex_lock(&cb->lock); // Handle overflow if (len > cb->capacity) { data = data + (len - cb->capacity); // keep last N bytes len = cb->capacity; cb->overflow_count++; } // Calculate positions size_t pos = cb->write_absolute % cb->capacity; // Write (might wrap around) if (pos + len <= cb->capacity) { memcpy(&cb->buffer[pos], data, len); } else { size_t first_part = cb->capacity - pos; memcpy(&cb->buffer[pos], data, first_part); memcpy(&cb->buffer[0], data + first_part, len - first_part); } cb->write_absolute += len; pthread_mutex_unlock(&cb->lock); return len; } Args: handle: Buffer to write to data: Bytes to write Returns: (Status.OK, bytes_written) where bytes_written = min(len(data), capacity) Thread-safe: Yes """ # Edge case: empty write if not data: return (Status.OK, 0) with handle.lock: bytes_to_write = len(data) # OVERFLOW HANDLING: If data > capacity, keep only newest bytes if bytes_to_write > handle.capacity: # Keep last 'capacity' bytes only data = data[-handle.capacity:] # Python slice (negative index from end) bytes_to_write = handle.capacity handle.overflow_count += 1 # CALCULATE WRITE POSITION # Like: size_t pos = write_absolute % capacity in C position = handle.write_absolute % handle.capacity end_position = position + bytes_to_write # WRITE DATA (handle wrap-around) if end_position <= handle.capacity: # Case 1: Data fits without wrapping # [___ABC___] → write at position 3 handle.buffer[position:end_position] = data else: # Case 2: Data wraps around to start # [___ABC] → write "DEFGH" becomes [GH_DEF] # ↑wrap here # Split into two parts first_part_size = handle.capacity - position # bytes until end # Write first part (to end of buffer) handle.buffer[position:] = data[:first_part_size] # Write second part (wrapped to start) handle.buffer[:end_position - handle.capacity] = data[first_part_size:] # UPDATE ABSOLUTE POSITION (always increases, never wraps) handle.write_absolute += bytes_to_write return (Status.OK, bytes_to_write) # ============================================================================= # READ OPERATION (SPAN COPY) # ============================================================================= def cb_copy_span( handle: CircularBufferHandle, start_absolute: int, end_absolute: int ) -> Tuple[Status, bytes]: """ Copy a span of bytes [start_absolute, end_absolute) from the buffer. KEY CONCEPT: Uses absolute offsets, not ring positions! Example with capacity=6, w_abs=10: Buffer state: [G][H][I][J][D][E][F] ↑ newest (w_abs=10) ↑ oldest (w_abs=4) Request: copy_span(5, 8) → want bytes at w_abs [5,6,7] Result: [F][G][H] If requested span > capacity: Returns the newest 'capacity' bytes (because older data was already overwritten) C equivalent: int cb_copy_span(CircularBuffer* cb, size_t start, size_t end, uint8_t* out_buffer) { pthread_mutex_lock(&cb->lock); size_t len = end - start; if (len > cb->capacity) { // Can't retrieve more than capacity start = cb->write_absolute - cb->capacity; end = cb->write_absolute; len = cb->capacity; } for (size_t i = 0; i < len; i++) { size_t pos = (start + i) % cb->capacity; out_buffer[i] = cb->buffer[pos]; } pthread_mutex_unlock(&cb->lock); return len; } Args: handle: Buffer to read from start_absolute: Start offset (absolute, not position in ring) end_absolute: End offset (exclusive) Returns: (Status.OK, bytes_data) Thread-safe: Yes """ # Edge case: empty span if end_absolute <= start_absolute: return (Status.OK, b"") span_length = end_absolute - start_absolute # OVERFLOW PROTECTION: Can't retrieve more than capacity if span_length > handle.capacity: # Adjust to newest available window with handle.lock: start_absolute = handle.write_absolute - handle.capacity end_absolute = handle.write_absolute span_length = handle.capacity # ALLOCATE OUTPUT BUFFER output = bytearray(span_length) # COPY BYTES ONE BY ONE (handling wrap-around automatically) with handle.lock: for i in range(span_length): # Calculate position in circular buffer # Like: size_t pos = (start + i) % capacity in C position = (start_absolute + i) % handle.capacity output[i] = handle.buffer[position] return (Status.OK, bytes(output)) # ============================================================================= # QUERY FUNCTIONS (read-only, thread-safe) # ============================================================================= def cb_overflows(handle: CircularBufferHandle) -> int: """ Get number of overflow events. An overflow happens when a single write is larger than capacity. C equivalent: int cb_overflows(CircularBuffer* cb) { pthread_mutex_lock(&cb->lock); int count = cb->overflow_count; pthread_mutex_unlock(&cb->lock); return count; } Returns: Number of times data was truncated """ with handle.lock: return handle.overflow_count def cb_w_abs(handle: CircularBufferHandle) -> int: """ Get total bytes written since creation. This is the absolute write offset (monotonic counter). C equivalent: size_t cb_w_abs(CircularBuffer* cb) { pthread_mutex_lock(&cb->lock); size_t w = cb->write_absolute; pthread_mutex_unlock(&cb->lock); return w; } Returns: Total bytes written (never decreases, never wraps) """ with handle.lock: return handle.write_absolute def cb_capacity(handle: CircularBufferHandle) -> int: """ Get buffer capacity in bytes. C equivalent: size_t cb_capacity(CircularBuffer* cb) { return cb->capacity; // No lock needed (const) } Returns: Total capacity (fixed at creation) """ # No lock needed - capacity never changes return handle.capacity def cb_fill_bytes(handle: CircularBufferHandle) -> int: """ Get current fill level in bytes. Definition: fill = min(write_absolute, capacity) Why min? Once we've written 'capacity' bytes, buffer is considered full (even though we keep writing, overwriting old data). Example with capacity=100: w_abs=50 → fill=50 (half full) w_abs=100 → fill=100 (full) w_abs=200 → fill=100 (still "full", old data overwritten) C equivalent: size_t cb_fill_bytes(CircularBuffer* cb) { pthread_mutex_lock(&cb->lock); size_t fill = (cb->write_absolute < cb->capacity) ? cb->write_absolute : cb->capacity; pthread_mutex_unlock(&cb->lock); return fill; } Returns: Bytes currently in buffer (0 to capacity) """ with handle.lock: # Ternary operator in Python: value_if_true if condition else value_if_false # C equivalent: write_absolute < capacity ? write_absolute : capacity return handle.write_absolute if handle.write_absolute < handle.capacity else handle.capacity def cb_fill_pct(handle: CircularBufferHandle) -> int: """ Get fill level as percentage (0-100). Useful for progress bars or monitoring. C equivalent: int cb_fill_pct(CircularBuffer* cb) { size_t fill = cb_fill_bytes(cb); size_t cap = cb->capacity > 0 ? cb->capacity : 1; return (fill * 100) / cap; // Integer division } Returns: Integer percentage 0-100 (no decimal, no '%' sign) Example: capacity=1000, fill=750 → returns 75 """ fill = cb_fill_bytes(handle) cap = handle.capacity if handle.capacity > 0 else 1 # Avoid division by zero # Integer division (// in Python = / in C for integers) return (fill * 100) // cap # ============================================================================= # HELPER: Visual representation (for debugging) # ============================================================================= def cb_debug_info(handle: CircularBufferHandle) -> str: """ Get human-readable debug information. Not in original, but useful for learning/debugging! Returns: Multi-line string with buffer state """ with handle.lock: info = f""" Circular Buffer State: ---------------------- Capacity: {handle.capacity} bytes Written total: {handle.write_absolute} bytes Fill level: {cb_fill_bytes(handle)} bytes ({cb_fill_pct(handle)}%) Overflows: {handle.overflow_count} Position: {handle.write_absolute % handle.capacity} """ return info # ============================================================================= # TEACHING NOTES # ============================================================================= """ KEY PYTHON vs C DIFFERENCES: ============================= 1. Memory Management: C: malloc/free, manual cleanup Python: Automatic garbage collection 2. Locking: C: pthread_mutex_lock(&lock); ... pthread_mutex_unlock(&lock); Python: with lock: ... (automatic unlock even if exception) 3. Arrays: C: uint8_t* buffer (pointer arithmetic) Python: bytearray (looks like list but is byte-optimized) 4. Slicing: C: memcpy(dst, src, len) Python: dst[:] = src (copies entire array) dst[2:5] = src[2:5] (copies slice) 5. Modulo: C: pos = offset % capacity; Python: pos = offset % capacity (same!) 6. Enums: C: enum Status { OK=0, ERROR=1 }; Python: class Status(Enum): OK=0; ERROR=1 7. Structs: C: typedef struct { int x; } MyStruct; Python: @dataclass class MyStruct: x: int WHEN TO USE THIS BUFFER: ========================= ✅ Streaming data (serial, network) ✅ Multi-threaded producer/consumer ✅ Fixed memory budget ✅ Don't care about old data ✅ Need thread-safe operations ❌ Need to keep ALL data (use list/deque instead) ❌ Single-threaded (simpler structures work) ❌ Need to read from middle frequently (use deque) """