Skip to content

SEB scenes and Event Player in FW 0.12.11

Use Berry SEB.land and SEB.emit in firmware 0.12.11 for causal scenes and timed Event-value segments.

SEB (Spectoda Event Batch) is a small binary file containing discrete Event values/records. Each record contains a native value, label, Spectoda ID, priority and an absolute time offset inside the segment. Landing or emitting a record produces normal EventStateUpdate processing through EventStore.

SEB is not a complete Player or Project format. It contains no scene name, Track name, loop or seek rules. Project Berry owns those policies. The static player.show file is the bounded directory for deterministic per-ID SEB chains stored in NetworkStorage.

One SEB segment contains at most 340 records, occupies at most 4,092 bytes and lasts at most 65,535 ms. This fills one 4,096-byte LittleFS data-block budget; filesystem metadata is stored separately. The runtime still accepts at most 340 due events in one atomic contribution, so one complete maximum Cue can commit atomically. A 341st pending event fails before any queue, EventStore or cursor mutation. A long Track is an ordered collection of bounded segments. Firmware owns the normative byte layout and validation rules in components/spectoda-library/docs/SEB_V1.md; this is the exact FW 0.12.11 review revision.

Use SEB.land on every prepared controller for a distributed Player. Events enter each local EventStore with the same Network time without an immediate execute-bytecode broadcast. Normal EventStore synchronization can later bring a controller that missed the landing up to date.

Use SEB.emit only when exactly one authoritative source exists. Calling it on every controller would multiply network event traffic.

The scalar API remains available in FW 0.12.11:

spectoda.landEvent(label, value, ids, vtype)
spectoda.landEvent(label, value, ids, vtype, event_millis)
spectoda.emitEvent(label, value, ids, vtype)
spectoda.emitEvent(label, value, ids, vtype, event_millis)

ids may be one ID, a list of IDs, or nil for broadcast. Optional event_millis preserves the exact causal Controller-local time of the source event. Use the SEB component for batches; it is not a plural overload of the scalar method.

var result = SEB.land("segment-000.seb", {
"source": "networkStorage",
"at": segment_at,
"cursor": cursor,
"until": position
})
if result["ok"]
cursor = result["cursor"]
else
print("SEB error", result["error"])
end
  • at is the wrapping local controller.millis() token corresponding to offset zero. Timed segments require it.
  • cursor is the first source record that has not been consumed yet. It defaults to 0.
  • until is the inclusive due offset. It defaults to 0.
  • source: "networkStorage" makes the first argument a filename. Without it, the first argument remains a Berry bytes value.

One call consumes the maximal due prefix. It validates and reserves the whole contribution before applying it atomically. A failure leaves EventStore, queues and cursor unchanged, so Berry can retry safely.

One args.at contract covers three explicit time domains:

  • timeline.at(segmentStart) projects a Player timeline position to a local wrapping millis token;
  • event_millis from an EVS.cb trigger preserves an event’s Network-clock causality for scene recall;
  • controller.millis() means now on this Controller.

Network clock remains the EventStore ordering authority. SEB only accepts the local token and has no timeline-specific overload.

SEB retains no Berry pointer, filename, file handle, cursor or Player state. The Project must keep at bit-identical for the exact source/cursor pair. It resets or replaces the cursor when changing segment, seeking, looping or changing the timing projection.

The reference Player handles pause/resume as explicit outer policy. It derives a fresh filename/at pair with the new projection and carries only the known first-unconsumed source index. Do not reuse that number with a different source or an arbitrary at.

A scene is an ordinary mixed-ID SEB with duration_ms = 0 and all offsets at zero. A trigger Event value can carry the scene label. Its EVS.cb callback also receives the exact local time of the source event; pass that value as at.

var scene = EVS("scene", ID255)
scene.cb = def(value, event_id, event_millis)
if event_millis == nil || !value.is(31)
return
end
var name = value.get(31) + ".seb"
var result = SEB.land(name, {
"source": "networkStorage",
"at": event_millis
})
if !result["ok"]
print("Invalid scene", result["error"])
end
end

Do not compile the $scene[ID255] trigger itself into the scene file, or the recall would recurse.

Scene recall never calls timeline.at(). It stays causal while the timeline is paused, because the callback’s event_millis came from the trigger event.

The small copy-ready Player reads <base>.show and polls timeline.getState(). While paused is true it makes no SEB call. Normal forward playback selects the next complete Cue from a per-ID segment, calculates at through timeline.at(segmentStart) and passes the current relative position as until.

timeline.at(position) records that the current timeline reference position P was valid at Network clock C, projects C + (position - P), and converts that clock to this Controller’s signed wrapping local token. The same formula is used while running and paused, so Controllers with different command processing latency still resolve the same Network timestamp. The call returns nil when the target clock is invalid or outside the unambiguous ±2^31 ms wrapping window.

Timeline position is a separate 24-hour ring, 0..86,399,999; 86,400,000 normalizes to 0. Midnight therefore remains continuous for the Player, Layers and animations. Projection selects the nearest occurrence within ±12 hours; an exact 12-hour delta is ambiguous and returns nil.

Pause/resume keeps the timeline epoch but resume creates a new causal projection; the Player reopens the remaining segment timing while retaining its already-consumed source prefix. Seek and loop change the epoch. Old timing state and cursors must then be discarded and rebuilt from a complete checkpoint.

Seeking is not a replay of every skipped event. The static player.show directory lets Project Berry locate the last complete Cue at or before the target independently for every relevant Track. It lands those Cue values at the seek time, then continues after them. Skipped historical side effects are not fired again.

SEB never mutates EventState behind EventStore. The latest EventStateUpdate therefore remains authoritative after historical synchronization, and an externally emitted event can change Player state through the normal path.

The examples use synthetic IDs and exact NetworkStorage artifacts for FW 0.12.11. To author a show in Studio, continue to Event Player in Spectoda Studio.