///|
/// ProcessImageSlice — an offset/length descriptor defining a task's region
/// within the ProcessImage.
///
/// Design rationale:
/// - HIL and multi-axis systems need isolated access to subsets of the PI
/// - Each task/controller owns one or more slices
/// - Slices are bounded and checked at creation, not at every access
/// - Maps directly to DMA descriptor regions on MCU targets
///
/// Note: The copy-based helpers `read_slice_inputs`/`write_slice_outputs`
/// operate on the immutable `ProcessImage` type and allocate on each call.
/// For zero-allocation hot-path access use `slice_inputs_view` and
/// `write_slice_outputs_inplace`, which operate on `@hal.MutableProcessImage`.
///
/// Safety: offsets are validated at slice creation; subsequent access is
/// bounds-checked by the underlying buffer API.
pub(all) struct ProcessImageSlice {
/// Byte offset into the parent ProcessImage outputs.
output_offset : Int
/// Number of output bytes in this slice.
output_length : Int
/// Byte offset into the parent ProcessImage inputs.
input_offset : Int
/// Number of input bytes in this slice.
input_length : Int
} derive(Eq, Debug)
///|
/// TaskDescriptor identifies a logical task that owns one PI partition.
///
/// Modeled after IEC 61131-3 task concept:
/// - Each task has a unique name, a priority level, and a cycle divisor
/// - The cycle divisor controls how often the task executes relative to
/// the base EtherCAT cycle (divisor=1 → every cycle, divisor=4 → every 4th)
/// - Priority determines execution order within a single base cycle
pub(all) struct TaskDescriptor {
/// Unique task name (e.g., "motion_axis_1", "safety_monitor").
name : String
/// Task priority (0 = highest / most urgent).
priority : Int
/// Cycle divisor relative to the base EtherCAT cycle.
cycle_divisor : Int
/// The PI slice this task owns.
slice : ProcessImageSlice
} derive(Eq, Debug)
///|
/// TaskSchedule holds an ordered sequence of tasks, sorted by priority,
/// with the base cycle period from the master configuration.
pub(all) struct TaskSchedule {
/// Base EtherCAT cycle period in nanoseconds.
base_period_ns : Int64
/// Tasks sorted by priority (0 = highest first).
tasks : Array[TaskDescriptor]
} derive(Eq, Debug)
///|
/// Create a ProcessImageSlice from a PdoMapping.
/// Validates that the mapping fits within the given ProcessImage dimensions.
pub fn slice_from_mapping(
mapping : @protocol.PdoMapping,
image : @protocol.ProcessImage,
) -> ProcessImageSlice? {
if mapping.output_offset + mapping.output_length > image.output_size {
return None
}
if mapping.input_offset + mapping.input_length > image.input_size {
return None
}
Some({
output_offset: mapping.output_offset,
output_length: mapping.output_length,
input_offset: mapping.input_offset,
input_length: mapping.input_length,
})
}
///|
/// Partition a PdoContext into per-slave slices.
/// Returns an array of (station_address, slice) pairs.
pub fn partition_by_slave(
ctx : @protocol.PdoContext,
) -> Array[(UInt, ProcessImageSlice)] {
ctx.mappings.map(fn(m) {
(
m.station,
{
output_offset: m.output_offset,
output_length: m.output_length,
input_offset: m.input_offset,
input_length: m.input_length,
},
)
})
}
///|
/// Build a TaskSchedule from task descriptors.
/// Validates: no overlapping output PI regions, no overlapping input PI regions,
/// positive divisors, unique names.
/// Returns None if validation fails.
pub fn build_task_schedule(
base_period_ns : Int64,
tasks : Array[TaskDescriptor],
) -> TaskSchedule? {
// Validate positive divisors
for t in tasks {
if t.cycle_divisor <= 0 {
return None
}
}
// Validate unique names
for i = 0; i < tasks.length(); i = i + 1 {
for j = i + 1; j < tasks.length(); j = j + 1 {
if tasks[i].name == tasks[j].name {
return None
}
}
}
// Validate no overlapping output regions
for i = 0; i < tasks.length(); i = i + 1 {
for j = i + 1; j < tasks.length(); j = j + 1 {
if slices_overlap_output(tasks[i].slice, tasks[j].slice) {
return None
}
}
}
// Validate no overlapping input regions
for i = 0; i < tasks.length(); i = i + 1 {
for j = i + 1; j < tasks.length(); j = j + 1 {
if slices_overlap_input(tasks[i].slice, tasks[j].slice) {
return None
}
}
}
// Sort by priority
let sorted = tasks.copy()
sorted.sort_by(fn(a, b) { a.priority.compare(b.priority) })
Some({ base_period_ns, tasks: sorted })
}
///|
fn slices_overlap_output(a : ProcessImageSlice, b : ProcessImageSlice) -> Bool {
if a.output_length == 0 || b.output_length == 0 {
return false
}
let a_end = a.output_offset + a.output_length
let b_end = b.output_offset + b.output_length
a.output_offset < b_end && b.output_offset < a_end
}
///|
fn slices_overlap_input(a : ProcessImageSlice, b : ProcessImageSlice) -> Bool {
if a.input_length == 0 || b.input_length == 0 {
return false
}
let a_end = a.input_offset + a.input_length
let b_end = b.input_offset + b.input_length
a.input_offset < b_end && b.input_offset < a_end
}
///|
/// Zero-copy read: returns the inputs buffer reference, offset, and length.
/// The caller can index directly into `buf` at `[offset.. (FixedArray[Byte], Int, Int) {
(image.inputs, slice.input_offset, slice.input_length)
}
///|
/// Zero-copy write: writes bytes from `src[src_offset..]` directly into
/// the outputs buffer at the slice's output_offset. No new buffer is
/// allocated; the update is performed in-place.
pub fn write_slice_outputs_inplace(
image : @hal.MutableProcessImage,
slice : ProcessImageSlice,
src : FixedArray[Byte],
src_offset : Int,
) -> Unit {
let available = src.length() - src_offset
let len = if available < slice.output_length {
available
} else {
slice.output_length
}
for i = 0; i < len; i = i + 1 {
image.outputs[slice.output_offset + i] = src[src_offset + i]
}
}
///|
pub fn tasks_due_at_cycle(
schedule : TaskSchedule,
cycle : Int64,
) -> Array[TaskDescriptor] {
schedule.tasks.filter(fn(t) { cycle % t.cycle_divisor.to_int64() == 0L })
}
///|
/// Read input bytes for a slice from a ProcessImage.
pub fn read_slice_inputs(
image : @protocol.ProcessImage,
slice : ProcessImageSlice,
) -> Bytes {
if slice.input_length == 0 {
return Bytes::new(0)
}
Bytes::makei(slice.input_length, fn(i) {
image.inputs[slice.input_offset + i]
})
}
///|
/// Write output bytes for a slice into a ProcessImage.
pub fn write_slice_outputs(
image : @protocol.ProcessImage,
slice : ProcessImageSlice,
data : Bytes,
) -> Unit {
let len = if data.length() < slice.output_length {
data.length()
} else {
slice.output_length
}
let buf = Buffer(size_hint=image.output_size)
for i = 0; i < image.output_size; i = i + 1 {
if i >= slice.output_offset && i < slice.output_offset + len {
buf.write_byte(data[i - slice.output_offset])
} else {
buf.write_byte(image.outputs[i])
}
}
image.outputs = buf.to_bytes()
}