diagrams and simulation

This commit is contained in:
deeaitch committed 2026-10-11 07:21:11 -04:00
1 parent bf9871eb9e
commit d65910a7c5
18 files changed
+926 -4

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
@startuml
title A/B storage: logical areas and access responsibilities
left to right direction
skinparam shadowing false
skinparam monochrome true
actor "Installer" as installer
actor "Boot control" as boot
actor "OTA manager /\nconfirmation program" as manager
actor "Running system\nand applications" as runtime
rectangle "Device storage (logical areas, not a partition table)" {
rectangle "Shared boot components\nSeparate update and recovery method" as shared
database "Boot state\nNormal slot, trial target, attempts, confirmation" as state
rectangle "Slot A: retained system\nKernel + device tree + root filesystem" as A
rectangle "Slot B: update target\nKernel + device tree + root filesystem" as B
database "Persistent device data" as data {
rectangle "Identity and calibration\nRestricted access" as identity
rectangle "Configuration and application data\nMust support the retained version" as appdata
rectangle "OTA progress, failure blocks\nand pending reports" as progress
}
rectangle "Download and work area\nDisposable preparation files" as work
}
installer --> B : replace inactive slot
installer --> work : use temporary files
boot --> shared : execute
boot --> state : read / save before Linux starts
boot --> A : select matching components
boot --> B : select matching components
manager --> state : request trial / confirm\nthrough platform interface
manager --> progress : read / save
runtime --> appdata : read / write
runtime --> identity : authorized access only
legend bottom
A is running while B is prepared. Preserve A, boot state and device data.
Persistent device data survives replacement of either system slot.
Shared data is not rolled back by selecting A.
Both system versions must understand the recovery records they read.
Separate logical areas do not guarantee safe writes or whole-device recovery.
endlegend
@enduml
+52
View File
@@ -0,0 +1,52 @@
@startuml
title A/B trial boot and recovery
skinparam shadowing false
skinparam monochrome true
state "A confirmed" as A
state "Prepare B\nA remains the normal boot choice" as Preparing
state "B prepared and verified" as Prepared
state "B trial pending\nSaved target and attempt limit" as Pending
state "B trial running\nUnconfirmed" as Trial
state "B confirmed" as Confirmed
state "Recovery to A pending" as Recovering
state "A recovery running" as Recovery
state "A recovery checked" as Recovered
state "Separate recovery required" as Failed
[*] --> A
A --> Preparing : begin installation
Preparing --> Preparing : reset during preparation; boot A\nB is not eligible for trial
Preparing --> A : abandon incomplete or failed preparation
Preparing --> Prepared : installation and verification complete
Prepared --> Pending : save trial request through boot control
Pending --> Trial : reset [attempts left > 0]; save attempts left - 1\nbefore handing control to B
Trial --> Confirmed : local checks pass within time limit;\nsave confirmation
Trial --> Recovering : local checks fail; save recovery request
Trial --> Pending : reset before confirmation\n(power loss or covered watchdog)
Pending --> Recovering : reset; no attempts left
Recovering --> Recovery : boot control selects A on reset
Recovery --> Recovered : local recovery checks pass;\nsave outcome
Recovery --> Failed : recovery checks fail or startup fails\n(platform recovery policy)
Confirmed --> [*]
Recovered --> [*]
Failed --> [*]
note right of Pending
Boot state is outside A and B.
Attempts are consumed before starting B.
Invalid records require a platform-defined
recovery path, never implicit confirmation.
end note
note right of Recovery
A must still be compatible with shared data.
Do not endlessly alternate failed systems.
end note
legend bottom
Preparation interruption assumes the installer preserves A and boot state.
State transitions assume valid durable records; torn writes need platform testing.
Checks and recovery are local; reports wait for connectivity.
endlegend
@enduml
+38
View File
@@ -0,0 +1,38 @@
@startuml
title OTA roles and device boundary
left to right direction
skinparam shadowing false
skinparam monochrome true
skinparam packageStyle rectangle
package "Release and deployment services" {
component "Build, test and publish" as release
component "Artifact storage" as artifacts
component "Deployment management" as deployment
}
package "Device" {
component "OTA manager" as manager
component "Installer" as installer
component "Boot control" as boot
component "Local health checks" as checks
database "Persistent update progress\nand pending reports" as progress
}
release --> artifacts : publish artifact and\nverification information
release --> deployment : register release and compatibility
manager --> deployment
manager --> installer : authorize preparation
installer --> artifacts : download and verify
manager --> boot : request trial through\nplatform interface
checks --> boot : confirm after local checks pass\nor request recovery after failure
manager --> progress : save / resume progress;\nqueue reports while offline
manager --> checks : arrange checks after activation
legend bottom
OTA manager -> deployment management: request releases; report running version and outcome.
Boot control runs before the OTA manager and handles exhausted trial attempts.
Hangs need a covered reset path. Checks and recovery do not require the server.
Roles may be combined in one tool. This view uses a system update with trial boot.
endlegend
@enduml
+17
View File
@@ -0,0 +1,17 @@
# OTA diagrams
PlantUML sources accompany chapters 03–05. The diagrams describe the illustrative A/B design, not a required platform or partition table.
| View | Source | Preview |
|---|---|---|
| Roles and the server/device boundary | [PlantUML](plantuml/ota-architecture.puml) | [SVG](rendered/ota-architecture.svg) |
| Trial boot, confirmation and recovery | [PlantUML](plantuml/ab-trial-boot.puml) | [SVG](rendered/ab-trial-boot.svg) |
| Storage areas and access responsibilities | [PlantUML](plantuml/ab-storage-layout.puml) | [SVG](rendered/ab-storage-layout.svg) |
From the repository root, regenerate previews with a local PlantUML installation and Graphviz:
```sh
JAVA_TOOL_OPTIONS=-Djava.awt.headless=true plantuml -tsvg -o ../rendered diagrams/plantuml/*.puml
```
Keep sources and previews together when changing a diagram. No external rendering service is needed. State transitions assume valid durable records; these pictures do not establish power-loss safety. The [A/B simulation](../examples/simulations/ab-trial-boot/readme.md) explores the same logical transitions.
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="1029px" preserveAspectRatio="none" style="width:766px;height:1029px;" version="1.1" viewBox="0 0 766 1029" width="766px" zoomAndPan="magnify"><defs/><g><text fill="#000000" font-family="sans-serif" font-size="18" lengthAdjust="spacingAndGlyphs" textLength="451" x="169" y="19.2419">A/B storage: logical areas and access responsibilities</text><!--MD5=[840fabc209cb087dc2fa62a6d2b2ef1b]
cluster Device storage (logical areas, not a partition table)--><rect fill="#FFFFFF" height="831" style="stroke: #000000; stroke-width: 1.5;" width="372" x="383" y="48.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="350" x="394" y="65.4818">Device storage (logical areas, not a partition table)</text><!--MD5=[dce4c2b5694dd28223e7b1d160ba96c4]
cluster data--><path d="M425,552.5158 C425,542.5158 569,542.5158 569,542.5158 C569,542.5158 713,542.5158 713,552.5158 L713,845.5158 C713,855.5158 569,855.5158 569,855.5158 C569,855.5158 425,855.5158 425,845.5158 L425,552.5158 " fill="#FFFFFF" style="stroke: #000000; stroke-width: 1.5;"/><path d="M425,552.5158 C425,562.5158 569,562.5158 569,562.5158 C569,562.5158 713,562.5158 713,552.5158 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="153" x="492.5" y="579.4818">Persistent device data</text><!--MD5=[8b2faa79b5ce75f56ca47380ca6b1f00]
entity shared--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="279" x="429.5" y="365.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="169" x="439.5" y="390.4818">Shared boot components</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="259" x="439.5" y="409.5497">Separate update and recovery method</text><!--MD5=[f350ee6f6cc17b9109e91461bf56a5ea]
entity state--><path d="M399,469.0158 C399,459.0158 569,459.0158 569,459.0158 C569,459.0158 739,459.0158 739,469.0158 L739,516.1517 C739,526.1517 569,526.1517 569,526.1517 C569,526.1517 399,526.1517 399,516.1517 L399,469.0158 " fill="#F8F8F8" style="stroke: #000000; stroke-width: 1.5;"/><path d="M399,469.0158 C399,479.0158 569,479.0158 569,479.0158 C569,479.0158 739,479.0158 739,469.0158 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="67" x="409" y="497.9818">Boot state</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="320" x="409" y="517.0497">Normal slot, trial target, attempts, confirmation</text><!--MD5=[770389ab9df153645bc6ef28055eb2b5]
entity A--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="269" x="434.5" y="272.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="154" x="444.5" y="297.4818">Slot A: retained system</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="249" x="444.5" y="316.5497">Kernel + device tree + root filesystem</text><!--MD5=[b2d39259b6aeabc16b394deeb0238426]
entity B--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="269" x="434.5" y="179.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="139" x="444.5" y="204.4818">Slot B: update target</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="249" x="444.5" y="223.5497">Kernel + device tree + root filesystem</text><!--MD5=[64a9df9efdb85e10fd4da2b3e755ab03]
entity work--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="210" x="464" y="86.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="167" x="474" y="111.4818">Download and work area</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="190" x="474" y="130.5497">Disposable preparation files</text><!--MD5=[e7744e8fbc1b270a63d01eb8a46af6a2]
entity identity--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="178" x="480" y="688.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="158" x="490" y="713.4818">Identity and calibration</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="116" x="490" y="732.5497">Restricted access</text><!--MD5=[7518041a300050aac907e20a36cd28cf]
entity appdata--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="256" x="441" y="781.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="236" x="451" y="806.4818">Configuration and application data</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="231" x="451" y="825.5497">Must support the retained version</text><!--MD5=[63a986ea2eddc5539a0918540cb05aed]
entity progress--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="210" x="464" y="595.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="190" x="474" y="620.4818">OTA progress, failure blocks</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="140" x="474" y="639.5497">and pending reports</text><!--MD5=[cfa4e52b1a5e747220fbc0f640efedd4]
entity installer--><ellipse cx="80.5" cy="131.5158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,139.5158 L80.5,166.5158 M67.5,147.5158 L93.5,147.5158 M80.5,166.5158 L67.5,181.5158 M80.5,166.5158 L93.5,181.5158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="56" x="52.5" y="198.9818">Installer</text><!--MD5=[cb76ffbfbf2ad469590be32074c51c9a]
entity boot--><ellipse cx="80.5" cy="326.5158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,334.5158 L80.5,361.5158 M67.5,342.5158 L93.5,342.5158 M80.5,361.5158 L67.5,376.5158 M80.5,361.5158 L93.5,376.5158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="81" x="40" y="393.9818">Boot control</text><!--MD5=[2b2d5ad518bf6746de4ffeb5be0566d4]
entity manager--><ellipse cx="80.5" cy="563.0158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,571.0158 L80.5,598.0158 M67.5,579.0158 L93.5,579.0158 M80.5,598.0158 L67.5,613.0158 M80.5,598.0158 L93.5,613.0158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="102" x="6" y="630.4818">OTA manager /</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="149" x="6" y="649.5497">confirmation program</text><!--MD5=[6857f13236cb144e56befd21da1781a5]
entity runtime--><ellipse cx="80.5" cy="715.0158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,723.0158 L80.5,750.0158 M67.5,731.0158 L93.5,731.0158 M80.5,750.0158 L67.5,765.0158 M80.5,750.0158 L93.5,765.0158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="109" x="24.5" y="782.4818">Running system</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="112" x="24.5" y="801.5497">and applications</text><!--MD5=[185f47122682ca8ce8f24fc040241013]
link installer to B--><path d="M108.5,170.1058 C129.25,175.6158 158.71,182.7358 185,186.5158 C265.32,198.0458 355.93,203.5858 428.84,206.2258 " fill="none" id="installer-&gt;B" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="434.06,206.4058,425.2147,202.0745,429.0634,206.2203,424.9177,210.069,434.06,206.4058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="117" x="217.5" y="182.4129">replace inactive slot</text><!--MD5=[5e4f21d9383ed71105fa8830a37cd002]
link installer to work--><path d="M108.7,159.8858 C174.46,153.5358 345.01,137.0558 458.75,126.0658 " fill="none" id="installer-&gt;work" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="463.78,125.5858,454.4426,122.4533,458.8023,126.0579,455.1978,130.4176,463.78,125.5858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="114" x="219" y="132.4129">use temporary files</text><!--MD5=[45b867fa1f8d8aa4541f3cf2eef528d9]
link boot to shared--><path d="M121.29,371.2058 C140.33,377.0958 163.59,383.3458 185,386.5158 C263.54,398.1458 352.12,400.6758 424.3,400.0558 " fill="none" id="boot-&gt;shared" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="429.47,400.0058,420.436,396.0831,424.4702,400.0487,420.5046,404.0828,429.47,400.0058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="47" x="252.5" y="382.4129">execute</text><!--MD5=[e0595da2e168bbec6fe2445f1534f63e]
link boot to state--><path d="M121.01,390.0958 C139.42,403.6858 162.25,418.3958 185,427.5158 C250.58,453.8158 326.64,469.4758 393.6,478.7958 " fill="none" id="boot-&gt;state" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="398.82,479.5158,390.4403,474.3408,393.8655,478.8428,389.3635,482.268,398.82,479.5158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="180" x="186" y="423.4129">read / save before Linux starts</text><!--MD5=[b889d15dcac99864b6524b7c7532a9ac]
link boot to A--><path d="M121.31,352.9358 C187.76,345.2858 324.3,329.5658 429.03,317.5158 " fill="none" id="boot-&gt;A" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="434.3,316.9058,424.9021,313.9597,429.3327,317.4768,425.8156,321.9074,434.3,316.9058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="173" x="189.5" y="322.4129">select matching components</text><!--MD5=[d8162b56ad1fdfbf80f85e7eafe33a3d]
link boot to B--><path d="M121.09,332.4658 C139.89,321.4258 163.01,309.0558 185,300.5158 C263.44,270.0558 354.95,247.6158 428.86,232.5358 " fill="none" id="boot-&gt;B" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="434.15,231.4558,424.5319,229.3363,429.251,232.4556,426.1316,237.1747,434.15,231.4558" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="173" x="189.5" y="244.4129">select matching components</text><!--MD5=[ddcc9cc09abe2ea6c86e27d5ca5369f5]
link manager to state--><path d="M155.12,586.7158 C224.08,570.9858 329.25,546.9858 416.18,527.1558 " fill="none" id="manager-&gt;state" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="421.09,526.0358,411.4259,524.1372,416.2152,527.1476,413.2048,531.9369,421.09,526.0358" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="129" x="211.5" y="519.4129">request trial / confirm</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="160" x="196" y="537.1189">through platform interface</text><!--MD5=[e5613818b242e6cfc0b7814e671c619e]
link manager to progress--><path d="M155.12,606.6958 C235.81,610.1758 366.08,615.7958 458.7,619.7958 " fill="none" id="manager-&gt;progress" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="463.91,620.0258,455.0895,615.6442,458.9146,619.8118,454.747,623.6368,463.91,620.0258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="65" x="243.5" y="604.4129">read / save</text><!--MD5=[b7c32426f5b9e92879708d4d6251b8ea]
link runtime to appdata--><path d="M136.68,766.5758 C152.26,769.4458 169.26,772.3358 185,774.5158 C268.02,786.0158 361.67,794.8858 435.59,800.9558 " fill="none" id="runtime-&gt;appdata" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="440.88,801.3858,432.2287,796.6789,435.896,800.986,431.5889,804.6533,440.88,801.3858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="68" x="242" y="770.4129">read / write</text><!--MD5=[1e5b522c1f502a1b8cbd35bf97fc6c8c]
link runtime to identity--><path d="M136.83,751.1958 C219.12,744.7658 374.77,732.6058 474.83,724.7958 " fill="none" id="runtime-&gt;identity" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="479.85,724.4058,470.5589,721.1384,474.866,724.8057,471.1987,729.1128,479.85,724.4058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="135" x="208.5" y="730.4129">authorized access only</text><rect fill="#DDDDDD" height="105.3396" rx="5" ry="5" style="stroke: #000000; stroke-width: 1.0;" width="534" x="127" y="913.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="487" x="133" y="933.4818">A is running while B is prepared. Preserve A, boot state and device data.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="440" x="133" y="952.5497">Persistent device data survives replacement of either system slot.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="303" x="133" y="971.6176">Shared data is not rolled back by selecting A.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="477" x="133" y="990.6855">Both system versions must understand the recovery records they read.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="522" x="133" y="1009.7534">Separate logical areas do not guarantee safe writes or whole-device recovery.</text><!--MD5=[fbf2d48276166d3e3c575df5d661f0a2]
@startuml
title A/B storage: logical areas and access responsibilities
left to right direction
skinparam shadowing false
skinparam monochrome true
actor "Installer" as installer
actor "Boot control" as boot
actor "OTA manager /\nconfirmation program" as manager
actor "Running system\nand applications" as runtime
rectangle "Device storage (logical areas, not a partition table)" {
rectangle "Shared boot components\nSeparate update and recovery method" as shared
database "Boot state\nNormal slot, trial target, attempts, confirmation" as state
rectangle "Slot A: retained system\nKernel + device tree + root filesystem" as A
rectangle "Slot B: update target\nKernel + device tree + root filesystem" as B
database "Persistent device data" as data {
rectangle "Identity and calibration\nRestricted access" as identity
rectangle "Configuration and application data\nMust support the retained version" as appdata
rectangle "OTA progress, failure blocks\nand pending reports" as progress
}
rectangle "Download and work area\nDisposable preparation files" as work
}
installer - -> B : replace inactive slot
installer - -> work : use temporary files
boot - -> shared : execute
boot - -> state : read / save before Linux starts
boot - -> A : select matching components
boot - -> B : select matching components
manager - -> state : request trial / confirm\nthrough platform interface
manager - -> progress : read / save
runtime - -> appdata : read / write
runtime - -> identity : authorized access only
legend bottom
A is running while B is prepared. Preserve A, boot state and device data.
Persistent device data survives replacement of either system slot.
Shared data is not rolled back by selecting A.
Both system versions must understand the recovery records they read.
Separate logical areas do not guarantee safe writes or whole-device recovery.
endlegend
@enduml
PlantUML version 1.2020.02(Sun Mar 01 05:22:07 EST 2020)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Java Version: 21.0.12.1+1-1-deb13u1-Debian
Operating System: Linux
Default Encoding: UTF-8
Language: en
Country: null
--></g></svg>

After

Width:  |  Height:  |  Size: 17 KiB

+81
View File
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="1165px" preserveAspectRatio="none" style="width:1029px;height:1165px;" version="1.1" viewBox="0 0 1029 1165" width="1029px" zoomAndPan="magnify"><defs/><g><text fill="#000000" font-family="sans-serif" font-size="18" lengthAdjust="spacingAndGlyphs" textLength="229" x="404" y="19.2419">A/B trial boot and recovery</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="102" x="683.79" y="169.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="683.79" x2="785.79" y1="198.5838" y2="198.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="693.79" y="189.4818">A confirmed</text><rect fill="#F8F8F8" height="58.1358" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="247" x="466.29" y="32.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="466.29" x2="713.29" y1="80.6517" y2="80.6517"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="66" x="556.79" y="52.4818">Prepare B</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="227" x="476.29" y="71.5497">A remains the normal boot choice</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="181" x="237.29" y="169.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="237.29" x2="418.29" y1="198.5838" y2="198.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="161" x="247.29" y="189.4818">B prepared and verified</text><rect fill="#F8F8F8" height="58.1358" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="226" x="214.79" y="310.0158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="214.79" x2="440.79" y1="358.1517" y2="358.1517"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="101" x="277.29" y="329.9818">B trial pending</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="206" x="224.79" y="349.0497">Saved target and attempt limit</text><rect fill="#F8F8F8" height="58.1358" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="119" x="121.29" y="476.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="121.29" x2="240.29" y1="524.6517" y2="524.6517"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="99" x="131.29" y="496.4818">B trial running</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="88" x="136.79" y="515.5497">Unconfirmed</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="102" x="7.79" y="711.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="7.79" x2="109.79" y1="740.5838" y2="740.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="17.79" y="731.4818">B confirmed</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="171" x="291.29" y="631.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="291.29" x2="462.29" y1="660.5838" y2="660.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="151" x="301.29" y="651.4818">Recovery to A pending</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="149" x="302.29" y="791.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="302.29" x2="451.29" y1="820.5838" y2="820.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="129" x="312.29" y="811.4818">A recovery running</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="149" x="368.29" y="938.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="368.29" x2="517.29" y1="967.5838" y2="967.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="129" x="378.29" y="958.4818">A recovery checked</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="204" x="111.79" y="938.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="111.79" x2="315.79" y1="967.5838" y2="967.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="184" x="121.79" y="958.4818">Separate recovery required</text><ellipse cx="1008.79"Line truncated
link *start to A--><path d="M1006.77,71.4758 C1002.61,87.7058 991.54,121.8958 968.79,139.5158 C917.66,179.1458 842.54,190.5658 791.3,193.4258 " fill="none" id="*start-&gt;A" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="786.11,193.6858,795.3036,197.2183,791.1034,193.429,794.8927,189.2289,786.11,193.6858" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[c262a729ace56793ce804c80164b78bb]
link A to Preparing--><path d="M683.6,189.4458 C650.05,183.8158 608.51,170.5558 586.79,139.5158 C578.12,127.1158 577.7,110.6458 579.93,96.1158 " fill="none" id="A-&gt;Preparing" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="580.86,90.8658,575.3541,99.0319,579.9894,95.7895,583.2319,100.4248,580.86,90.8658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="103" x="587.79" y="135.4129">begin installation</text><!--MD5=[8ac39ec8ceeada5156d86e8c940ca0fe]
link Preparing to A--><path d="M655.3,90.6258 C669.76,98.8358 684.21,108.8158 695.79,120.5158 C708.06,132.8958 717.45,149.9358 723.95,164.4358 " fill="none" id="Preparing-&gt;A" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="726.04,169.2458,726.1343,159.3974,724.0532,164.6575,718.7931,162.5764,726.04,169.2458" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="249" x="710.79" y="135.4129">abandon incomplete or failed preparation</text><!--MD5=[07f470d462742561469e04f57962dde0]
link Preparing to Preparing--><path d="M713.4,49.6658 C734.05,51.3258 748.29,55.2758 748.29,61.5158 C748.29,67.2258 736.39,71.0158 718.58,72.8858 " fill="none" id="Preparing-&gt;Preparing" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="713.4,73.3658,722.7285,76.5249,718.379,72.908,721.9959,68.5585,713.4,73.3658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="193" x="754.29" y="57.9129">reset during preparation; boot A</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="135" x="783.29" y="75.6189">B is not eligible for trial</text><!--MD5=[14c61493ce9a076f09f2189989cfcc27]
link Preparing to Prepared--><path d="M466.24,80.8158 C416.86,90.4458 367.51,103.7658 349.79,120.5158 C337.87,131.7958 332.34,149.2658 329.8,164.2958 " fill="none" id="Preparing-&gt;Prepared" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="329.05,169.2858,334.3666,160.9953,329.8071,164.3435,326.4588,159.784,329.05,169.2858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="220" x="350.79" y="135.4129">installation and verification complete</text><!--MD5=[9d3566f475c31f8d28eabcacf9587b86]
link Prepared to Pending--><path d="M327.79,219.6858 C327.79,243.0658 327.79,278.6758 327.79,304.7258 " fill="none" id="Prepared-&gt;Pending" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="327.79,309.8058,331.79,300.8058,327.79,304.8058,323.79,300.8058,327.79,309.8058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="233" x="328.79" y="264.4129">save trial request through boot control</text><!--MD5=[2b80e139dec28a8ca7d421262724a74e]
link Pending to Trial--><path d="M214.47,350.3858 C135.44,360.2358 39.83,378.3058 14.79,409.5158 C-18.81,451.3958 56.27,478.2758 116.14,492.3858 " fill="none" id="Pending-&gt;Trial" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="121.15,493.5458,113.285,487.6178,116.279,492.4174,111.4794,495.4114,121.15,493.5458" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="268" x="15.79" y="424.4129">reset [attempts left &gt; 0]; save attempts left - 1</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="165" x="67.29" y="442.1189">before handing control to B</text><!--MD5=[83187eb056d347350e70da73cc8be795]
link Trial to Pending--><path d="M240.34,482.2458 C257.83,473.3358 275.76,461.5458 288.79,446.5158 C306.79,425.7758 316.75,396.0258 322.09,373.2358 " fill="none" id="Trial-&gt;Pending" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="323.23,368.1958,317.3509,376.0975,322.1318,373.0737,325.1555,377.8547,323.23,368.1958" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="152" x="336.79" y="424.4129">reset before confirmation</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="202" x="311.79" y="442.1189">(power loss or covered watchdog)</text><!--MD5=[123c6cef540f58bde17912cb320a2a04]
link Trial to Confirmed--><path d="M121.13,518.1258 C95.2,526.4458 67.26,540.5958 51.79,564.5158 C24.03,607.4358 36.74,669.8958 48.07,706.2458 " fill="none" id="Trial-&gt;Confirmed" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="49.73,711.4158,50.7935,701.6246,48.2046,706.6542,43.1749,704.0653,49.73,711.4158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="202" x="52.79" y="579.4129">local checks pass within time limit;</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="107" x="100.29" y="597.1189">save confirmation</text><!--MD5=[788b26dbcee76df69ae24d57af56fb68]
link Trial to Recovering--><path d="M224.17,534.5358 C237.29,543.6058 251.47,554.0458 263.79,564.5158 C281.28,579.3758 282.32,586.6458 299.79,601.5158 C310.76,610.8458 323.3,620.1558 335.04,628.3558 " fill="none" id="Trial-&gt;Recovering" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="339.32,631.3258,334.2093,622.9068,335.2132,628.4739,329.6461,629.4778,339.32,631.3258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="227" x="300.79" y="588.4129">local checks fail; save recovery request</text><!--MD5=[8cd5eda113903aa9882c5f6ff29c10bf]
link Pending to Recovering--><path d="M420.65,368.0558 C467.5,383.0958 515.89,400.3758 522.79,409.5158 C574.26,477.7258 582.36,531.9058 532.79,601.5158 C517.44,623.0758 492.87,636.1058 467.72,643.9558 " fill="none" id="Pending-&gt;Recovering" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="462.69,645.4558,472.4581,646.7144,467.4811,644.0257,470.1698,639.0487,462.69,645.4558" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="132" x="567.79" y="510.9129">reset; no attempts left</text><!--MD5=[09dc23c1c901f79ee75b5b595eb0935b]
link Recovering to Recovery--><path d="M376.79,681.8558 C376.79,710.0058 376.79,756.3158 376.79,786.3658 " fill="none" id="Recovering-&gt;Recovery" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="376.79,791.4058,380.79,782.4058,376.79,786.4058,372.79,782.4058,376.79,791.4058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="181" x="377.79" y="741.9129">boot control selects A on reset</text><!--MD5=[0ce13a800b8a1f17080f923bfff08aab]
link Recovery to Recovered--><path d="M394.33,841.7658 C400.38,850.8158 406.87,861.3658 411.79,871.5158 C421.45,891.4158 429.42,915.1058 434.86,933.3258 " fill="none" id="Recovery-&gt;Recovered" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="436.32,938.3058,437.6132,928.5423,434.9069,933.5097,429.9394,930.8033,436.32,938.3058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="158" x="427.79" y="886.4129">local recovery checks pass;</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="84" x="464.79" y="904.1189">save outcome</text><!--MD5=[f5fa9e9e750dea8a99a710e758197f77]
link Recovery to Failed--><path d="M302.24,820.0058 C264.54,825.5258 221.8,839.2058 197.79,871.5158 C184.65,889.2058 190.22,914.1058 198.2,933.3958 " fill="none" id="Recovery-&gt;Failed" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="200.28,938.1658,200.3743,928.3174,198.2932,933.5775,193.0331,931.4964,200.28,938.1658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="204" x="198.79" y="886.4129">recovery checks fail or startup fails</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="153" x="224.29" y="904.1189">(platform recovery policy)</text><!--MD5=[4470f671c244ee8b6362840b5fcd1a01]
link Confirmed to *end--><path d="M55.63,761.7058 C50.69,809.4758 46.14,917.4358 93.79,988.5158 C119.37,1026.6658 173.31,1046.7958 199.25,1054.6058 " fill="none" id="Confirmed-&gt;*end" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="204.34,1056.0858,196.82,1049.7258,199.54,1054.6858,194.58,1057.4058,204.34,1056.0858" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[95cc10cd26e44889f83ba83c5e220ef1]
link Recovered to *end--><path d="M384.12,988.6058 C331.5,1010.1958 258.07,1040.3458 227.74,1052.7858 " fill="none" id="Recovered-&gt;*end" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="222.92,1054.7658,232.764,1055.0745,227.5505,1052.8794,229.7456,1047.6658,222.92,1054.7658" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[3c91aadb35076bbec70261ad1b979304]
link Failed to *end--><path d="M213.79,988.7258 C213.79,1006.3558 213.79,1029.6458 213.79,1044.3558 " fill="none" id="Failed-&gt;*end" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="213.79,1049.3558,217.79,1040.3558,213.79,1044.3558,209.79,1040.3558,213.79,1049.3558" style="stroke: #383838; stroke-width: 1.0;"/><rect fill="#DDDDDD" height="67.2038" rx="5" ry="5" style="stroke: #000000; stroke-width: 1.0;" width="562" x="237" y="1087.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="501" x="243" y="1107.4818">Preparation interruption assumes the installer preserves A and boot state.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="550" x="243" y="1126.5497">State transitions assume valid durable records; torn writes need platform testing.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="399" x="243" y="1145.6176">Checks and recovery are local; reports wait for connectivity.</text><!--MD5=[d4e2cf11ff88427b3c50e83f198cfc1a]
@startuml
title A/B trial boot and recovery
skinparam shadowing false
skinparam monochrome true
state "A confirmed" as A
state "Prepare B\nA remains the normal boot choice" as Preparing
state "B prepared and verified" as Prepared
state "B trial pending\nSaved target and attempt limit" as Pending
state "B trial running\nUnconfirmed" as Trial
state "B confirmed" as Confirmed
state "Recovery to A pending" as Recovering
state "A recovery running" as Recovery
state "A recovery checked" as Recovered
state "Separate recovery required" as Failed
[*] - -> A
A - -> Preparing : begin installation
Preparing - -> Preparing : reset during preparation; boot A\nB is not eligible for trial
Preparing - -> A : abandon incomplete or failed preparation
Preparing - -> Prepared : installation and verification complete
Prepared - -> Pending : save trial request through boot control
Pending - -> Trial : reset [attempts left > 0]; save attempts left - 1\nbefore handing control to B
Trial - -> Confirmed : local checks pass within time limit;\nsave confirmation
Trial - -> Recovering : local checks fail; save recovery request
Trial - -> Pending : reset before confirmation\n(power loss or covered watchdog)
Pending - -> Recovering : reset; no attempts left
Recovering - -> Recovery : boot control selects A on reset
Recovery - -> Recovered : local recovery checks pass;\nsave outcome
Recovery - -> Failed : recovery checks fail or startup fails\n(platform recovery policy)
Confirmed - -> [*]
Recovered - -> [*]
Failed - -> [*]
note right of Pending
Boot state is outside A and B.
Attempts are consumed before starting B.
Invalid records require a platform-defined
recovery path, never implicit confirmation.
end note
note right of Recovery
A must still be compatible with shared data.
Do not endlessly alternate failed systems.
end note
legend bottom
Preparation interruption assumes the installer preserves A and boot state.
State transitions assume valid durable records; torn writes need platform testing.
Checks and recovery are local; reports wait for connectivity.
endlegend
@enduml
PlantUML version 1.2020.02(Sun Mar 01 05:22:07 EST 2020)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Java Version: 21.0.12.1+1-1-deb13u1-Debian
Operating System: Linux
Default Encoding: UTF-8
Language: en
Country: null
--></g></svg>

After

Width:  |  Height:  |  Size: 21 KiB

+69
View File
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="765px" preserveAspectRatio="none" style="width:1092px;height:765px;" version="1.1" viewBox="0 0 1092 765" width="1092px" zoomAndPan="magnify"><defs/><g><text fill="#000000" font-family="sans-serif" font-size="18" lengthAdjust="spacingAndGlyphs" textLength="265" x="419.5" y="19.2419">OTA roles and device boundary</text><!--MD5=[a9e6cbe5f305c80ab86c0c23625bafb4]
cluster Release and deployment services--><polygon fill="#FFFFFF" points="395,48.5158,630,48.5158,637,73.5838,1070,73.5838,1070,233.5158,395,233.5158,395,48.5158" style="stroke: #000000; stroke-width: 1.5;"/><line style="stroke: #000000; stroke-width: 1.5;" x1="395" x2="637" y1="73.5838" y2="73.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="229" x="399" y="65.4818">Release and deployment services</text><!--MD5=[eba421ac8e20ac42f3ee759fe6c4cf96]
cluster Device--><polygon fill="#FFFFFF" points="22,257.5158,73,257.5158,80,282.5838,1023,282.5838,1023,634.5158,22,634.5158,22,257.5158" style="stroke: #000000; stroke-width: 1.5;"/><line style="stroke: #000000; stroke-width: 1.5;" x1="22" x2="80" y1="282.5838" y2="282.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="45" x="26" y="274.4818">Device</text><!--MD5=[57fb6a492e484ef8c93e363da48f07ef]
entity release--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="173" x="411" y="140.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="406" y="145.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="406" y="169.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="153" x="421" y="164.9818">Build, test and publish</text><!--MD5=[9b50232802efdec09056d2fe94d4716d]
entity artifacts--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="124" x="894.5" y="170.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="889.5" y="175.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="889.5" y="199.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="104" x="904.5" y="194.9818">Artifact storage</text><!--MD5=[389badd7c35f4ea505f5befbc26a5052]
entity deployment--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="195" x="859" y="87.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="854" y="92.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="854" y="116.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="175" x="869" y="111.9818">Deployment management</text><!--MD5=[2b2d5ad518bf6746de4ffeb5be0566d4]
entity manager--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="113" x="38" y="398.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="33" y="403.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="33" y="427.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="93" x="48" y="422.9818">OTA manager</text><!--MD5=[cfa4e52b1a5e747220fbc0f640efedd4]
entity installer--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="76" x="459.5" y="398.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="454.5" y="403.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="454.5" y="427.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="56" x="469.5" y="422.9818">Installer</text><!--MD5=[cb76ffbfbf2ad469590be32074c51c9a]
entity boot--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="101" x="906" y="520.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="901" y="525.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="901" y="549.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="81" x="916" y="544.9818">Boot control</text><!--MD5=[e132700e034a4f2f51ed9e2a9fef93f1]
entity checks--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="150" x="422.5" y="472.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="417.5" y="477.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="417.5" y="501.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="130" x="432.5" y="496.9818">Local health checks</text><!--MD5=[63a986ea2eddc5539a0918540cb05aed]
entity progress--><path d="M396,306.0158 C396,296.0158 497.5,296.0158 497.5,296.0158 C497.5,296.0158 599,296.0158 599,306.0158 L599,353.1517 C599,363.1517 497.5,363.1517 497.5,363.1517 C497.5,363.1517 396,363.1517 396,353.1517 L396,306.0158 " fill="#F8F8F8" style="stroke: #000000; stroke-width: 1.5;"/><path d="M396,306.0158 C396,316.0158 497.5,316.0158 497.5,316.0158 C497.5,316.0158 599,316.0158 599,306.0158 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="183" x="406" y="334.9818">Persistent update progress</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="140" x="406" y="354.0497">and pending reports</text><!--MD5=[54cda06c1d8843fb6ff1b57076ef2079]
link release to artifacts--><path d="M584.15,178.9158 C599.03,181.6158 614.41,183.9858 629,185.5158 C718.7,194.9158 823.02,194.1758 889.14,192.2758 " fill="none" id="release-&gt;artifacts" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="894.23,192.1258,885.1194,188.3845,889.232,192.2686,885.3479,196.3813,894.23,192.1258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="115" x="671.5" y="163.4129">publish artifact and</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="140" x="659" y="181.1189">verification information</text><!--MD5=[954388852704645441443e24d4a7d174]
link release to deployment--><path d="M584.19,149.5758 C660.57,140.7158 772.47,127.7458 853.45,118.3558 " fill="none" id="release-&gt;deployment" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="858.74,117.7358,849.3421,114.7897,853.7727,118.3068,850.2556,122.7374,858.74,117.7358" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="198" x="630" y="118.4129">register release and compatibility</text><!--MD5=[43a73133501dd7da1265b9492ab9c514]
link manager to deployment--><path d="M107.23,397.9558 C141.78,342.4958 249.36,184.2158 387,122.5158 C538.26,54.7058 735.61,68.9558 853.56,86.7958 " fill="none" id="manager-&gt;deployment" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="858.77,87.5958,850.4794,82.2792,853.8276,86.8388,849.2681,90.187,858.77,87.5958" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[7fc8070790f1222e2683050d3de11d7a]
link manager to installer--><path d="M151.26,417.5158 C232.48,417.5158 381.57,417.5158 454.33,417.5158 " fill="none" id="manager-&gt;installer" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="459.43,417.5158,450.43,413.5158,454.43,417.5158,450.43,421.5158,459.43,417.5158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="131" x="208" y="413.4129">authorize preparation</text><!--MD5=[88d9015c92c57aa4fad28cffb4a9fd80]
link installer to artifacts--><path d="M535.75,405.1958 C554.9,398.5058 578.48,389.7358 599,380.5158 C718.78,326.6958 853.8,249.5858 917.88,211.9258 " fill="none" id="installer-&gt;artifacts" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="922.37,209.2858,912.5871,210.424,918.0662,211.8309,916.6593,217.31,922.37,209.2858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="122" x="668" y="263.4129">download and verify</text><!--MD5=[041cd83e0169fe8848d028533b469304]
link manager to boot--><path d="M116.74,437.0258 C133.32,451.5158 157.43,471.0258 181,484.5158 C266.39,533.3658 290.34,547.1958 387,565.5158 C572.91,600.7558 798.83,568.7158 900.85,550.3358 " fill="none" id="manager-&gt;boot" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="905.91,549.4258,896.3432,547.0856,900.9893,550.3127,897.7622,554.9588,905.91,549.4258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="125" x="435" y="543.4129">request trial through</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="107" x="444" y="561.1189">platform interface</text><!--MD5=[c0dd462a887d2095200a8326f34ac787]
link checks to boot--><path d="M572.69,499.3058 C664.45,508.9458 818.54,525.1258 900.44,533.7358 " fill="none" id="checks-&gt;boot" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="905.73,534.2858,897.2033,529.3567,900.758,533.7572,896.3575,537.3119,905.73,534.2858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="180" x="639" y="483.4129">confirm after local checks pass</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="188" x="635" y="501.1189">or request recovery after failure</text><!--MD5=[e5613818b242e6cfc0b7814e671c619e]
link manager to progress--><path d="M151.35,398.8658 C161.17,395.8758 171.34,392.9658 181,390.5158 C250.17,372.9658 328.97,357.7258 390.52,346.8758 " fill="none" id="manager-&gt;progress" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="395.67,345.9658,386.1177,343.5671,390.7439,346.8226,387.4885,351.4488,395.67,345.9658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="142" x="202.5" y="330.4129">save / resume progress;</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="159" x="194" y="348.1189">queue reports while offline</text><!--MD5=[a165eaa03005a3447a7d5b3956d8aa72]
link manager to checks--><path d="M151.26,436.5158 C161.1,439.4658 171.29,442.2758 181,444.5158 C260.38,462.8458 352.72,475.6258 416.91,483.1858 " fill="none" id="manager-&gt;checks" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="422.25,483.8158,413.7815,478.7874,417.2845,483.2291,412.8428,486.7322,422.25,483.8158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="183" x="182" y="440.4129">arrange checks after activation</text><rect fill="#DDDDDD" height="86.2717" rx="5" ry="5" style="stroke: #000000; stroke-width: 1.0;" width="678" x="212.5" y="668.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="666" x="218.5" y="688.4818">OTA manager -&gt; deployment management: request releases; report running version and outcome.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="548" x="218.5" y="707.5497">Boot control runs before the OTA manager and handles exhausted trial attempts.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="546" x="218.5" y="726.6176">Hangs need a covered reset path. Checks and recovery do not require the server.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="557" x="218.5" y="745.6855">Roles may be combined in one tool. This view uses a system update with trial boot.</text><!--MD5=[eb0d27d0bff260dba490671681420f7d]
@startuml
title OTA roles and device boundary
left to right direction
skinparam shadowing false
skinparam monochrome true
skinparam packageStyle rectangle
package "Release and deployment services" {
component "Build, test and publish" as release
component "Artifact storage" as artifacts
component "Deployment management" as deployment
}
package "Device" {
component "OTA manager" as manager
component "Installer" as installer
component "Boot control" as boot
component "Local health checks" as checks
database "Persistent update progress\nand pending reports" as progress
}
release - -> artifacts : publish artifact and\nverification information
release - -> deployment : register release and compatibility
manager - -> deployment
manager - -> installer : authorize preparation
installer - -> artifacts : download and verify
manager - -> boot : request trial through\nplatform interface
checks - -> boot : confirm after local checks pass\nor request recovery after failure
manager - -> progress : save / resume progress;\nqueue reports while offline
manager - -> checks : arrange checks after activation
legend bottom
OTA manager -> deployment management: request releases; report running version and outcome.
Boot control runs before the OTA manager and handles exhausted trial attempts.
Hangs need a covered reset path. Checks and recovery do not require the server.
Roles may be combined in one tool. This view uses a system update with trial boot.
endlegend
@enduml
PlantUML version 1.2020.02(Sun Mar 01 05:22:07 EST 2020)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Java Version: 21.0.12.1+1-1-deb13u1-Debian
Operating System: Linux
Default Encoding: UTF-8
Language: en
Country: null
--></g></svg>

After

Width:  |  Height:  |  Size: 15 KiB

+6 -4
View File
@@ -71,7 +71,7 @@ The emphasis is always on understanding the underlying design rather than memori
## Repository Structure ## Repository Structure
The structure below shows the planned repository. The `diagrams/` and `examples/` directories will be added as the practical material develops. The structure below includes the current documentation, PlantUML diagrams, and A/B simulation. Hardware demonstrations and later chapters will be added as the practical material develops.
```text ```text
ota-reference-design/ ota-reference-design/
@@ -86,12 +86,12 @@ ota-reference-design/
│ ├── ... │ ├── ...
│ │
├── diagrams/ ├── diagrams/
│ └── plantuml/ │ ├── plantuml/
│ └── rendered/
│ │
├── examples/ ├── examples/
│ ├── raspberry-pi/
│ ├── qemu/
│ └── simulations/ │ └── simulations/
│ └── ab-trial-boot/
│ │
├── adr/ ├── adr/
│ ├── ADR-001-repository-name.md │ ├── ADR-001-repository-name.md
@@ -196,6 +196,8 @@ Where possible, theoretical discussions are accompanied by practical material, i
The objective is to connect high-level architecture with practical implementation. The objective is to connect high-level architecture with practical implementation.
Available now: [PlantUML diagrams and SVG previews](../diagrams/readme.md) and a [Python A/B trial boot simulation](../examples/simulations/ab-trial-boot/readme.md). Raspberry Pi and QEMU demonstrations are planned.
--- ---
## Intended Audience ## Intended Audience
+6
View File
@@ -45,6 +45,12 @@ Alongside the update files, it needs a few details: which version this is, which
These are roles, not a shopping list of separate services. One tool may cover several of them. What matters is knowing who handles each step. These are roles, not a shopping list of separate services. One tool may cover several of them. What matters is knowing who handles each step.
The following view shows the roles and the server/device boundary for the system-update example used in the next chapter.
![OTA roles and device boundary](../diagrams/rendered/ota-architecture.svg)
[PlantUML source](../diagrams/plantuml/ota-architecture.puml)
## 3. One Update, Start to Finish ## 3. One Update, Start to Finish
Imagine a device running version 1. Version 2 is ready. In this example, the device can prepare the new system while keeping the previous one available for recovery. Imagine a device running version 1. Version 2 is ready. In this example, the device can prepare the new system while keeping the previous one available for recovery.
@@ -99,6 +99,12 @@ Returning to a previous version is called **rollback**. Keeping A available make
[ADR-014: Trial Boot and Confirmation for System Updates](../adr/ADR-014-trial-boot-and-confirmation.md) explains when to choose this policy and what the platform must support. [ADR-014: Trial Boot and Confirmation for System Updates](../adr/ADR-014-trial-boot-and-confirmation.md) explains when to choose this policy and what the platform must support.
The state diagram separates an explicit health-check failure from a startup failure that prevents the checks from running. It also shows the checks after returning to A.
![A/B trial boot and recovery](../diagrams/rendered/ab-trial-boot.svg)
[PlantUML source](../diagrams/plantuml/ab-trial-boot.puml) · [Runnable simulation and failure scenarios](../examples/simulations/ab-trial-boot/readme.md)
## 5. Example: Starting an Updated Service ## 5. Example: Starting an Updated Service
Now imagine a device with a stable system platform and an independently updated communication service. Version 1 works. Version 2 arrives as a package. Now imagine a device with a stable system platform and an independently updated communication service. Version 1 works. Version 2 arrives as a package.
+6
View File
@@ -64,6 +64,12 @@ The layout still has shared parts. If an update damages the shared boot componen
Two slots on one storage device also do not protect against failure of that whole device. Two slots on one storage device also do not protect against failure of that whole device.
The following diagram adds access responsibilities to the logical layout. A is running while the installer prepares B. Trial requests and confirmation use the platform's boot control interface.
![A/B storage areas and access responsibilities](../diagrams/rendered/ab-storage-layout.svg)
[PlantUML source](../diagrams/plantuml/ab-storage-layout.puml) · [Simulation of saved boot state across resets](../examples/simulations/ab-trial-boot/readme.md)
## 3. What Lives Outside the System Slots? ## 3. What Lives Outside the System Slots?
A system release provides common software. Device identity and calibration belong to an individual device. Installing a system image must not replace them with values from the release build. A system release provides common software. Device identity and calibration belong to an individual device. Installing a system image must not replace them with values from the release build.
@@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.20)
project(ab_trial_boot LANGUAGES CXX)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(ab-simulation simulation.cpp)
target_compile_features(ab-simulation PUBLIC cxx_std_17)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(ab-simulation PRIVATE -Wall -Wextra -Wpedantic)
endif()
add_executable(ab-simulate main.cpp)
target_link_libraries(ab-simulate PRIVATE ab-simulation)
include(CTest)
if(BUILD_TESTING)
add_executable(ab-simulation-tests tests.cpp)
target_link_libraries(ab-simulation-tests PRIVATE ab-simulation)
add_test(NAME recovery-invariants COMMAND ab-simulation-tests)
add_test(NAME all-scenarios COMMAND ab-simulate all)
add_test(NAME cli-help COMMAND ab-simulate --help)
add_test(NAME cli-invalid-scenario COMMAND ab-simulate invalid)
set_tests_properties(cli-invalid-scenario PROPERTIES WILL_FAIL TRUE)
endif()
@@ -0,0 +1,30 @@
#include "simulation.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
int main(int argc, char* argv[]) {
const std::string selected = argc > 1 ? argv[1] : "all";
if (argc > 2 || selected == "--help" || selected == "-h" ||
(selected != "all" && std::find(ota::scenarios().begin(), ota::scenarios().end(),
selected) == ota::scenarios().end())) {
std::ostream& output = (argc == 2 && (selected == "--help" || selected == "-h"))
? std::cout : std::cerr;
output << "Usage: ab-simulate [all";
for (const auto& scenario : ota::scenarios()) output << '|' << scenario;
output << "]\n";
return &output == &std::cout ? 0 : 2;
}
try {
for (const auto& scenario : ota::scenarios()) {
if (selected != "all" && selected != scenario) continue;
std::cout << "\n=== " << scenario << " ===\n";
ota::TemporaryDirectory directory;
ota::run_scenario(scenario, directory.path(), &std::cout);
}
} catch (const std::exception& error) {
std::cerr << "Simulation failed: " << error.what() << '\n';
return 1;
}
}
@@ -0,0 +1,43 @@
# A/B trial boot simulation
This C++17 standard-library example follows the [trial boot diagram](../../../diagrams/plantuml/ab-trial-boot.puml), [chapter 04](../../../docs/04-starting-an-updated-system-or-service.md), and [ADR-014](../../../adr/ADR-014-trial-boot-and-confirmation.md). It illustrates boot decisions; it does not install software or access a device. No third-party libraries are required.
Build and run from the repository root with CMake 3.20 or later and a C++17 compiler. These commands use a Linux temporary build directory to keep generated files outside the repository:
```sh
cmake -S examples/simulations/ab-trial-boot -B /tmp/ota-ab-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/ota-ab-build
/tmp/ota-ab-build/ab-simulate all
/tmp/ota-ab-build/ab-simulate attempts-exhausted
ctest --test-dir /tmp/ota-ab-build --output-on-failure
```
| Scenario | Expected result |
|---|---|
| `success` | Checks pass; B is confirmed and selected on the next boot. |
| `health-failure` | B fails checks; reset selects A; recovery checks pass. |
| `attempts-exhausted` | B never reaches checks; two unconfirmed attempts lead to A. |
| `restart-during-trial` | A reset preserves the consumed attempt; the second trial passes. |
| `interrupted-preparation` | Incomplete B remains ineligible; reset selects A. |
| `recovery-failure` | A also fails; stop at the boundary of a separate recovery policy. |
With no argument, `ab-simulate` runs all scenarios. Use `--help` to list the accepted arguments. Invalid arguments return a nonzero exit status.
The attempt limit of two is an illustrative choice. Each reset reconstructs the device from a versioned text record (`boot-state.txt`) in a temporary directory. The running slot is volatile. The saved normal slot stays A until B is confirmed. Boot control saves each consumed attempt before returning B as the selected system. A failed health check requests recovery immediately rather than spending the remaining trial attempts.
The example keeps boot state outside the simulated slots, as shown in the [storage diagram](../../../diagrams/plantuml/ab-storage-layout.puml). Each scenario uses a separate temporary directory, removed when it finishes. The model covers one A-to-B update; it does not model subsequent releases.
## Assumptions and limits
Installation and artifact verification are represented by one preparation operation. Health checks are supplied boolean outcomes, including a timeout represented as failure. Resets are explicit calls; watchdog coverage and real startup timing are not simulated. Recovery assumes that A and shared data remain usable, unless the recovery-failure scenario supplies a failed check.
The text file is a teaching stand-in for boot state, using standard C++ streams and `std::filesystem`. It records the phase, normal slot, optional trial slot, attempts remaining, slot readiness, and outcome. Replacement uses a temporary file and rename, but does not flush storage, implement redundant records, or recover invalid records. Unreadable records stop the simulation with an error; they are never treated as confirmation. This is not a power-loss-safe boot-state implementation. The simulation assumes completed valid writes; it does not test interruption within a write, coordination with OTA progress, data migration, signatures, bootloader integration, or hardware failure. These need platform-specific implementation and failure-injection tests.
Outcomes remain local in the model. There is no server dependency or reporting transport. A real implementation must retain pending reports separately and send them when connectivity returns.
## Source and checks
- [simulation.hpp](simulation.hpp) and [simulation.cpp](simulation.cpp): typed state model, saved records, reset behavior, and scenarios.
- [main.cpp](main.cpp): command-line runner.
- [tests.cpp](tests.cpp): scenario outcomes and invariants for saved attempts, incomplete slots, confirmation, and unreadable state. Checks remain enabled in Release builds.
- [CMakeLists.txt](CMakeLists.txt): builds the runner and registers the invariant and CLI checks with CTest.
@@ -0,0 +1,233 @@
#include "simulation.hpp"
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <random>
#include <stdexcept>
#include <utility>
namespace ota {
namespace {
constexpr const char* phase_names[] = {
"A confirmed", "Prepare B", "B prepared and verified", "B trial pending",
"B trial running", "B confirmed", "Recovery to A pending",
"A recovery running", "A recovery checked", "Separate recovery required"
};
Phase read_phase(const std::string& value) {
for (int i = 0; i < 10; ++i) {
if (value == phase_names[i]) return static_cast<Phase>(i);
}
throw std::runtime_error("Invalid saved phase");
}
Slot read_slot(const std::string& value) {
if (value == "A") return Slot::A;
if (value == "B") return Slot::B;
throw std::runtime_error("Invalid saved slot");
}
void require(bool condition, const char* message) {
if (!condition) throw std::logic_error(message);
}
} // namespace
const char* name(Phase phase) { return phase_names[static_cast<int>(phase)]; }
const char* name(Slot slot) { return slot == Slot::A ? "A" : "B"; }
Device::Device(const std::filesystem::path& directory)
: path_(directory / "boot-state.txt") {
if (!std::filesystem::exists(path_)) {
save();
return;
}
std::ifstream input(path_);
std::string header, phase, normal, trial;
int a_ready = 0, b_ready = 0;
if (!(input >> header >> std::quoted(phase) >> normal >> trial
>> state_.attempts_left >> a_ready >> b_ready
>> std::quoted(state_.outcome)) || header != "OTA_AB_STATE_V1") {
throw std::runtime_error("Unreadable saved boot state");
}
input >> std::ws;
if (!input.eof() || state_.attempts_left < 0 ||
(a_ready != 0 && a_ready != 1) || (b_ready != 0 && b_ready != 1)) {
throw std::runtime_error("Invalid saved boot state");
}
state_.phase = read_phase(phase);
state_.normal = read_slot(normal);
if (trial != "none") state_.trial = read_slot(trial);
state_.a_ready = a_ready != 0;
state_.b_ready = b_ready != 0;
}
void Device::save() const {
auto temporary = path_;
temporary += ".tmp";
std::ofstream output(temporary, std::ios::trunc);
output << "OTA_AB_STATE_V1\n" << std::quoted(name(state_.phase)) << '\n'
<< name(state_.normal) << '\n'
<< (state_.trial ? name(*state_.trial) : "none") << '\n'
<< state_.attempts_left << '\n'
<< state_.a_ready << ' ' << state_.b_ready << '\n'
<< std::quoted(state_.outcome) << '\n';
output.close();
if (!output) throw std::runtime_error("Cannot save boot state");
// Teaching stand-in: rename alone does not guarantee power-loss durability.
std::filesystem::rename(temporary, path_);
}
void Device::prepare(bool complete) {
require(state_.phase == Phase::AConfirmed || state_.phase == Phase::PreparingB,
"Cannot prepare B in this phase");
state_.b_ready = false;
state_.phase = Phase::PreparingB;
save();
if (complete) {
// Represents complete installation AND artifact verification.
state_.b_ready = true;
state_.phase = Phase::BPrepared;
save();
}
}
void Device::request_trial(int attempts) {
require(state_.phase == Phase::BPrepared && state_.b_ready,
"B must be prepared and verified before trial");
require(attempts > 0, "The trial needs at least one attempt");
state_.phase = Phase::TrialPending;
state_.trial = Slot::B;
state_.attempts_left = attempts;
save();
}
Slot Device::boot() {
require(state_.phase != Phase::SeparateRecoveryRequired,
"Separate recovery is required; do not alternate failed systems");
if (state_.trial) {
if (state_.attempts_left > 0) {
--state_.attempts_left;
state_.phase = Phase::TrialRunning;
// Persist the consumed attempt BEFORE handing control to B.
save();
running_ = *state_.trial;
return *running_;
}
state_.phase = Phase::RecoveryPending;
state_.trial.reset();
save();
}
if (state_.phase == Phase::RecoveryPending) {
state_.phase = Phase::RecoveryRunning;
save();
}
running_ = state_.normal;
return *running_;
}
void Device::check(bool healthy) {
require(running_.has_value(), "Boot before checking the running system");
require(state_.phase == Phase::TrialRunning || state_.phase == Phase::RecoveryRunning,
"Checks require a running trial or recovery");
if (state_.phase == Phase::TrialRunning) {
state_.trial.reset();
state_.attempts_left = 0;
if (healthy) {
state_.phase = Phase::BConfirmed;
state_.normal = Slot::B;
state_.outcome = "B confirmed";
} else {
state_.phase = Phase::RecoveryPending;
}
} else if (healthy) {
state_.phase = Phase::RecoveryChecked;
state_.outcome = "Recovered to A";
} else {
state_.phase = Phase::SeparateRecoveryRequired;
state_.outcome = "Recovery A failed";
}
save();
}
TemporaryDirectory::TemporaryDirectory() {
std::random_device random;
for (int attempt = 0; attempt < 100; ++attempt) {
auto candidate = std::filesystem::temp_directory_path() /
("ota-ab-simulation-" + std::to_string(random()) + "-" + std::to_string(random()));
if (std::filesystem::create_directory(candidate)) {
path_ = std::move(candidate);
return;
}
}
throw std::runtime_error("Cannot create a unique temporary directory");
}
TemporaryDirectory::~TemporaryDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::vector<std::string>& scenarios() {
static const std::vector<std::string> names = {
"success", "health-failure", "attempts-exhausted", "restart-during-trial",
"interrupted-preparation", "recovery-failure"
};
return names;
}
Device run_scenario(const std::string& scenario, const std::filesystem::path& directory,
std::ostream* output) {
if (std::find(scenarios().begin(), scenarios().end(), scenario) == scenarios().end()) {
throw std::invalid_argument("Unknown scenario: " + scenario);
}
Device device(directory);
auto show = [&](const std::string& event) {
if (!output) return;
const auto& state = device.state();
*output << event << ": " << name(state.phase) << "; normal=" << name(state.normal)
<< "; attempts_left=" << state.attempts_left << "; running="
<< (device.running() ? name(*device.running()) : "none") << '\n';
};
auto reset = [&] {
device = Device(directory);
const auto selected = device.boot();
show(std::string("Reset -> ") + name(selected));
};
auto message = [&](const char* text) { if (output) *output << text << '\n'; };
show("Initial state");
if (scenario == "interrupted-preparation") {
device.prepare(false);
show("Interrupted installation: B remains ineligible");
reset();
return device;
}
device.prepare();
show("Installation and verification complete");
device.request_trial(2);
show("Trial requested");
reset();
if (scenario == "attempts-exhausted" || scenario == "restart-during-trial") {
message("No confirmation: simulate a reset caused by power loss or watchdog");
reset();
}
if (scenario == "attempts-exhausted") {
message("B still cannot reach health checks; simulate another reset");
reset();
device.check(true);
show("Recovery checks pass");
} else if (scenario == "health-failure" || scenario == "recovery-failure") {
device.check(false);
show("B health checks fail");
reset();
device.check(scenario != "recovery-failure");
show("Recovery checks complete");
} else {
device.check(true);
show("B health checks pass; confirmation saved");
reset();
}
return device;
}
} // namespace ota
@@ -0,0 +1,68 @@
#pragma once
#include <filesystem>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
namespace ota {
enum class Phase {
AConfirmed, PreparingB, BPrepared, TrialPending, TrialRunning,
BConfirmed, RecoveryPending, RecoveryRunning, RecoveryChecked,
SeparateRecoveryRequired
};
enum class Slot { A, B };
const char* name(Phase phase);
const char* name(Slot slot);
struct State {
Phase phase = Phase::AConfirmed;
Slot normal = Slot::A;
std::optional<Slot> trial;
int attempts_left = 0;
bool a_ready = true;
bool b_ready = false;
std::string outcome;
};
class Device {
public:
explicit Device(const std::filesystem::path& directory);
const State& state() const { return state_; }
std::optional<Slot> running() const { return running_; }
void prepare(bool complete = true);
void request_trial(int attempts = 2);
Slot boot();
void check(bool healthy);
private:
void save() const;
State state_;
std::filesystem::path path_;
// Volatile: reconstructing a Device requires another boot before checks.
std::optional<Slot> running_;
};
// Own only a freshly created directory and clean it up at scope exit.
class TemporaryDirectory {
public:
TemporaryDirectory();
~TemporaryDirectory();
TemporaryDirectory(const TemporaryDirectory&) = delete;
TemporaryDirectory& operator=(const TemporaryDirectory&) = delete;
const std::filesystem::path& path() const { return path_; }
private:
std::filesystem::path path_;
};
const std::vector<std::string>& scenarios();
Device run_scenario(const std::string& scenario,
const std::filesystem::path& directory,
std::ostream* output = nullptr);
} // namespace ota
@@ -0,0 +1,124 @@
#include "simulation.hpp"
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
void expect(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template <class Operation>
void expect_rejected(Operation operation) {
try {
operation();
} catch (const std::logic_error&) {
return;
}
throw std::runtime_error("Invalid operation was accepted");
}
void scenario_outcomes() {
struct Expected {
const char* scenario;
ota::Phase phase;
ota::Slot normal;
const char* outcome;
};
const Expected expected[] = {
{"success", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
{"health-failure", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
{"attempts-exhausted", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
{"restart-during-trial", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
{"interrupted-preparation", ota::Phase::PreparingB, ota::Slot::A, ""},
{"recovery-failure", ota::Phase::SeparateRecoveryRequired, ota::Slot::A,
"Recovery A failed"}
};
for (const auto& result : expected) {
ota::TemporaryDirectory directory;
auto device = ota::run_scenario(result.scenario, directory.path());
const ota::Device restored(directory.path());
const auto& state = restored.state();
expect(state.phase == result.phase, "Unexpected saved phase");
expect(state.normal == result.normal, "Unexpected normal slot");
expect(state.outcome == result.outcome, "Unexpected saved outcome");
expect(state.a_ready, "The retained slot was modified");
if (state.phase == ota::Phase::SeparateRecoveryRequired) {
expect_rejected([&] { device.boot(); });
}
}
}
void saved_attempts() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare();
device.request_trial(2);
for (int left : {1, 0}) {
device = ota::Device(directory.path());
expect(device.boot() == ota::Slot::B, "Expected trial boot of B");
const ota::Device restored(directory.path());
expect(restored.state().attempts_left == left, "Attempt was not saved before boot");
expect(restored.state().normal == ota::Slot::A, "B was confirmed prematurely");
}
device = ota::Device(directory.path());
expect(device.boot() == ota::Slot::A, "Exhausted trial did not return to A");
expect(device.state().phase == ota::Phase::RecoveryRunning, "Recovery checks skipped");
}
void incomplete_slot() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare(false);
device = ota::Device(directory.path());
expect(!device.state().b_ready, "Incomplete B was marked ready");
expect_rejected([&] { device.request_trial(); });
expect(device.boot() == ota::Slot::A, "Incomplete B was selected");
}
void confirmation_requires_boot() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare();
expect_rejected([&] { device.request_trial(0); });
expect_rejected([&] { device.request_trial(-1); });
device.request_trial();
expect_rejected([&] { device.check(true); });
device.boot();
ota::Device restored(directory.path());
expect(!restored.running(), "Running slot survived reconstruction");
expect_rejected([&] { restored.check(true); });
device.check(true);
expect_rejected([&] { device.check(false); });
}
void unreadable_state() {
ota::TemporaryDirectory directory;
{
std::ofstream output(directory.path() / "boot-state.txt");
output << "incomplete record\n";
}
try {
const ota::Device device(directory.path());
} catch (const std::runtime_error&) {
return;
}
throw std::runtime_error("Unreadable state was accepted");
}
} // namespace
int main() {
try {
scenario_outcomes();
saved_attempts();
incomplete_slot();
confirmation_requires_boot();
unreadable_state();
std::cout << "All recovery invariant checks passed\n";
} catch (const std::exception& error) {
std::cerr << "Test failed: " << error.what() << '\n';
return 1;
}
}
+2
View File
@@ -22,6 +22,8 @@ This repository is not intended to be a production-ready framework. Instead, it
The repository is being developed incrementally as a structured series of articles, diagrams, architecture decisions, and practical examples. The repository is being developed incrementally as a structured series of articles, diagrams, architecture decisions, and practical examples.
Start with the [series overview](docs/00-series-overview.md). The current practical material includes [PlantUML diagrams](diagrams/readme.md) and an [A/B trial boot simulation](examples/simulations/ab-trial-boot/readme.md) with startup and recovery failure scenarios.
## License ## License