///|
priv suberror InputError {
InputError(String, String)
}
///|
priv struct Request {
devices : Array[Device]
baseline : Array[Device]
mode : String
max_nodes : Int
reserved_pins : Array[Int]
}
///|
fn[T] input_error(message : String) -> T raise InputError {
raise InputError("INPUT_SCHEMA", message)
}
///|
fn object_fields(
value : Json,
path : String,
) -> Map[String, Json] raise InputError {
match value {
Object(fields) => fields
_ => input_error("\{path}: expected an object")
}
}
///|
fn reject_unknown(
fields : Map[String, Json],
allowed : Array[String],
path : String,
) -> Unit raise InputError {
for key, _ in fields {
if !allowed.contains(key) {
raise InputError("UNKNOWN_FIELD", "\{path}.\{key}: unknown field")
}
}
}
///|
fn required(
fields : Map[String, Json],
key : String,
path : String,
) -> Json raise InputError {
match fields.get(key) {
Some(value) => value
None => input_error("\{path}.\{key}: required field is missing")
}
}
///|
fn string_value(value : Json, path : String) -> String raise InputError {
match value {
String(text) => text
_ => input_error("\{path}: expected a string")
}
}
///|
fn integer_value(value : Json, path : String) -> Int raise InputError {
match value {
Number(n, ..) if n >= -2147483648.0 && n <= 2147483647.0 => {
let result = n.to_int()
if result.to_double() != n {
input_error("\{path}: expected an integer, not a fractional number")
} else {
result
}
}
_ => input_error("\{path}: expected a finite 32-bit integer")
}
}
///|
fn decode_device(
value : Json,
index : Int,
collection? : String = "devices",
) -> Device raise InputError {
let path = "\{collection}[\{index}]"
let fields = object_fields(value, path)
reject_unknown(
fields,
["id", "kind", "pins", "instance", "bus", "frequency_hz", "i2c_address"],
path,
)
let id = string_value(required(fields, "id", path), "\{path}.id")
let kind = match
string_value(required(fields, "kind", path), "\{path}.kind") {
"uart" => Uart
"spi" => Spi
"i2c" => I2c
"pwm" => Pwm
_ =>
raise InputError(
"UNKNOWN_KIND",
"\{path}.kind: use uart, spi, i2c or pwm",
)
}
let pins : Map[String, Int] = Map([])
match fields.get("pins") {
Some(value) => {
let pin_fields = object_fields(value, "\{path}.pins")
for signal, pin in pin_fields {
pins[signal] = integer_value(pin, "\{path}.pins.\{signal}")
}
}
None => ()
}
let instance = match fields.get("instance") {
None | Some(Null) => None
Some(value) => Some(integer_value(value, "\{path}.instance"))
}
let bus = match fields.get("bus") {
None | Some(Null) => None
Some(value) => Some(string_value(value, "\{path}.bus"))
}
let frequency_hz = match fields.get("frequency_hz") {
None => if kind == Pwm { 1000 } else { 0 }
Some(value) => integer_value(value, "\{path}.frequency_hz")
}
let i2c_address = match fields.get("i2c_address") {
None | Some(Null) => None
Some(value) => Some(integer_value(value, "\{path}.i2c_address"))
}
device(id, kind, pins~, instance~, bus~, frequency_hz~, i2c_address~)
}
// Core 0.10.14 exposes this depth parameter with a deprecated label_migration
// annotation. Keep its actual protection, confining compatibility to this call.
///|
#warnings("-deprecated")
fn parse_bounded(text : String) -> Json raise InputError {
@json.parse(text, max_nesting_depth=32) catch {
_ =>
raise InputError(
"JSON_PARSE", "Malformed JSON or nesting deeper than 32 levels.",
)
}
}
///|
fn decode_request(text : String) -> Request raise InputError {
if text.length() > 65536 {
raise InputError("INPUT_SIZE", "Input exceeds 65536 UTF-16 code units.")
}
let value = parse_bounded(text)
let fields = object_fields(value, "$root")
reject_unknown(
fields,
["board", "mode", "devices", "max_nodes", "baseline", "reserved_pins"],
"$root",
)
let board = match fields.get("board") {
None => "pico"
Some(value) => string_value(value, "board")
}
if board != "pico" {
raise InputError(
"UNKNOWN_BOARD", "Only the original Raspberry Pi Pico (board=pico) is supported.",
)
}
let mode = match fields.get("mode") {
None => "plan"
Some(value) => string_value(value, "mode")
}
if !["plan", "check", "repair", "diagnose"].contains(mode) {
raise InputError(
"UNKNOWN_MODE", "mode must be plan, check, repair or diagnose.",
)
}
if mode != "repair" && fields.contains("baseline") {
raise InputError(
"BASELINE_MODE", "baseline is only accepted in repair mode.",
)
}
let max_nodes = match fields.get("max_nodes") {
None => 50000
Some(value) => integer_value(value, "max_nodes")
}
if max_nodes < 0 || max_nodes > 200000 {
raise InputError("INVALID_BUDGET", "max_nodes must be in 0..200000.")
}
let values = match required(fields, "devices", "$root") {
Array(values) => values
_ => input_error("devices: expected an array")
}
if values.length() > 16 {
raise InputError("DEVICE_LIMIT", "At most 16 devices are supported.")
}
let devices : Array[Device] = []
for index = 0; index < values.length(); index = index + 1 {
devices.push(decode_device(values[index], index))
}
let baseline : Array[Device] = []
if mode == "repair" {
let values = match required(fields, "baseline", "$root") {
Array(values) => values
_ => input_error("baseline: expected an array")
}
if values.length() > 16 {
raise InputError(
"DEVICE_LIMIT", "At most 16 baseline devices are supported.",
)
}
for index = 0; index < values.length(); index = index + 1 {
baseline.push(decode_device(values[index], index, collection="baseline"))
}
}
let reserved_pins : Array[Int] = match fields.get("reserved_pins") {
None => []
Some(Array(values)) => {
if values.length() > 26 {
input_error("reserved_pins: at most 26 GPIOs")
}
values.map(v => integer_value(v, "reserved_pins"))
}
_ => input_error("reserved_pins: expected an integer array")
}
{ devices, baseline, mode, max_nodes, reserved_pins, }
}
///|
/// Check fully specified devices, inferring a peripheral instance from mappings.
pub fn check(
devices : Array[Device],
reserved_pins? : Array[Int] = [],
) -> Report {
let findings = validate_devices(devices)
for item in validate_reserved_pins(reserved_pins) {
findings.push(item)
}
let assignments : Array[Assignment] = []
for d in devices {
let mut complete = true
for signal in signals(d.kind) {
if !d.pins.contains(signal) {
complete = false
findings.push(
finding(
"MISSING_SIGNAL",
"check mode requires every signal to have an explicit GPIO.",
[d.id],
signal,
),
)
}
}
if complete {
let cs = candidates(d)
if cs.length() == 0 {
findings.push(
finding(
"SIGNAL_MAPPING",
"No peripheral instance accepts all explicitly assigned signals.",
[d.id],
d.kind.name(),
),
)
} else {
assignments.push(cs[0])
}
}
}
if findings.length() != 0 {
return { status: Invalid, assignments: [], findings, nodes: 0, }
}
let conflicts = check_assignments(assignments, reserved_pins~)
{
status: if conflicts.length() == 0 {
Solved
} else {
Invalid
},
assignments,
findings: conflicts,
nodes: 0,
}
}
///|
/// Convert a report to the versioned JSON response protocol.
pub fn report_json(
report : Report,
mode? : String = "plan",
reserved_pins? : Array[Int] = [],
repair_optimal? : Bool? = None,
) -> Json {
let assignments = report.assignments.map(fn(a) {
let bus : Json = match a.bus {
Some(b) => Json::string(b)
None => Json::null()
}
Json::object({
"id": Json::string(a.id),
"kind": Json::string(a.kind.name()),
"instance": @json.to_json(a.instance),
"pins": @json.to_json(a.pins),
"bus": bus,
"frequency_hz": @json.to_json(a.frequency_hz),
"i2c_address": match a.i2c_address {
Some(n) => @json.to_json(n)
None => Json::null()
},
})
})
let findings = report.findings.map(fn(f) {
Json::object({
"code": Json::string(f.code),
"message": Json::string(f.message),
"devices": @json.to_json(f.devices),
"resource": Json::string(f.resource),
})
})
let status = match report.status {
Solved => "solved"
Invalid => "invalid"
Unsatisfiable => "unsatisfiable"
Limit => "limit"
}
let fields : Map[String, Json] = {
"schema_version": @json.to_json(1),
"reserved_pins": @json.to_json(reserved_pins),
"board": Json::string("pico"),
"mode": Json::string(mode),
"status": Json::string(status),
"assignments": Json::array(assignments),
"findings": Json::array(findings),
"nodes": @json.to_json(report.nodes),
}
if report.status == Solved &&
check_assignments(report.assignments, reserved_pins~).is_empty() {
fields["exports"] = Json::object({
"csv": match
export_wiring_csv(report.assignments, reserved_pins~, repair_optimal~) {
Some(s) => Json::string(s)
None => Json::null()
},
"pico_header": match
export_pico_header(report.assignments, reserved_pins~, repair_optimal~) {
Some(s) => Json::string(s)
None => Json::null()
},
})
fields["wiring"] = Json::array(
wiring_rows(report.assignments, reserved_pins~).map(fn(row) {
Json::object({
"device": Json::string(row.device),
"signal": Json::string(row.signal),
"gpio": @json.to_json(row.gpio),
"header_pin": @json.to_json(row.header_pin),
"resource": Json::string(row.resource),
"bus": match row.bus {
Some(bus) => Json::string(bus)
None => Json::null()
},
})
}),
)
}
Json::object(fields)
}
///|
fn with_report_field(report : Json, name : String, value : Json) -> Json {
match report {
Object(fields) => {
fields[name] = value
Json::object(fields)
}
_ => report
}
}
///|
fn repair_json(result : RepairReport, reserved_pins? : Array[Int] = []) -> Json {
with_report_field(
report_json(
result.report,
mode="repair",
reserved_pins~,
repair_optimal=Some(result.optimal),
),
"repair",
Json::object({
"changed_pins": match result.changed_pins {
Some(n) => @json.to_json(n)
None => Json::null()
},
"optimal": @json.to_json(result.optimal),
"changes": Json::array(
result.changes.map(fn(change) {
Json::object({
"device": Json::string(change.device),
"signal": Json::string(change.signal),
"from_gpio": @json.to_json(change.from_gpio),
"to_gpio": @json.to_json(change.to_gpio),
})
}),
),
}),
)
}
///|
fn diagnosis_json(
result : DiagnosisReport,
reserved_pins? : Array[Int] = [],
) -> Json {
with_report_field(
report_json(result.report, mode="diagnose", reserved_pins~),
"diagnosis",
Json::object({
"core_devices": @json.to_json(result.core_devices),
"irreducible": @json.to_json(result.irreducible),
}),
)
}
///|
/// Plan, check, repair or diagnose bounded JSON and always return a JSON report.
pub fn run_json(text : String) -> String {
let report = try {
let request = decode_request(text)
match request.mode {
"repair" =>
repair_json(
repair(
request.devices,
request.baseline,
reserved_pins=request.reserved_pins,
max_nodes=request.max_nodes,
),
reserved_pins=request.reserved_pins,
)
"diagnose" =>
diagnosis_json(
diagnose(
request.devices,
reserved_pins=request.reserved_pins,
max_nodes=request.max_nodes,
),
reserved_pins=request.reserved_pins,
)
"check" =>
report_json(
check(request.devices, reserved_pins=request.reserved_pins),
mode="check",
reserved_pins=request.reserved_pins,
)
_ =>
report_json(
plan(
request.devices,
reserved_pins=request.reserved_pins,
max_nodes=request.max_nodes,
),
reserved_pins=request.reserved_pins,
mode="plan",
)
}
} catch {
InputError(code, message) =>
report_json(
{
status: Invalid,
assignments: [],
findings: [finding(code, message, [], "input")],
nodes: 0,
},
mode="unknown",
)
}
report.stringify(indent=2)
}