1380 lines
45 KiB
JSON
1380 lines
45 KiB
JSON
{
|
||
"$schema": "../../../tools/card-layouts/schemas/card-source.schema.json",
|
||
"schema": "esc-card-source.v1",
|
||
"card": {
|
||
"id": "mpabi-freertos-c-06-software-timers",
|
||
"series": "freertos-c",
|
||
"series_title": "FreeRTOS C",
|
||
"number": "06",
|
||
"count": "15",
|
||
"slug": "software-timers",
|
||
"title": "Software timers i timer daemon",
|
||
"topic": "one-shot, auto-reload, command queue i callback",
|
||
"project": "Freestanding C nad FreeRTOS",
|
||
"subject": "Informatyka",
|
||
"level": "Rok 2 · L06 · RV32I/Hazard3",
|
||
"revision_date": "2026-07-19T00:00:00+02:00",
|
||
"status": "Gotowa",
|
||
"version": "v00.01",
|
||
"uuid": "40613f0f-6688-53f0-8c20-e87c6ef73c4e",
|
||
"author": "M. Pabiszczak",
|
||
"year": "2026"
|
||
},
|
||
"generated": {
|
||
"tex": "doc/generated/main.tex",
|
||
"html": "web/index.html",
|
||
"html_css": "web/style.css",
|
||
"html_tree_inspector": false,
|
||
"react_app": true
|
||
},
|
||
"render_dictionary": false,
|
||
"viewpoints": [
|
||
{
|
||
"id": "A1",
|
||
"label": "CONTEXT",
|
||
"subtitle": "Application → command queue → daemon",
|
||
"status": "enabled",
|
||
"target_label": "fig:a1-context"
|
||
},
|
||
{
|
||
"id": "A2",
|
||
"label": "STRUCTURE",
|
||
"subtitle": "Struktura",
|
||
"status": "unavailable",
|
||
"reason": "Timer_t i TimerTag są pokazane dokładniej w A3 i A7."
|
||
},
|
||
{
|
||
"id": "A3",
|
||
"label": "DISPATCH",
|
||
"subtitle": "TimerHandle_t, pvTimerID i callback",
|
||
"status": "enabled",
|
||
"target_label": "fig:a3-dispatch"
|
||
},
|
||
{
|
||
"id": "A4",
|
||
"label": "APPLICATION",
|
||
"subtitle": "Eksperyment",
|
||
"status": "unavailable",
|
||
"reason": "One-shot i auto-reload są kompletne w A5."
|
||
},
|
||
{
|
||
"id": "A5",
|
||
"label": "FLOW",
|
||
"subtitle": "E01–E10: commands i expiry",
|
||
"status": "enabled",
|
||
"target_label": "fig:a5-flow"
|
||
},
|
||
{
|
||
"id": "A6",
|
||
"label": "STATE",
|
||
"subtitle": "Inactive, active, callback, reload",
|
||
"status": "enabled",
|
||
"target_label": "fig:a6-state"
|
||
},
|
||
{
|
||
"id": "A7",
|
||
"label": "RUNTIME",
|
||
"subtitle": "Daemon TCB, callback stack i tick",
|
||
"status": "enabled",
|
||
"target_label": "fig:a7-runtime"
|
||
},
|
||
{
|
||
"id": "A8",
|
||
"label": "PATTERNS",
|
||
"subtitle": "Wzorce",
|
||
"status": "unavailable",
|
||
"reason": "Wzorzec callback adapter jest już jawny w A3; karta skupia się na semantyce daemon task."
|
||
}
|
||
],
|
||
"debug_strategies": {
|
||
"code.timer-contract": {
|
||
"id": "code.timer-contract",
|
||
"kind": "code",
|
||
"action": "open",
|
||
"expected_observations": [
|
||
"daemon ma priorytet 3 i własny stack",
|
||
"jeden callback rozróżnia dwa IDs"
|
||
],
|
||
"assertions": [
|
||
"callback nie blokuje",
|
||
"TimerHandle_t nie jest application task handle"
|
||
],
|
||
"evidence_fields": [
|
||
"code_ref",
|
||
"config",
|
||
"symbol"
|
||
],
|
||
"prerequisites": [
|
||
"źródło FC06"
|
||
],
|
||
"layout": [
|
||
"FreeRTOSConfig",
|
||
"TimerTag",
|
||
"callback"
|
||
],
|
||
"commands": [
|
||
"rg -n 'configTIMER|TimerTag|fc06_timer_callback' include src"
|
||
]
|
||
},
|
||
"run.commands": {
|
||
"id": "run.commands",
|
||
"kind": "run",
|
||
"action": "replay",
|
||
"expected_observations": [
|
||
"pdPASS oznacza enqueue",
|
||
"callback następuje później"
|
||
],
|
||
"assertions": [
|
||
"zero block time",
|
||
"queue length 8"
|
||
],
|
||
"evidence_fields": [
|
||
"event",
|
||
"accepted",
|
||
"tick",
|
||
"PC",
|
||
"timestamp"
|
||
],
|
||
"prerequisites": [
|
||
"Hazard3 online",
|
||
"czysta RAM"
|
||
],
|
||
"layout": [
|
||
"controller",
|
||
"timer queue",
|
||
"daemon",
|
||
"active lists"
|
||
],
|
||
"commands": [
|
||
"print g_fc06.start_accepted",
|
||
"print g_fc06.reset_accepted",
|
||
"print g_fc06.change_accepted"
|
||
]
|
||
},
|
||
"run.callback": {
|
||
"id": "run.callback",
|
||
"kind": "run",
|
||
"action": "replay",
|
||
"expected_observations": [
|
||
"callback wykonuje daemon",
|
||
"ID wskazuje TimerTag"
|
||
],
|
||
"assertions": [
|
||
"daemon != controller",
|
||
"callback SP != controller SP"
|
||
],
|
||
"evidence_fields": [
|
||
"timer kind",
|
||
"daemon handle",
|
||
"SP",
|
||
"tick"
|
||
],
|
||
"prerequisites": [
|
||
"timer active"
|
||
],
|
||
"layout": [
|
||
"callback source",
|
||
"pvTimerID",
|
||
"daemon TCB",
|
||
"SP"
|
||
],
|
||
"commands": [
|
||
"print g_fc06.daemon_seen",
|
||
"print g_fc06.callback_sp",
|
||
"print g_fc06.auto_tick"
|
||
]
|
||
},
|
||
"run.pass": {
|
||
"id": "run.pass",
|
||
"kind": "run",
|
||
"action": "replay",
|
||
"expected_observations": [
|
||
"auto 2/4/7",
|
||
"one-shot 6 raz"
|
||
],
|
||
"assertions": [
|
||
"10 records",
|
||
"digest stabilny"
|
||
],
|
||
"evidence_fields": [
|
||
"PASS",
|
||
"timeline",
|
||
"digest",
|
||
"ELF hash",
|
||
"timestamp"
|
||
],
|
||
"prerequisites": [
|
||
"controller_done=1"
|
||
],
|
||
"layout": [
|
||
"timeline",
|
||
"counts",
|
||
"digest"
|
||
],
|
||
"commands": [
|
||
"print g_fc06",
|
||
"print g_fc06_pass"
|
||
]
|
||
}
|
||
},
|
||
"template": "templates/karta-klasyczna.json",
|
||
"title_block": {
|
||
"category": "KARTA PRACY · INFORMATYKA",
|
||
"prepared_by": "M. Pabiszczak",
|
||
"prepared_on": "2026-07-19T00:00:00+02:00",
|
||
"title": "Software timers i timer daemon",
|
||
"url": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/ab188300-f03d-5ddb-8316-b9534876a5bc",
|
||
"repository_url": "https://zsl-gitea.mpabi.pl/edu-freertos-c/lab-rv32i-freertos-c-software-timers",
|
||
"url_host_uuid": "dce7fb9d-7b2f-5d49-96a2-3a30d3070b84",
|
||
"url_domain": "mpabi.pl",
|
||
"doc_uuid": "ab188300-f03d-5ddb-8316-b9534876a5bc",
|
||
"revision": "v00.01",
|
||
"issued_on": "2026-07-19T00:00:00+02:00",
|
||
"series": "FREERTOS-C-06",
|
||
"document_type": "karta pracy",
|
||
"tool": "card-layouts",
|
||
"show_qr": true,
|
||
"show_repository_qr": true,
|
||
"height_cm": 2.6,
|
||
"repeat_on_every_page": true,
|
||
"replace_front_matter": true
|
||
},
|
||
"front_page_scope": {
|
||
"title": "Cel karty",
|
||
"content_tex": "Uczeń rozdziela przyjęcie komendy timera od jej wykonania przez timer daemon oraz bada callback na osobnym stosie taska systemowego.",
|
||
"scope_title": "Zakres karty",
|
||
"scope_content_tex": "Controller uruchamia one-shot i auto-reload, resetuje one-shot przed pierwotnym terminem, po dwóch callbackach zmienia okres auto z 2 na 3 ticki i zatrzymuje go po trzecim. Callback rozpoznaje timer przez pvTimerID i zapisuje wyłącznie bounded evidence.",
|
||
"scope_table": {
|
||
"headers": [
|
||
"Lekcja",
|
||
"Task",
|
||
"Najważniejsza idea",
|
||
"Priorytet",
|
||
"Status",
|
||
"Version"
|
||
],
|
||
"rows": [
|
||
{
|
||
"chapter": "FC06",
|
||
"task": "Task01",
|
||
"idea_tex": "one-shot, auto-reload, command queue i callback",
|
||
"priority": "główny",
|
||
"status": "ready",
|
||
"version": "v00.01",
|
||
"key": true
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"side_margin_tree_layout": {
|
||
"columns": [
|
||
{
|
||
"id": "zawodowe",
|
||
"label": "TECH",
|
||
"side": "left",
|
||
"tree": "WE -> EK -> KW",
|
||
"description": "Dowody runtime."
|
||
},
|
||
{
|
||
"id": "ogolne",
|
||
"label": "OG",
|
||
"side": "right",
|
||
"tree": "WE -> EN -> KW",
|
||
"description": "Model FreeRTOS C."
|
||
}
|
||
]
|
||
},
|
||
"learning_effects": {
|
||
"FC06.EN01": {
|
||
"bloom_level": "Analiza",
|
||
"label": "Model timer service",
|
||
"text": "Uczeń rozdziela command queue, active list, daemon task i callback.",
|
||
"assessment_criteria": [
|
||
"FC06.KW01"
|
||
]
|
||
},
|
||
"FC06.EK01": {
|
||
"bloom_level": "Zastosowanie",
|
||
"label": "Replay one-shot i auto-reload",
|
||
"text": "Uczeń odtwarza E01–E10 i wiąże command acceptance, expiry tick, TimerTag, daemon TCB i stack.",
|
||
"assessment_criteria": [
|
||
"FC06.KW01"
|
||
]
|
||
}
|
||
},
|
||
"assessment_criteria": {
|
||
"FC06.KW01": {
|
||
"text": "Program kończy się PASS; one-shot=6 raz; auto=2/4/7; komendy są accepted; daemon i controller mają różne handles/SP; digest jest deterministyczny.",
|
||
"learning_effects": [
|
||
"FC06.EN01",
|
||
"FC06.EK01"
|
||
]
|
||
}
|
||
},
|
||
"educational_requirements": {
|
||
"FC06.WE01": {
|
||
"text": "Zaprojektowanie i zbadanie deterministycznego eksperymentu software timers.",
|
||
"label": "one-shot, auto-reload, command queue i callback",
|
||
"learning_effects": [
|
||
"FC06.EN01",
|
||
"FC06.EK01"
|
||
],
|
||
"learning_tree": {
|
||
"schema": "we-learning-tree.v1",
|
||
"policy": "Każda aktywna kotwica wskazuje kod lub checkpoint.",
|
||
"ogolne": [
|
||
{
|
||
"effect_ref": "FC06.EN01",
|
||
"display": "EN LOCAL RTOS.06",
|
||
"source": "LOCAL",
|
||
"official": "RTOS",
|
||
"local": "06",
|
||
"kind": "EN",
|
||
"tree_id": "FC06.WE01.OG.LOCAL.RTOS.06",
|
||
"text": "Uczeń rozdziela command queue, active list, daemon task i callback.",
|
||
"kw": [
|
||
{
|
||
"criterion_ref": "FC06.KW01",
|
||
"display": "KW LOCAL RTOS.06",
|
||
"source": "LOCAL",
|
||
"kind": "KW",
|
||
"official": "RTOS",
|
||
"local": "06",
|
||
"text": "Łączy kod, diagram i zweryfikowany dowód."
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"zawodowe": [
|
||
{
|
||
"effect_ref": "FC06.EK01",
|
||
"display": "EK LOCAL RTOS.06",
|
||
"source": "LOCAL",
|
||
"official": "RTOS",
|
||
"local": "06",
|
||
"kind": "EK",
|
||
"tree_id": "FC06.WE01.TECH.LOCAL.RTOS.06",
|
||
"text": "Uczeń odtwarza E01–E10 i wiąże command acceptance, expiry tick, TimerTag, daemon TCB i stack.",
|
||
"kw": [
|
||
{
|
||
"criterion_ref": "FC06.KW01",
|
||
"display": "KW LOCAL RTOS.06",
|
||
"source": "LOCAL",
|
||
"kind": "KW",
|
||
"official": "RTOS",
|
||
"local": "06",
|
||
"text": "Łączy kod, diagram i zweryfikowany dowód."
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
},
|
||
"debug_checkpoints": {
|
||
"schema": "stem-debug-checkpoints.v1",
|
||
"semantics": "deterministic-replay",
|
||
"artifact": {
|
||
"source": "src/tasks/task01_software_timers.c",
|
||
"source_git_blob": "c024c7e4d5e9209ed6d11aab2645b8983d89532b",
|
||
"elf": "build/task01_software_timers/prog.elf",
|
||
"hazard3_elf_sha256": "8c7b6da680ecaba4a0447ff223bcfd6f3266cd7c059f23f3e0503d71bc155b9d",
|
||
"hazard3_image": "build/task01_software_timers/prog.bin",
|
||
"hazard3_image_sha256": "93a38d315b3b709aee047ad2d556d1e3c8e183a614507283a152a5117d1a6133"
|
||
},
|
||
"targets": {
|
||
"hazard3-sim": {
|
||
"adapter": "hazard3-reset-load-replay.v1",
|
||
"baseline": "restart-backend",
|
||
"clean_ram": true
|
||
}
|
||
},
|
||
"items": {
|
||
"task01.config": {
|
||
"event_id": "E01",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 1"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06_event_count",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06.one_shot == 0",
|
||
"equals": 1
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.started": {
|
||
"event_id": "E02",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 2"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.start_accepted",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06.auto_count",
|
||
"equals": 0
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.reset": {
|
||
"event_id": "E03",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 3"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.reset_accepted",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06.one_shot_count",
|
||
"equals": 0
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.auto-1": {
|
||
"event_id": "E04",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 4"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.auto_count",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06.auto_tick[0]",
|
||
"equals": 2
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.auto-2": {
|
||
"event_id": "E05",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 5"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.auto_count",
|
||
"equals": 2
|
||
},
|
||
{
|
||
"expr": "g_fc06.auto_tick[1]",
|
||
"equals": 4
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.period-change": {
|
||
"event_id": "E06",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 6"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.change_accepted",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06.auto_count",
|
||
"equals": 2
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.one-shot": {
|
||
"event_id": "E07",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 7"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.one_shot_count",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06.one_shot_tick",
|
||
"equals": 6
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.auto-3": {
|
||
"event_id": "E08",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 8"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.auto_count",
|
||
"equals": 3
|
||
},
|
||
{
|
||
"expr": "g_fc06.auto_tick[2]",
|
||
"equals": 7
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.stopped": {
|
||
"event_id": "E09",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 9"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06.stop_accepted",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06.controller_done",
|
||
"equals": 1
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"task01.pass": {
|
||
"event_id": "E10",
|
||
"stop": {
|
||
"symbol": "fc06_checkpoint_committed",
|
||
"offset": 0,
|
||
"condition": "$a0 == 10"
|
||
},
|
||
"verify": {
|
||
"expressions": [
|
||
{
|
||
"expr": "g_fc06_pass",
|
||
"equals": 1
|
||
},
|
||
{
|
||
"expr": "g_fc06_event_count",
|
||
"equals": 10
|
||
},
|
||
{
|
||
"expr": "g_fc06.daemon_seen != g_fc06.controller",
|
||
"equals": 1
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"sections": [
|
||
{
|
||
"title": "A1 — Context",
|
||
"order": 10,
|
||
"content_kind": "prose",
|
||
"asset_page_mode": "one-per-page",
|
||
"page_orientation": "landscape",
|
||
"content_tex": "Task aplikacyjny nie wywołuje callbacku. Wysyła komendę do kolejki, a timer daemon aktualizuje listy i później uruchamia callback.",
|
||
"assets": [
|
||
{
|
||
"path": "assets/a1-context.png",
|
||
"html_path": "assets/a1-context.svg",
|
||
"source_path": "assets/a1-context.puml",
|
||
"caption": "A1 CONTEXT — controller, command queue, daemon i callback.",
|
||
"label": "fig:a1-context",
|
||
"alt": "Warstwy software timer service.",
|
||
"kind": "diagram",
|
||
"width": 1.0,
|
||
"page_grid": {
|
||
"columns": 1,
|
||
"rows": 1,
|
||
"page_width": 940,
|
||
"page_height": 560,
|
||
"overview": true,
|
||
"step_tiles": {
|
||
"app": 1,
|
||
"queue": 1,
|
||
"daemon": 1,
|
||
"lists": 1,
|
||
"callback": 1
|
||
}
|
||
},
|
||
"interactive": {
|
||
"kind": "uml-class",
|
||
"storage_key": "fc06-a1-context",
|
||
"title": "A1 · granice timer service",
|
||
"task": {
|
||
"id": "task01",
|
||
"label": "Task01 · software timers"
|
||
},
|
||
"block": {
|
||
"id": "a1",
|
||
"label": "A1 CONTEXT",
|
||
"description": "Dwie płaszczyzny: application command i daemon execution."
|
||
},
|
||
"phases": [
|
||
{
|
||
"id": "context",
|
||
"label": "COMMAND / DAEMON / CALLBACK",
|
||
"steps": [
|
||
{
|
||
"id": "app",
|
||
"number": 1,
|
||
"label": "application submits commands",
|
||
"mode": "RUN",
|
||
"event_id": "E02",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "01",
|
||
"code_ref": "src/tasks/task01_software_timers.c:122",
|
||
"snapshot_ref": "task01.started",
|
||
"description": "API zwraca stan wysłania komendy.",
|
||
"evidence": "start_accepted=1."
|
||
},
|
||
{
|
||
"id": "queue",
|
||
"number": 2,
|
||
"label": "timer queue decouples execution",
|
||
"mode": "CODE",
|
||
"strategy_ref": "code.timer-contract",
|
||
"svg_label": "02",
|
||
"code_ref": "include/FreeRTOSConfig.h:29",
|
||
"description": "Długość 8 ogranicza pending commands.",
|
||
"evidence": "configTIMER_QUEUE_LENGTH."
|
||
},
|
||
{
|
||
"id": "daemon",
|
||
"number": 3,
|
||
"label": "daemon owns command processing",
|
||
"mode": "RUN",
|
||
"event_id": "E04",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "03",
|
||
"code_ref": "src/tasks/task01_software_timers.c:77",
|
||
"snapshot_ref": "task01.auto-1",
|
||
"description": "Callback widzi daemon handle.",
|
||
"evidence": "daemon priority 3."
|
||
},
|
||
{
|
||
"id": "lists",
|
||
"number": 4,
|
||
"label": "active lists hold expiry",
|
||
"mode": "RUN",
|
||
"event_id": "E03",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "04",
|
||
"code_ref": "src/tasks/task01_software_timers.c:131",
|
||
"snapshot_ref": "task01.reset",
|
||
"description": "Reset przesuwa one-shot.",
|
||
"evidence": "brak callbacku w tick 5."
|
||
},
|
||
{
|
||
"id": "callback",
|
||
"number": 5,
|
||
"label": "callback records only",
|
||
"mode": "RUN",
|
||
"event_id": "E07",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "05",
|
||
"code_ref": "src/tasks/task01_software_timers.c:68",
|
||
"snapshot_ref": "task01.one-shot",
|
||
"description": "Krótka funkcja na daemon stack.",
|
||
"evidence": "one_shot_count=1."
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "A3 — Dispatch",
|
||
"order": 30,
|
||
"content_kind": "prose",
|
||
"asset_page_mode": "one-per-page",
|
||
"page_orientation": "landscape",
|
||
"content_tex": "Callback otrzymuje nieprzezroczysty \\texttt{TimerHandle\\_t}. \\texttt{pvTimerGetTimerID} odzyskuje jawny kontekst aplikacyjny bez globalnej tablicy rozgałęzień po adresach.",
|
||
"assets": [
|
||
{
|
||
"path": "assets/a3-dispatch.png",
|
||
"html_path": "assets/a3-dispatch.svg",
|
||
"source_path": "assets/a3-dispatch.puml",
|
||
"caption": "A3 DISPATCH — jeden callback i dwa typowane IDs.",
|
||
"label": "fig:a3-dispatch",
|
||
"alt": "Mechanizm dispatch software timera.",
|
||
"kind": "diagram",
|
||
"width": 1.0,
|
||
"page_grid": {
|
||
"columns": 1,
|
||
"rows": 1,
|
||
"page_width": 940,
|
||
"page_height": 560,
|
||
"overview": true,
|
||
"step_tiles": {
|
||
"handle": 1,
|
||
"timer": 1,
|
||
"callback": 1,
|
||
"tag": 1,
|
||
"evidence": 1
|
||
}
|
||
},
|
||
"interactive": {
|
||
"kind": "uml-class",
|
||
"storage_key": "fc06-a3-dispatch",
|
||
"title": "A3 · callback dispatch",
|
||
"task": {
|
||
"id": "task01",
|
||
"label": "Task01 · software timers"
|
||
},
|
||
"block": {
|
||
"id": "a3",
|
||
"label": "A3 DISPATCH",
|
||
"description": "Handle → Timer_t → callback → TimerTag."
|
||
},
|
||
"phases": [
|
||
{
|
||
"id": "dispatch",
|
||
"label": "HANDLE / ID / CALLBACK",
|
||
"steps": [
|
||
{
|
||
"id": "handle",
|
||
"number": 1,
|
||
"label": "opaque handle identifies Timer_t",
|
||
"mode": "CODE",
|
||
"strategy_ref": "code.timer-contract",
|
||
"svg_label": "01",
|
||
"code_ref": "include/task01_software_timers.h:37",
|
||
"description": "Aplikacja nie czyta pól Timer_t.",
|
||
"evidence": "TimerHandle_t."
|
||
},
|
||
{
|
||
"id": "timer",
|
||
"number": 2,
|
||
"label": "kernel stores callback and ID",
|
||
"mode": "CODE",
|
||
"strategy_ref": "code.timer-contract",
|
||
"svg_label": "02",
|
||
"code_ref": "src/tasks/task01_software_timers.c:249",
|
||
"description": "xTimerCreate wiąże funkcję z TimerTag.",
|
||
"evidence": "one-shot i auto."
|
||
},
|
||
{
|
||
"id": "callback",
|
||
"number": 3,
|
||
"label": "daemon invokes shared callback",
|
||
"mode": "RUN",
|
||
"event_id": "E04",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "03",
|
||
"code_ref": "src/tasks/task01_software_timers.c:68",
|
||
"snapshot_ref": "task01.auto-1",
|
||
"description": "Wywołanie jest synchroniczne w daemon task.",
|
||
"evidence": "PC w callback, daemon SP."
|
||
},
|
||
{
|
||
"id": "tag",
|
||
"number": 4,
|
||
"label": "pvTimerID restores kind",
|
||
"mode": "RUN",
|
||
"event_id": "E07",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "04",
|
||
"code_ref": "src/tasks/task01_software_timers.c:70",
|
||
"snapshot_ref": "task01.one-shot",
|
||
"description": "Marker chroni kontrakt context.",
|
||
"evidence": "ONE_SHOT versus AUTO."
|
||
},
|
||
{
|
||
"id": "evidence",
|
||
"number": 5,
|
||
"label": "callback commits bounded record",
|
||
"mode": "RUN",
|
||
"event_id": "E08",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "05",
|
||
"code_ref": "src/tasks/task01_software_timers.c:89",
|
||
"snapshot_ref": "task01.auto-3",
|
||
"description": "Payload to expiry tick.",
|
||
"evidence": "E08 tick 7."
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "A5 — Flow",
|
||
"order": 40,
|
||
"content_kind": "prose",
|
||
"asset_page_mode": "one-per-page",
|
||
"page_orientation": "landscape",
|
||
"content_tex": "Komenda i callback są osobnymi momentami. Oś 2/4/6/7 wynika z okresów, resetu i change period.",
|
||
"assets": [
|
||
{
|
||
"path": "assets/a5-flow.png",
|
||
"html_path": "assets/a5-flow.svg",
|
||
"source_path": "assets/a5-flow.puml",
|
||
"caption": "A5 FLOW — one-shot reset i auto period change.",
|
||
"label": "fig:a5-flow",
|
||
"alt": "Sekwencja controller, daemon i timers.",
|
||
"kind": "diagram",
|
||
"width": 1.0,
|
||
"page_grid": {
|
||
"columns": 1,
|
||
"rows": 1,
|
||
"page_width": 940,
|
||
"page_height": 560,
|
||
"overview": true,
|
||
"step_tiles": {
|
||
"config": 1,
|
||
"start": 1,
|
||
"reset": 1,
|
||
"auto1": 1,
|
||
"auto2": 1,
|
||
"change": 1,
|
||
"one": 1,
|
||
"auto3": 1,
|
||
"stop": 1,
|
||
"pass": 1
|
||
}
|
||
},
|
||
"interactive": {
|
||
"kind": "uml-sequence",
|
||
"storage_key": "fc06-a5-flow",
|
||
"title": "A5 · timer replay",
|
||
"task": {
|
||
"id": "task01",
|
||
"label": "Task01 · software timers"
|
||
},
|
||
"block": {
|
||
"id": "a5",
|
||
"label": "A5 FLOW",
|
||
"description": "E01–E10: enqueue, process, expiry i PASS."
|
||
},
|
||
"phases": [
|
||
{
|
||
"id": "flow",
|
||
"label": "COMMANDS / EXPIRY / PASS",
|
||
"steps": [
|
||
{
|
||
"id": "config",
|
||
"number": 1,
|
||
"label": "configuration committed",
|
||
"mode": "RUN",
|
||
"event_id": "E01",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "01",
|
||
"code_ref": "src/tasks/task01_software_timers.c:247",
|
||
"snapshot_ref": "task01.config",
|
||
"description": "Timer objects jeszcze nie istnieją.",
|
||
"evidence": "event count 1."
|
||
},
|
||
{
|
||
"id": "start",
|
||
"number": 2,
|
||
"label": "two starts accepted",
|
||
"mode": "RUN",
|
||
"event_id": "E02",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "02",
|
||
"code_ref": "src/tasks/task01_software_timers.c:122",
|
||
"snapshot_ref": "task01.started",
|
||
"description": "Daemon przetwarza dwa commands.",
|
||
"evidence": "start_accepted=1."
|
||
},
|
||
{
|
||
"id": "reset",
|
||
"number": 3,
|
||
"label": "one-shot reset at tick 1",
|
||
"mode": "RUN",
|
||
"event_id": "E03",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "03",
|
||
"code_ref": "src/tasks/task01_software_timers.c:131",
|
||
"snapshot_ref": "task01.reset",
|
||
"description": "Expiry przesuwa się z 5 do 6.",
|
||
"evidence": "reset_accepted=1."
|
||
},
|
||
{
|
||
"id": "auto1",
|
||
"number": 4,
|
||
"label": "auto callback at tick 2",
|
||
"mode": "RUN",
|
||
"event_id": "E04",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "04",
|
||
"code_ref": "src/tasks/task01_software_timers.c:87",
|
||
"snapshot_ref": "task01.auto-1",
|
||
"description": "Pierwszy period=2.",
|
||
"evidence": "auto_count=1."
|
||
},
|
||
{
|
||
"id": "auto2",
|
||
"number": 5,
|
||
"label": "auto callback at tick 4",
|
||
"mode": "RUN",
|
||
"event_id": "E05",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "05",
|
||
"code_ref": "src/tasks/task01_software_timers.c:87",
|
||
"snapshot_ref": "task01.auto-2",
|
||
"description": "Drugi period=2.",
|
||
"evidence": "auto_count=2."
|
||
},
|
||
{
|
||
"id": "change",
|
||
"number": 6,
|
||
"label": "period changed to 3",
|
||
"mode": "RUN",
|
||
"event_id": "E06",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "06",
|
||
"code_ref": "src/tasks/task01_software_timers.c:147",
|
||
"snapshot_ref": "task01.period-change",
|
||
"description": "Nowy termin liczony od change command.",
|
||
"evidence": "change_accepted=1."
|
||
},
|
||
{
|
||
"id": "one",
|
||
"number": 7,
|
||
"label": "one-shot callback at tick 6",
|
||
"mode": "RUN",
|
||
"event_id": "E07",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "07",
|
||
"code_ref": "src/tasks/task01_software_timers.c:96",
|
||
"snapshot_ref": "task01.one-shot",
|
||
"description": "Fires exactly once.",
|
||
"evidence": "count=1, tick=6."
|
||
},
|
||
{
|
||
"id": "auto3",
|
||
"number": 8,
|
||
"label": "auto callback at tick 7",
|
||
"mode": "RUN",
|
||
"event_id": "E08",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "08",
|
||
"code_ref": "src/tasks/task01_software_timers.c:87",
|
||
"snapshot_ref": "task01.auto-3",
|
||
"description": "Nowy okres wynosi 3.",
|
||
"evidence": "4→7."
|
||
},
|
||
{
|
||
"id": "stop",
|
||
"number": 9,
|
||
"label": "auto stop accepted",
|
||
"mode": "RUN",
|
||
"event_id": "E09",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "09",
|
||
"code_ref": "src/tasks/task01_software_timers.c:163",
|
||
"snapshot_ref": "task01.stopped",
|
||
"description": "Controller kończy po trzech callbackach.",
|
||
"evidence": "controller_done=1."
|
||
},
|
||
{
|
||
"id": "pass",
|
||
"number": 10,
|
||
"label": "verifier commits PASS",
|
||
"mode": "RUN",
|
||
"event_id": "E10",
|
||
"strategy_ref": "run.pass",
|
||
"svg_label": "10",
|
||
"code_ref": "src/tasks/task01_software_timers.c:194",
|
||
"snapshot_ref": "task01.pass",
|
||
"description": "Timeline, handles, stacks i digest.",
|
||
"evidence": "PASS=1."
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "A6 — State",
|
||
"order": 50,
|
||
"content_kind": "prose",
|
||
"asset_page_mode": "one-per-page",
|
||
"page_orientation": "landscape",
|
||
"content_tex": "pdPASS oznacza pending command, nie natychmiastową zmianę active list. Auto-reload wraca do ACTIVE, one-shot do INACTIVE.",
|
||
"assets": [
|
||
{
|
||
"path": "assets/a6-state.png",
|
||
"html_path": "assets/a6-state.svg",
|
||
"source_path": "assets/a6-state.puml",
|
||
"caption": "A6 STATE — command pending, active, callback i reload.",
|
||
"label": "fig:a6-state",
|
||
"alt": "Maszyna stanów software timera.",
|
||
"kind": "diagram",
|
||
"width": 1.0,
|
||
"page_grid": {
|
||
"columns": 1,
|
||
"rows": 1,
|
||
"page_width": 940,
|
||
"page_height": 560,
|
||
"overview": true,
|
||
"step_tiles": {
|
||
"inactive": 1,
|
||
"active": 1,
|
||
"pending": 1,
|
||
"callback": 1,
|
||
"reload": 1,
|
||
"stopped": 1
|
||
}
|
||
},
|
||
"interactive": {
|
||
"kind": "uml-class",
|
||
"storage_key": "fc06-a6-state",
|
||
"title": "A6 · timer lifecycle",
|
||
"task": {
|
||
"id": "task01",
|
||
"label": "Task01 · software timers"
|
||
},
|
||
"block": {
|
||
"id": "a6",
|
||
"label": "A6 STATE",
|
||
"description": "Stan logiczny timera i pending command."
|
||
},
|
||
"phases": [
|
||
{
|
||
"id": "state",
|
||
"label": "INACTIVE / ACTIVE / CALLBACK",
|
||
"steps": [
|
||
{
|
||
"id": "inactive",
|
||
"number": 1,
|
||
"label": "new timer is inactive",
|
||
"mode": "CODE",
|
||
"strategy_ref": "code.timer-contract",
|
||
"svg_label": "01",
|
||
"code_ref": "src/tasks/task01_software_timers.c:249",
|
||
"description": "Create nie startuje timera.",
|
||
"evidence": "Brak expiry przed command."
|
||
},
|
||
{
|
||
"id": "active",
|
||
"number": 2,
|
||
"label": "processed start makes active",
|
||
"mode": "RUN",
|
||
"event_id": "E02",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "02",
|
||
"code_ref": "src/tasks/task01_software_timers.c:122",
|
||
"snapshot_ref": "task01.started",
|
||
"description": "Daemon dodaje timer do active list.",
|
||
"evidence": "Commands accepted."
|
||
},
|
||
{
|
||
"id": "pending",
|
||
"number": 3,
|
||
"label": "reset/change travel through queue",
|
||
"mode": "RUN",
|
||
"event_id": "E06",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "03",
|
||
"code_ref": "src/tasks/task01_software_timers.c:147",
|
||
"snapshot_ref": "task01.period-change",
|
||
"description": "Stan przejściowy jest asynchroniczny.",
|
||
"evidence": "pdPASS przed kolejnym expiry."
|
||
},
|
||
{
|
||
"id": "callback",
|
||
"number": 4,
|
||
"label": "expiry invokes callback",
|
||
"mode": "RUN",
|
||
"event_id": "E07",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "04",
|
||
"code_ref": "src/tasks/task01_software_timers.c:68",
|
||
"snapshot_ref": "task01.one-shot",
|
||
"description": "Callback działa w daemon task.",
|
||
"evidence": "tick 6."
|
||
},
|
||
{
|
||
"id": "reload",
|
||
"number": 5,
|
||
"label": "auto timer reloads",
|
||
"mode": "RUN",
|
||
"event_id": "E08",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "05",
|
||
"code_ref": "src/tasks/task01_software_timers.c:80",
|
||
"snapshot_ref": "task01.auto-3",
|
||
"description": "One-shot nie wraca do active list.",
|
||
"evidence": "auto_count=3."
|
||
},
|
||
{
|
||
"id": "stopped",
|
||
"number": 6,
|
||
"label": "stop removes active timer",
|
||
"mode": "RUN",
|
||
"event_id": "E09",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "06",
|
||
"code_ref": "src/tasks/task01_software_timers.c:163",
|
||
"snapshot_ref": "task01.stopped",
|
||
"description": "Brak czwartego callbacku.",
|
||
"evidence": "stop_accepted=1."
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "A7 — Runtime",
|
||
"order": 60,
|
||
"content_kind": "prose",
|
||
"asset_page_mode": "one-per-page",
|
||
"page_orientation": "landscape",
|
||
"content_tex": "A7 dowodzi, że callback nie jest kontynuacją controller task. Jego PC i SP należą do timer daemon, a TimerTag pozostaje application context.",
|
||
"assets": [
|
||
{
|
||
"path": "assets/a7-runtime.png",
|
||
"html_path": "assets/a7-runtime.svg",
|
||
"source_path": "assets/a7-runtime.puml",
|
||
"caption": "A7 RUNTIME — timeline, Timer_t, daemon handle i stos.",
|
||
"label": "fig:a7-runtime",
|
||
"alt": "Runtime software timer callback.",
|
||
"kind": "diagram",
|
||
"width": 1.0,
|
||
"page_grid": {
|
||
"columns": 1,
|
||
"rows": 1,
|
||
"page_width": 940,
|
||
"page_height": 560,
|
||
"overview": true,
|
||
"step_tiles": {
|
||
"exp": 1,
|
||
"controller": 1,
|
||
"daemon": 1,
|
||
"one": 1,
|
||
"auto": 1,
|
||
"events": 1
|
||
}
|
||
},
|
||
"interactive": {
|
||
"kind": "uml-class",
|
||
"storage_key": "fc06-a7-runtime",
|
||
"title": "A7 · daemon runtime",
|
||
"task": {
|
||
"id": "task01",
|
||
"label": "Task01 · software timers"
|
||
},
|
||
"block": {
|
||
"id": "a7",
|
||
"label": "A7 RUNTIME",
|
||
"description": "Controller i callback mają różne TCB oraz stosy."
|
||
},
|
||
"phases": [
|
||
{
|
||
"id": "runtime",
|
||
"label": "TCB / STACK / TIMER / TICK",
|
||
"steps": [
|
||
{
|
||
"id": "exp",
|
||
"number": 1,
|
||
"label": "experiment owns bounded results",
|
||
"mode": "RUN",
|
||
"event_id": "E10",
|
||
"strategy_ref": "run.pass",
|
||
"svg_label": "01",
|
||
"code_ref": "src/tasks/task01_software_timers.c:194",
|
||
"snapshot_ref": "task01.pass",
|
||
"description": "Wynik nie zależy od prywatnych list.",
|
||
"evidence": "timeline i counts."
|
||
},
|
||
{
|
||
"id": "controller",
|
||
"number": 2,
|
||
"label": "controller submits commands",
|
||
"mode": "RUN",
|
||
"event_id": "E06",
|
||
"strategy_ref": "run.commands",
|
||
"svg_label": "02",
|
||
"code_ref": "src/tasks/task01_software_timers.c:110",
|
||
"snapshot_ref": "task01.period-change",
|
||
"description": "Priority 2 i controller_sp.",
|
||
"evidence": "current TCB=controller."
|
||
},
|
||
{
|
||
"id": "daemon",
|
||
"number": 3,
|
||
"label": "daemon executes callbacks",
|
||
"mode": "RUN",
|
||
"event_id": "E07",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "03",
|
||
"code_ref": "src/tasks/task01_software_timers.c:77",
|
||
"snapshot_ref": "task01.one-shot",
|
||
"description": "Priority 3 i callback_sp.",
|
||
"evidence": "daemon_seen != controller."
|
||
},
|
||
{
|
||
"id": "one",
|
||
"number": 4,
|
||
"label": "one-shot expires once",
|
||
"mode": "RUN",
|
||
"event_id": "E07",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "04",
|
||
"code_ref": "src/tasks/task01_software_timers.c:94",
|
||
"snapshot_ref": "task01.one-shot",
|
||
"description": "Reset ustala tick 6.",
|
||
"evidence": "count=1."
|
||
},
|
||
{
|
||
"id": "auto",
|
||
"number": 5,
|
||
"label": "auto timer owns changing period",
|
||
"mode": "RUN",
|
||
"event_id": "E08",
|
||
"strategy_ref": "run.callback",
|
||
"svg_label": "05",
|
||
"code_ref": "src/tasks/task01_software_timers.c:80",
|
||
"snapshot_ref": "task01.auto-3",
|
||
"description": "Period change zachowuje ten sam handle.",
|
||
"evidence": "2/4/7."
|
||
},
|
||
{
|
||
"id": "events",
|
||
"number": 6,
|
||
"label": "ten records form digest",
|
||
"mode": "RUN",
|
||
"event_id": "E10",
|
||
"strategy_ref": "run.pass",
|
||
"svg_label": "06",
|
||
"code_ref": "src/tasks/task01_software_timers.c:208",
|
||
"snapshot_ref": "task01.pass",
|
||
"description": "Marker każdego rekordu jest zapisywany na końcu.",
|
||
"evidence": "stable digest."
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "Task01 — software timers",
|
||
"order": 90,
|
||
"content_kind": "tasks",
|
||
"task_refs": [
|
||
"task01"
|
||
]
|
||
}
|
||
],
|
||
"tasks": {
|
||
"task01": {
|
||
"title": "One-shot, auto-reload i timer daemon",
|
||
"uuid": "ab188300-f03d-5ddb-8316-b9534876a5bc",
|
||
"prompt_tex": "Przejdź po A1, A3, A5, A6 i A7. Odtwórz E01–E10 i rozdziel moment enqueue command od callback execution.",
|
||
"criterion": "Program kończy się PASS; one-shot=6 raz; auto=2/4/7; komendy są accepted; daemon i controller mają różne handles/SP; digest jest deterministyczny.",
|
||
"conclusion_tex": "Software timer jest obsługiwany przez daemon task. API wysyła komendę, aktywna lista przechowuje expiry, a callback działa później na stosie daemon task.",
|
||
"flow": [
|
||
{
|
||
"kind": "block",
|
||
"id": "model",
|
||
"title": "A — model timer service",
|
||
"content_tex": "Przewidź oś czasu i właściciela każdego wywołania.",
|
||
"steps": [
|
||
{
|
||
"id": "commands",
|
||
"title": "Rozdziel command i callback.",
|
||
"content_tex": "Wskaż dwa różne momenty dowodowe."
|
||
},
|
||
{
|
||
"id": "timeline",
|
||
"title": "Przewidź 2/4/6/7.",
|
||
"content_tex": "Uwzględnij reset i change period."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"kind": "block",
|
||
"id": "replay",
|
||
"title": "B — replay Hazard3",
|
||
"content_tex": "Zbadaj controller i daemon w osobnych layoutach.",
|
||
"steps": [
|
||
{
|
||
"id": "enqueue",
|
||
"title": "Zbadaj E02/E03/E06.",
|
||
"content_tex": "Sprawdź accepted flags i command source."
|
||
},
|
||
{
|
||
"id": "callbacks",
|
||
"title": "Zbadaj E04/E05/E07/E08.",
|
||
"content_tex": "Porównaj TimerTag, PC, SP i tick."
|
||
},
|
||
{
|
||
"id": "pass",
|
||
"title": "Zbadaj E09/E10.",
|
||
"content_tex": "Sprawdź brak czwartego callbacku i digest."
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"kind": "exercise",
|
||
"id": "late-command",
|
||
"title": "Ćwiczenie — command queue pressure",
|
||
"prompt_tex": "Zmniejsz \\texttt{configTIMER\\_QUEUE\\_LENGTH} do 1 i przygotuj kontrolowany burst dwóch komend z block time 0. Obsłuż \\texttt{pdFAIL} bez utraty poprzedniego stanu timera.",
|
||
"evidence_tex": "Snapshot queue pressure, jawny expected failure i deterministyczny final state.",
|
||
"criterion": "Uczeń nie interpretuje pdFAIL jako wykonanej komendy i nie blokuje callbacku.",
|
||
"based_on": [
|
||
"model",
|
||
"replay"
|
||
]
|
||
}
|
||
],
|
||
"educational_requirement_refs": [
|
||
"FC06.WE01"
|
||
],
|
||
"learning_effect_refs": [
|
||
"FC06.EN01",
|
||
"FC06.EK01"
|
||
],
|
||
"assessment_criterion_ref": "FC06.KW01"
|
||
}
|
||
},
|
||
"tasks_order": [
|
||
"task01"
|
||
]
|
||
}
|