Skip to content

SMSP Technical Specification

Synchronized Multimodal Score Protocol

Version: 2.0 (Vector Time Integration)
Status: Draft / Experimental
Parent Document: Connectionless Distributed Timing Prior Art (DOI: 10.5281/zenodo.18078264)
Repository: https://github.com/lemonforest/mlehaptics


Abstract

SMSP (Synchronized Multimodal Score Protocol) defines what synchronized nodes do, completing the protocol triad with UTLP (when) and RFIP (where). Version 2.0 integrates with UTLP Vector Time, enabling phase-indexed scores that execute identically across all phase-locked nodes without runtime coordination or start synchronization.

The key insight: patterns are deterministic functions of phase. Given the same phase, every node computes the same output. Phases converge automatically via UTLP. Therefore, outputs converge automatically—no "start now" signal required.


1. Introduction

1.1 The Protocol Triad

Protocol Question Direction
UTLP When is it? Broadcast (time source → all)
RFIP Where am I? Peer-to-peer (mutual ranging)
SMSP What do I do? / What did I see? Bidirectional (instructions ↔ observations)

Any node with synchronized time (UTLP), known position (RFIP), and a score (SMSP) can participate in coordinated behavior.

1.2 Design Philosophy

SMSP separates concerns:

┌─────────────────────────────────────────────────────────────┐
│  DECLARATIVE LAYER (Human Intent)                           │
│  "Alternate left/right at 1Hz with 50% duty cycle"          │
│  "SAE J845 Quad Flash pattern"                              │
│  "Bilateral EMDR standard protocol"                         │
├─────────────────────────────────────────────────────────────┤
│  COMPILER LAYER (Design Tool / PWA)                         │
│  Transforms intent into phase-indexed events                │
│  Validates parameters, encodes tick → phases                │
├─────────────────────────────────────────────────────────────┤
│  EXECUTION LAYER (Score + Playback Engine)                  │
│  "When my phases match event phases, execute action"        │
│  Engine only knows: current phases, score, outputs          │
└─────────────────────────────────────────────────────────────┘

This separation means: - Firmware stays simple: The playback engine is "dumb"—it matches phases, sets outputs - Complexity lives in the compiler: Updated without touching firmware - Advanced users can bypass: Raw phase-indexed scores can be authored directly

1.3 Version History

Version Model Time Representation Sync Requirement
1.0 Imperative Scalar ticks Explicit start signal
2.0 Declarative Phase vector None (auto-converge)

2. Core Concepts

2.1 The Paradigm Shift: Scalar vs Vector Time

Scalar Time (v1.0):

// "At tick 1000, turn LED on"
if (current_tick == 1000) {
    led_on();
}

Problem: Requires all nodes to agree on absolute tick count. Needs start synchronization.

Vector Time (v2.0):

// "When phases are [35, 246, 44, 68, 84, 92, 108, 156], turn LED on"
if (phases_match(my_phases, event_phases)) {
    led_on();
}

Advantage: Phases converge automatically. No start signal needed.

2.2 Why This Works

Device A: powered on at arbitrary time, counts ticks, broadcasts phases
Device B: powered on later, counts ticks, hears A's beacon, nudges toward A
Device C: powered on even later, hears both, nudges toward consensus

After a few beacon cycles: all devices have same phases
                          WITHOUT agreeing on "what tick is it"

Same phases → same score position → same output

The pattern becomes a pure function of phase. Sync the phases (UTLP does this automatically), and the outputs sync automatically.

2.3 Encoding: Tick Space → Phase Space

Patterns are designed in tick-space (human-intuitive), compiled to phase-space (machine-executable):

# Human writes this:
pattern = [
    { "tick": 0,   "led": ON  },
    { "tick": 100, "led": OFF },
    { "tick": 200, "led": ON  },
    { "tick": 300, "led": OFF },
]

# Compiler generates this:
PRIMES = [241, 251, 239, 233, 229, 227, 223, 211]

compiled = [
    { "phases": [0 % p for p in PRIMES],   "led": ON  },   # [0,0,0,0,0,0,0,0]
    { "phases": [100 % p for p in PRIMES], "led": OFF },   # [100,100,100,100,100,100,100,100]
    { "phases": [200 % p for p in PRIMES], "led": ON  },   # [200,200,200,200,200,200,200,200]
    { "phases": [300 % p for p in PRIMES], "led": OFF },   # [59,49,61,67,71,73,77,89]
]

Encoding is trivial: phase[i] = tick % prime[i]

No CRT at runtime: Just compare 8 bytes per event.


3. Score Format

3.1 Phase-Indexed Event (v2.0)

typedef struct {
    // Phase coordinates (when to execute)
    uint8_t phases[8];          // Target phase for each prime
    uint8_t phase_mask;         // Which phases must match (0xFF = all)

    // Transition
    uint8_t transition_ticks;   // Interpolation duration (in pattern ticks)
    uint8_t easing;             // EASE_LINEAR, EASE_IN, EASE_OUT, EASE_INOUT

    // Output state
    uint8_t channels[];         // Variable-length channel state array
} smsp_event_v2_t;

3.2 Single-Phase Optimization

For patterns fitting within one prime cycle (≤241 ticks at smallest prime):

typedef struct {
    uint8_t phase;              // Single phase value (prime index 0)
    uint8_t transition_ticks;
    uint8_t channels[];
} smsp_event_simple_t;

Runtime check becomes one byte comparison.

3.3 Region-Based Scores (Declarative)

Instead of discrete events, define continuous regions:

typedef struct {
    uint8_t phase_index;        // Which prime dimension (0-7)
    uint8_t region_start;       // Inclusive start
    uint8_t region_end;         // Exclusive end
    uint8_t channels[];         // State while in this region
} smsp_region_t;

typedef struct {
    uint8_t num_regions;
    smsp_region_t regions[];
} smsp_region_score_t;

Example: Bilateral EMDR with RGB LEDs

// Channel layout: L_R, L_G, L_B, L_haptic, R_R, R_G, R_B, R_haptic
// Using prime[0] = 241, pattern period = 241 ticks (~241ms at 1ms ticks)

#define LED_OFF     0
#define LED_FULL    255
#define HAPTIC_MED  180

smsp_region_t bilateral_score[] = {
    // Left active: Cyan LED (R=0, G=255, B=255) + haptic
    { .phase_index = 0, .region_start = 0,   .region_end = 120, 
      .channels = { LED_OFF, LED_FULL, LED_FULL, HAPTIC_MED,   // Left: Cyan + haptic
                    LED_OFF, LED_OFF,  LED_OFF,  0 } },         // Right: Off

    // Right active: Cyan LED + haptic  
    { .phase_index = 0, .region_start = 120, .region_end = 241,
      .channels = { LED_OFF, LED_OFF,  LED_OFF,  0,             // Left: Off
                    LED_OFF, LED_FULL, LED_FULL, HAPTIC_MED } } // Right: Cyan + haptic
};

Every node with phase[0] ∈ [0,120) outputs LEFT active. Every node with phase[0] ∈ [120,241) outputs RIGHT active. No coordination message needed.

3.4 Score Container

typedef struct {
    // Header
    uint8_t  version;           // SMSP_VERSION_2
    uint8_t  format;            // FORMAT_EVENTS | FORMAT_REGIONS | FORMAT_SIMPLE
    uint8_t  pattern_class;     // BILATERAL, EMERGENCY, SWARM, CUSTOM
    uint8_t  prime_config;      // Which prime set (standard, fine, custom)

    // Timing
    uint8_t  tick_period_log2;  // Tick period = 2^N microseconds (0=1μs, 10=1ms)
    uint8_t  pattern_prime_idx; // Which prime defines pattern period (0-7)

    // Loop control
    uint8_t  loop_count;        // 0 = infinite, N = play N times
    uint8_t  loop_point;        // Event/region index to loop back to

    // Channel configuration
    uint8_t  num_channels;
    uint8_t  channel_types[];   // LED_RGB, LED_MONO, HAPTIC, AUDIO_FREQ, etc.

    // Score data
    uint16_t data_length;
    uint8_t  data[];            // Events or regions, format-dependent
} smsp_score_t;

4. Playback Engine

4.1 Minimal Implementation (Region-Based)

void smsp_tick(smsp_engine_t* engine) {
    uint8_t phase = engine->current_phases[engine->score->pattern_prime_idx];

    for (int i = 0; i < engine->score->num_regions; i++) {
        smsp_region_t* r = &engine->score->regions[i];

        if (phase >= r->region_start && phase < r->region_end) {
            apply_channels(r->channels, engine->score->num_channels);
            return;
        }
    }
}

This runs on an ATtiny85. The ESP32-C6 is only needed for wireless bootstrap and UTLP phase synchronization.

4.2 Event-Based Implementation

void smsp_tick(smsp_engine_t* engine) {
    // Check if current phases match any event
    for (int i = 0; i < engine->score->num_events; i++) {
        smsp_event_v2_t* e = &engine->score->events[i];

        if (phases_match(engine->current_phases, e->phases, e->phase_mask)) {
            // Start transition to this event's state
            engine->target_state = e->channels;
            engine->transition_remaining = e->transition_ticks;
            engine->easing = e->easing;
            return;
        }
    }

    // Continue any active transition
    if (engine->transition_remaining > 0) {
        interpolate_outputs(engine);
        engine->transition_remaining--;
    }
}

bool phases_match(uint8_t* current, uint8_t* target, uint8_t mask) {
    for (int i = 0; i < 8; i++) {
        if ((mask & (1 << i)) && current[i] != target[i]) {
            return false;
        }
    }
    return true;
}

4.3 Integration with UTLP Vector Time

// UTLP callback - called every tick
void utlp_on_tick(uint8_t* phases) {
    // Update engine's phase view
    memcpy(smsp_engine.current_phases, phases, 8);

    // Execute score
    smsp_tick(&smsp_engine);
}

// UTLP callback - called when beacon received
void utlp_on_beacon(uint8_t* peer_phases) {
    // UTLP handles phase nudging internally
    // SMSP doesn't need to know about sync mechanics
}

Key insight: SMSP doesn't manage time. It receives phases from UTLP and reacts. The sync is invisible to the score engine.


5. Pattern Compiler

5.1 Compilation Pipeline

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Human Intent   │────▶│    Compiler     │────▶│  Binary Score   │
│  (JSON/YAML)    │     │  (PWA/CLI)      │     │  (Flash/OTA)    │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                       │                       │
        ▼                       ▼                       ▼
  "1Hz bilateral"      tick→phase encode        smsp_score_t
  "SAE J845 quad"      validation               ready for MCU
  "custom timeline"    optimization

5.2 Intent Format (Input)

# bilateral_1hz.smsp.yaml
name: "Bilateral 1Hz"
class: bilateral
tick_period_us: 1000    # 1ms ticks
pattern_period_ms: 1000 # 1 second cycle

channels:
  - name: left_led
    type: led_mono
  - name: right_led  
    type: led_mono
  - name: left_haptic
    type: haptic
  - name: right_haptic
    type: haptic

regions:
  - name: left_active
    start_ms: 0
    end_ms: 500
    state:
      left_led: 255
      right_led: 0
      left_haptic: 200
      right_haptic: 0

  - name: right_active
    start_ms: 500
    end_ms: 1000
    state:
      left_led: 0
      right_led: 255
      left_haptic: 0
      right_haptic: 200

5.3 Compilation Process

def compile_score(intent):
    # 1. Select prime configuration
    primes = select_primes(intent.pattern_period_ms, intent.tick_period_us)

    # 2. Calculate pattern period in ticks
    period_ticks = intent.pattern_period_ms * 1000 // intent.tick_period_us

    # 3. Find best-fit prime for pattern period
    pattern_prime_idx = find_closest_prime(period_ticks, primes)
    pattern_prime = primes[pattern_prime_idx]

    # 4. Scale regions to fit prime
    scale = pattern_prime / period_ticks

    # 5. Convert regions
    compiled_regions = []
    for region in intent.regions:
        start_tick = region.start_ms * 1000 // intent.tick_period_us
        end_tick = region.end_ms * 1000 // intent.tick_period_us

        compiled_regions.append({
            'phase_index': pattern_prime_idx,
            'region_start': int(start_tick * scale) % pattern_prime,
            'region_end': int(end_tick * scale) % pattern_prime,
            'channels': encode_channels(region.state, intent.channels)
        })

    # 6. Package into score container
    return smsp_score_t(
        version=2,
        format=FORMAT_REGIONS,
        pattern_class=intent.class,
        prime_config=PRIMES_STANDARD,
        pattern_prime_idx=pattern_prime_idx,
        regions=compiled_regions
    )

5.4 Prime Selection Strategy

Design Note: Prime selection is a compiler responsibility, not an engine responsibility. The playback engine on the MCU is intentionally "dumb"—it only matches phases and sets outputs. All the intelligence about selecting optimal primes, scaling patterns, and handling edge cases lives in the PWA/CLI compiler that runs on a more capable device (phone, laptop). This keeps the embedded firmware simple, deterministic, and portable across MCU architectures.

Pattern Period Recommended Prime Fit Error
~211 ticks 211 Exact
~223 ticks 223 Exact
~227 ticks 227 Exact
~229 ticks 229 Exact
~233 ticks 233 Exact
~239 ticks 239 Exact
~241 ticks 241 Exact
~251 ticks 251 Exact
~500 ticks 251 × 2 Use dual-phase
~1000 ticks 251 × 4 Use dual-phase
Arbitrary Use mask Match subset of phases

Compiler Algorithm: 1. Calculate desired pattern period in ticks 2. Find the prime closest to that period 3. Scale pattern timing to fit the selected prime 4. Encode scaled timing into phase values 5. Store selected prime index in score header

For patterns that don't fit a single prime, use phase masking to match only relevant phases, or use CRT tick recovery at the cost of more computation.


6. Pattern Classes

6.1 Classification Enum

typedef enum {
    PATTERN_BILATERAL,      // Antiphase pair (EMDR, tDCS)
    PATTERN_EMERGENCY,      // SAE J845 compliant (lightbars)
    PATTERN_SWARM_SYNC,     // In-phase coherence (mutual visibility)
    PATTERN_PURSUIT,        // Sequential chase (around geometry)
    PATTERN_BINARY_ORBIT,   // Wobble + rotation composite
    PATTERN_SENSING,        // Coordinated sampling schedule
    PATTERN_CUSTOM          // Raw score, no assumptions
} pattern_class_t;

6.2 Bilateral Patterns

Characteristics: - Two zones (LEFT, RIGHT) - Antiphase operation (when LEFT is active, RIGHT is inactive) - Typically therapeutic (EMDR, bilateral stimulation)

Constraints: - Duty cycle should sum to ≤100% - Frequency range: 0.5-2 Hz typical for EMDR

Example with RGB LEDs:

// 1Hz bilateral, 50% duty cycle each side
// Pattern period fits prime[0] = 241 ticks (~241ms at 1ms ticks)
// Channel layout: L_R, L_G, L_B, L_haptic, R_R, R_G, R_B, R_haptic

smsp_region_t bilateral[] = {
    // Left active: Soft blue LED + haptic buzz
    { 0, 0, 120,   { 0, 100, 255, 180,    // Left: Blue-ish, haptic on
                    0, 0,   0,   0   } }, // Right: Off
    // Right active: Soft blue LED + haptic buzz                
    { 0, 120, 241, { 0, 0,   0,   0,      // Left: Off
                    0, 100, 255, 180 } }  // Right: Blue-ish, haptic on
};

6.3 Emergency Patterns (SAE J845)

Characteristics: - Flash rate: 1.0 - 4.0 Hz - Dark interval: ≥160 ms between flash bursts - Pulse train: pulses within burst start ≤100 ms apart - SAE J845 Class 1 for high-visibility roadway applications

Police Lightbar Example (Red/Blue/White):

Typical police lightbar configuration: Red on driver's side, Blue on passenger side, White for takedowns/pursuit. Pattern alternates colors for maximum visibility—each color flash ensures optimal wavelength is visible regardless of ambient lighting conditions.

// SAE J845 Quad Flash with Red/Blue alternating + White accent
// At 1ms ticks: 250 ticks per cycle (4Hz), use prime[7] = 251
// Channel layout: L_R, L_G, L_B, R_R, R_G, R_B (Left RGB, Right RGB)

#define RED_FULL    {255, 0,   0  }   // Red: high daytime visibility
#define BLUE_FULL   {0,   0,   255}   // Blue: high nighttime visibility  
#define WHITE_FULL  {255, 255, 255}   // White: pursuit/takedown flash
#define LED_OFF     {0,   0,   0  }

smsp_region_t police_lightbar[] = {
    // Phase 1: Left RED quad flash
    { 7, 0,   10,  { 255,0,0, 0,0,0 } },     // L=RED, R=OFF
    { 7, 10,  20,  { 0,0,0,   0,0,0 } },     // Gap
    { 7, 20,  30,  { 255,0,0, 0,0,0 } },     // L=RED
    { 7, 30,  40,  { 0,0,0,   0,0,0 } },     // Gap
    { 7, 40,  50,  { 255,0,0, 0,0,0 } },     // L=RED
    { 7, 50,  60,  { 0,0,0,   0,0,0 } },     // Gap
    { 7, 60,  70,  { 255,0,0, 0,0,0 } },     // L=RED (4th flash)

    // Phase 2: Right BLUE quad flash  
    { 7, 70,  80,  { 0,0,0, 0,0,255 } },     // L=OFF, R=BLUE
    { 7, 80,  90,  { 0,0,0,   0,0,0 } },     // Gap
    { 7, 90,  100, { 0,0,0, 0,0,255 } },     // R=BLUE
    { 7, 100, 110, { 0,0,0,   0,0,0 } },     // Gap
    { 7, 110, 120, { 0,0,0, 0,0,255 } },     // R=BLUE
    { 7, 120, 130, { 0,0,0,   0,0,0 } },     // Gap
    { 7, 130, 140, { 0,0,0, 0,0,255 } },     // R=BLUE (4th flash)

    // Phase 3: WHITE pursuit burst (both sides)
    { 7, 140, 150, { 255,255,255, 255,255,255 } },  // Both WHITE
    { 7, 150, 160, { 0,0,0, 0,0,0 } },              // Gap
    { 7, 160, 170, { 255,255,255, 255,255,255 } },  // Both WHITE

    // Dark interval (81 ticks = 81ms, satisfies ≥80ms for readability)
    { 7, 170, 251, { 0,0,0, 0,0,0 } },       // Dark period before repeat
};

Pattern Analysis: - Total cycle: 251ms (~4Hz flash rate) ✓ - Each quad flash burst: 70ms (4 flashes × 10ms + 3 gaps × 10ms) ✓ - Inter-phase gaps: adequate for color differentiation ✓ - White accent: high-intensity burst attracts attention during pursuit - Dark interval: 81ms at end provides visual reset

Color Selection Rationale: - Red (255,0,0): Superior daytime visibility, traditional "stop" association - Blue (0,0,255): Superior nighttime visibility, reduced eye fatigue - White (255,255,255): Maximum intensity burst, pursuit mode indicator

6.4 Sensing Patterns

Characteristics: - Coordinated sampling across distributed nodes - Timestamps in phase space - Observations flow back to coordinator

Example: Acoustic Sampling

// Sample every 100μs (10 kHz rate)
// At 1μs ticks: period = 100, use prime[0] mod 100
smsp_event_simple_t sample_schedule[] = {
    { .phase = 0,  .action = ACTION_SAMPLE },
    { .phase = 50, .action = ACTION_SAMPLE },  // Two samples per prime cycle
};


7. Bidirectional Operation

7.1 The Orchestra Model

┌─────────────┐     score (baton)      ┌─────────────┐
│             │ ─────────────────────▶ │             │
│  Conductor  │                        │   Nodes     │
│             │ ◀───────────────────── │             │
└─────────────┘    observations        └─────────────┘

SMSP supports bidirectional flow: - Instructions (conductor → nodes): "Sample at phase P", "Set output to X" - Observations (nodes → conductor): "At phase P, I measured V"

7.2 Observation Format

typedef struct {
    uint8_t  phases[8];         // When it happened
    uint16_t source_node;       // Who observed it
    uint8_t  observation_type;  // What kind
    uint8_t  payload[];         // Data
} smsp_observation_t;

typedef enum {
    OBS_HEARTBEAT,          // "I'm alive, sync quality = X"
    OBS_SAMPLE,             // "At phase P, sensor read V"
    OBS_EVENT,              // "At phase P, threshold crossed"
    OBS_ACK,                // "I executed instruction N"
    OBS_ERROR,              // "Instruction N failed"
} observation_type_t;

7.3 Closed-Loop Operation

         ┌──────────────────────────────────────────┐
         │           Coordinator/Gateway            │
         │  - Distributes sampling schedule         │
         │  - Receives observations                 │
         │  - Correlates data (beamforming, etc.)   │
         └────────────────┬───────────────────────┬─┘
                          │                       │
              SMSP instructions           SMSP observations
              (sample at phase P)         (at P, saw V)
                          │                       │
                          ▼                       ▲
         ┌────────────────┴───────────────────────┴─┐
         │              Sensor Nodes                 │
         │  - Execute at synchronized phases        │
         │  - Report observations with phase stamps │
         └───────────────────────────────────────────┘

8. Transport Agnosticism

SMSP defines format and semantics, not delivery:

Transport Score Delivery Observation Return
ESP-NOW Broadcast/unicast Unicast to coordinator
BLE GATT write GATT notify
LoRa Broadcast Unicast to gateway
WiFi UDP multicast UDP to server
Serial Point-to-point Collected by host
Flash Baked at build Not applicable

The protocol is complete when a node possesses: 1. A score (however delivered) 2. Phase lock with peers (however achieved via UTLP) 3. Zone assignment (however determined, potentially via RFIP)


9. Scale Invariance

The same score format works regardless of physical scale:

Scale "Zones" Are Example
PCB GPIO pins RGB LEDs on one board
Device Peer MAC addresses Bilateral handhelds
Room Node positions Warning light array
Field Drone IDs Search and rescue swarm
Building Sensor clusters Structural monitoring
Region Base stations Seismic array

A score written for three LEDs on a PCB plays identically on three drones 100 meters apart. The playback engine doesn't know physical spacing—it only knows phases and channels.


10. Relationship to Existing Standards

Standard What It Does SMSP Difference
DMX512 512 channels, continuous broadcast Score uploaded once, nodes execute independently
MIDI Note events, musical timing Continuous interpolated states, sub-ms sync
SMPTE Timecode, devices chase master Devices agree on phase then execute autonomously
OSC Real-time control messages Score complete before execution, no runtime traffic
Art-Net DMX over Ethernet Connectionless during execution

SMSP combines: - DMX's channel abstraction - MIDI's event timing - SMPTE's frame accuracy - OSC's flexibility

...while eliminating: - Continuous network traffic during execution - Central controller as single point of failure - Wired infrastructure requirements - Per-node cost barriers (runs on $0.50 MCU)


11. Implementation Requirements

11.1 Minimum Engine (Region-Based)

Resource Requirement
Flash ~500 bytes code + score size
RAM ~50 bytes state
CPU One comparison per region per tick
Timer UTLP tick interrupt

Runs on: ATtiny85, ATmega328, any Cortex-M0+

11.2 Full Engine (Event-Based with Interpolation)

Resource Requirement
Flash ~2 KB code + score size
RAM ~200 bytes state
CPU Phase matching + interpolation
Timer UTLP tick interrupt

Runs on: ESP32, STM32, any Cortex-M3+

11.3 Coordinator (Bidirectional)

Resource Requirement
Flash ~10 KB code
RAM ~1 KB + observations buffer
Network ESP-NOW/BLE/WiFi
Storage Optional logging

Runs on: ESP32-C6, ESP32-S3, Linux gateway


12. Wire Format

12.1 Score Upload Packet

┌──────────────────────────────────────────────────────────┐
│ Byte 0: SMSP_MAGIC (0x53)                                │
│ Byte 1: Command (SCORE_UPLOAD = 0x01)                    │
│ Byte 2-3: Total length (little-endian)                   │
│ Byte 4: Fragment index                                   │
│ Byte 5: Total fragments                                  │
│ Byte 6-N: Score data (smsp_score_t fragment)             │
└──────────────────────────────────────────────────────────┘

12.2 Observation Packet

┌──────────────────────────────────────────────────────────┐
│ Byte 0: SMSP_MAGIC (0x53)                                │
│ Byte 1: Command (OBSERVATION = 0x02)                     │
│ Byte 2-9: Phases[8] (when observed)                      │
│ Byte 10-11: Source node ID                               │
│ Byte 12: Observation type                                │
│ Byte 13-N: Payload (type-dependent)                      │
└──────────────────────────────────────────────────────────┘

13. Security Considerations

13.1 Score Integrity

Scores should be signed when delivered over untrusted channels:

typedef struct {
    smsp_score_t score;
    uint8_t      signature[32];  // Ed25519 or HMAC-SHA256
} smsp_signed_score_t;

13.2 Observation Authentication

Observations should include node authentication:

typedef struct {
    smsp_observation_t obs;
    uint8_t            mac[8];  // Truncated HMAC
} smsp_authenticated_obs_t;

13.3 Phase Lock Attacks

An attacker broadcasting false UTLP beacons could desynchronize nodes. Mitigations: - Stratum hierarchy (prefer higher-authority sources) - Byzantine detection (reject outliers via vector similarity) - Cryptographic beacons (signed by trusted time source)

See UTLP Technical Supplement S2 (Biological Governance) for security architecture.


14. Reference Implementation

14.1 Score Compiler (Python)

#!/usr/bin/env python3
"""
SMSP Score Compiler
Converts human-readable pattern definitions to binary scores.
"""

import struct
from dataclasses import dataclass
from typing import List

PRIMES = [241, 251, 239, 233, 229, 227, 223, 211]

@dataclass
class Region:
    phase_index: int
    start: int
    end: int
    channels: bytes

@dataclass
class Score:
    version: int = 2
    format: int = 1  # FORMAT_REGIONS
    pattern_class: int = 0
    prime_config: int = 0
    pattern_prime_idx: int = 0
    regions: List[Region] = None

    def to_bytes(self) -> bytes:
        header = struct.pack('<BBBBBB',
            self.version,
            self.format,
            self.pattern_class,
            self.prime_config,
            self.pattern_prime_idx,
            len(self.regions)
        )

        region_data = b''
        for r in self.regions:
            region_data += struct.pack('<BBB',
                r.phase_index,
                r.start,
                r.end
            ) + r.channels

        return header + region_data


def compile_bilateral(frequency_hz: float, num_channels: int = 4) -> Score:
    """Compile a bilateral (EMDR-style) pattern."""

    # Select prime closest to desired period
    # At 1ms ticks, 1Hz = 1000 ticks
    tick_period_ms = 1
    period_ticks = int(1000 / frequency_hz / tick_period_ms)

    # Find best prime
    best_prime_idx = min(range(len(PRIMES)), 
                         key=lambda i: abs(PRIMES[i] - period_ticks))
    prime = PRIMES[best_prime_idx]

    # Create antiphase regions
    midpoint = prime // 2

    # Channel layout: L_R, L_G, L_B, L_haptic, R_R, R_G, R_B, R_haptic
    # Soft blue color (R=0, G=100, B=255) for therapeutic calming effect
    left_channels = bytes([0, 100, 255, 180,   0, 0, 0, 0])    # Left: blue+haptic
    right_channels = bytes([0, 0, 0, 0,   0, 100, 255, 180])   # Right: blue+haptic

    return Score(
        pattern_class=0,  # BILATERAL
        pattern_prime_idx=best_prime_idx,
        regions=[
            Region(best_prime_idx, 0, midpoint, left_channels),
            Region(best_prime_idx, midpoint, prime, right_channels),
        ]
    )


def compile_police_lightbar() -> Score:
    """Compile SAE J845 police lightbar pattern with Red/Blue/White."""

    # 4Hz = 250ms period, use prime[7] = 251
    prime_idx = 7  # 251

    # Channel layout: L_R, L_G, L_B, R_R, R_G, R_B (6 bytes per region)
    RED =   bytes([255, 0, 0])
    BLUE =  bytes([0, 0, 255])
    WHITE = bytes([255, 255, 255])
    OFF =   bytes([0, 0, 0])

    return Score(
        pattern_class=1,  # EMERGENCY
        pattern_prime_idx=prime_idx,
        regions=[
            # Left RED quad flash
            Region(prime_idx, 0,   10,  RED + OFF),      # L=RED
            Region(prime_idx, 10,  20,  OFF + OFF),     # Gap
            Region(prime_idx, 20,  30,  RED + OFF),      # L=RED
            Region(prime_idx, 30,  40,  OFF + OFF),     # Gap
            Region(prime_idx, 40,  50,  RED + OFF),      # L=RED
            Region(prime_idx, 50,  60,  OFF + OFF),     # Gap
            Region(prime_idx, 60,  70,  RED + OFF),      # L=RED (4th)
            # Right BLUE quad flash
            Region(prime_idx, 70,  80,  OFF + BLUE),    # R=BLUE
            Region(prime_idx, 80,  90,  OFF + OFF),     # Gap
            Region(prime_idx, 90,  100, OFF + BLUE),    # R=BLUE
            Region(prime_idx, 100, 110, OFF + OFF),     # Gap
            Region(prime_idx, 110, 120, OFF + BLUE),    # R=BLUE
            Region(prime_idx, 120, 130, OFF + OFF),     # Gap
            Region(prime_idx, 130, 140, OFF + BLUE),    # R=BLUE (4th)
            # WHITE pursuit burst
            Region(prime_idx, 140, 150, WHITE + WHITE), # Both WHITE
            Region(prime_idx, 150, 160, OFF + OFF),     # Gap
            Region(prime_idx, 160, 170, WHITE + WHITE), # Both WHITE
            # Dark interval
            Region(prime_idx, 170, 251, OFF + OFF),     # Dark period
        ]
    )


if __name__ == '__main__':
    # Example: compile 1Hz bilateral
    score = compile_bilateral(1.0)
    binary = score.to_bytes()
    print(f"Bilateral 1Hz: {len(binary)} bytes")
    print(f"  Prime index: {score.pattern_prime_idx}")
    print(f"  Prime value: {PRIMES[score.pattern_prime_idx]}")
    print(f"  Regions: {len(score.regions)}")

    # Example: compile police lightbar
    score = compile_police_lightbar()
    binary = score.to_bytes()
    print(f"\nPolice Lightbar: {len(binary)} bytes")
    print(f"  Prime index: {score.pattern_prime_idx}")
    print(f"  Prime value: {PRIMES[score.pattern_prime_idx]}")
    print(f"  Regions: {len(score.regions)}")

14.2 Playback Engine (C)

/**
 * SMSP Playback Engine
 * Minimal region-based implementation
 */

#include <stdint.h>
#include <string.h>

#define SMSP_MAX_CHANNELS 8
#define SMSP_MAX_REGIONS 16

typedef struct {
    uint8_t phase_index;
    uint8_t region_start;
    uint8_t region_end;
    uint8_t channels[SMSP_MAX_CHANNELS];
} smsp_region_t;

typedef struct {
    uint8_t version;
    uint8_t format;
    uint8_t pattern_class;
    uint8_t prime_config;
    uint8_t pattern_prime_idx;
    uint8_t num_regions;
    uint8_t num_channels;
    smsp_region_t regions[SMSP_MAX_REGIONS];
} smsp_score_t;

typedef struct {
    smsp_score_t* score;
    uint8_t current_phases[8];
    uint8_t output_channels[SMSP_MAX_CHANNELS];
} smsp_engine_t;

/**
 * Initialize engine with a score
 */
void smsp_init(smsp_engine_t* engine, smsp_score_t* score) {
    engine->score = score;
    memset(engine->current_phases, 0, 8);
    memset(engine->output_channels, 0, SMSP_MAX_CHANNELS);
}

/**
 * Update phases from UTLP
 */
void smsp_update_phases(smsp_engine_t* engine, uint8_t* phases) {
    memcpy(engine->current_phases, phases, 8);
}

/**
 * Execute one tick - find matching region and set outputs
 */
void smsp_tick(smsp_engine_t* engine) {
    if (!engine->score) return;

    uint8_t phase = engine->current_phases[engine->score->pattern_prime_idx];

    for (int i = 0; i < engine->score->num_regions; i++) {
        smsp_region_t* r = &engine->score->regions[i];

        // Check if phase is in this region
        if (r->region_start <= r->region_end) {
            // Normal range
            if (phase >= r->region_start && phase < r->region_end) {
                memcpy(engine->output_channels, r->channels, 
                       engine->score->num_channels);
                return;
            }
        } else {
            // Wrapped range (e.g., 200-50 means 200-255 and 0-50)
            if (phase >= r->region_start || phase < r->region_end) {
                memcpy(engine->output_channels, r->channels,
                       engine->score->num_channels);
                return;
            }
        }
    }
}

/**
 * Get current output for a channel
 */
uint8_t smsp_get_output(smsp_engine_t* engine, uint8_t channel) {
    if (channel < SMSP_MAX_CHANNELS) {
        return engine->output_channels[channel];
    }
    return 0;
}

15. Migration from v1.0

15.1 Score Conversion

v1.0 scores (tick-indexed) can be converted to v2.0 (phase-indexed):

def convert_v1_to_v2(v1_score):
    """Convert tick-indexed score to phase-indexed."""

    # Find pattern period from v1 score
    max_tick = max(event.time_offset for event in v1_score.events)
    period_ticks = v1_score.loop_period or (max_tick + 1)

    # Select best prime
    prime_idx = find_closest_prime(period_ticks, PRIMES)
    prime = PRIMES[prime_idx]

    # Scale factor
    scale = prime / period_ticks

    # Convert events to phase-indexed
    v2_events = []
    for event in v1_score.events:
        tick = event.time_offset
        phase = int(tick * scale) % prime

        v2_events.append(smsp_event_v2(
            phases=[phase if i == prime_idx else 0 for i in range(8)],
            phase_mask=(1 << prime_idx),
            channels=event.channels
        ))

    return smsp_score_v2(events=v2_events)

15.2 Engine Compatibility

v2.0 engines can execute v1.0 scores by: 1. Converting at load time (recommended) 2. Using CRT recovery to get tick, then index (slower)


16. Future Extensions

16.1 Multi-Prime Patterns

For patterns requiring more precision than a single prime:

typedef struct {
    uint8_t phases[8];      // All 8 phases must match
    uint8_t phase_mask;     // 0xFF = all must match
    // ...
} smsp_event_multiprime_t;

16.2 Conditional Execution

typedef struct {
    uint8_t condition_type;  // IF_ZONE, IF_ROLE, IF_INPUT
    uint8_t condition_value;
    smsp_region_t region;
} smsp_conditional_region_t;

16.3 Dynamic Scores

Scores generated at runtime based on sensor input or network state.


17. Prior Art Claims

SMSP-related claims are documented in the Claims Appendix:

Claim Range Topic
33-42 Core SMSP structure and execution
50 Scale invariance
82-86 Bidirectional operation
87-100 Sensing and metasurface applications

Appendix A: Quick Reference

A.1 Pattern Classes

Class Value Use Case
BILATERAL 0 EMDR, therapeutic
EMERGENCY 1 SAE J845 lightbars
SWARM_SYNC 2 In-phase coordination
PURSUIT 3 Sequential chase
SENSING 4 Distributed sampling
CUSTOM 255 Raw scores

A.2 Channel Types

Type Value Bytes
LED_MONO 0 1 (brightness)
LED_RGB 1 3 (R, G, B)
LED_RGBW 2 4 (R, G, B, W)
HAPTIC 3 1 (intensity)
AUDIO_FREQ 4 3 (freq_hz LE16, amplitude)
GPIO 5 1 (on/off)

A.3 Standard Primes

Index Prime Typical Use
0 241 ~4.1 Hz patterns
1 251 ~4.0 Hz patterns
2 239 ~4.2 Hz patterns
3 233 ~4.3 Hz patterns
4 229 ~4.4 Hz patterns
5 227 ~4.4 Hz patterns
6 223 ~4.5 Hz patterns
7 211 ~4.7 Hz patterns

(Frequencies assume 1ms tick period)


This document is published as prior art for the Synchronized Multimodal Score Protocol.


PHYRFLY Protocol Family: UTLP | RFIP | SMSP