Skip to content

Commit 1611123

Browse files
authored
os: Add online update support (#958)
I originally wanted to use [Rauc poller](https://rauc.readthedocs.io/en/latest/reference.html#poller-interface) but unfortunately that feature is not available in Rauc stable (1.5.2 as of writing) yet. See https://rauc.readthedocs.io/en/latest/advanced.html#update-polling So instead I implemented a similar and simpler poll mechanism. There is no automatic poll, user needs to check for updates. See `backend/src/update.md` for API.
1 parent 5f9ee65 commit 1611123

16 files changed

Lines changed: 409 additions & 206 deletions

File tree

backend/src/service.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ app.post("/api/capture", async (req, res) => {
2929
res.json({ url_jpeg: url })
3030
})
3131

32-
app.use("/api/files", express.static("/home/pi/data"))
33-
3432
app.post("/api/reset", async (req, res) => {
3533
await removeConfig()
3634
res.status(200)

backend/src/update.js

Lines changed: 46 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import { reboot } from "../../lib/hardware.js"
1212

1313
import app from "./app.js"
1414
import { watchProperty } from "../../lib/dbus-helpers.js"
15-
16-
const service = systemBus().getService("de.pengutronix.rauc")
17-
const rauc = await service.getInterface("/", "de.pengutronix.rauc.Installer")
15+
import {
16+
installer,
17+
getBundleInfo,
18+
triggerInstall,
19+
checkForUpdate,
20+
} from "../../lib/software.js"
21+
import { procedure, publish } from "../../lib/mqtt.js"
1822

1923
const uploads = multer({
2024
dest: "/data/tmp",
@@ -25,29 +29,6 @@ app.post("/api/update/upload", uploads.single("bundle"), async (req, res) => {
2529
res.json(info)
2630
})
2731

28-
app.post("/api/update/install", express.json(), async (req, res) => {
29-
const { path } = req.body
30-
if (!path) {
31-
res.status(404)
32-
res.end()
33-
return
34-
}
35-
36-
await triggerInstall(path)
37-
38-
res.status(200)
39-
res.end()
40-
})
41-
42-
app.post("/api/update/reboot", async (req, res) => {
43-
res.status(201)
44-
res.end()
45-
46-
setTimeout(() => {
47-
reboot().catch(console.error)
48-
}, 1000)
49-
})
50-
5132
const props = [
5233
"Operation",
5334
"LastError",
@@ -58,80 +39,62 @@ const signals = ["Completed"]
5839

5940
async function getStatus() {
6041
const values = await Promise.all(
61-
props.map((prop) => readProperty(rauc, prop)),
42+
props.map((prop) => readProperty(installer, prop)),
6243
)
6344
const obj = Object.fromEntries(props.map((key, i) => [key, values[i]]))
6445
return obj
6546
}
6647

67-
// Keep track of clients
68-
const clients = new Set()
69-
app.get("/api/update/events", (req, res) => {
70-
// Required SSE headers
71-
res.setHeader("Content-Type", "text/event-stream")
72-
res.setHeader("Cache-Control", "no-cache")
73-
res.setHeader("Connection", "keep-alive")
74-
75-
// Send initial comment to establish connection in some proxies
76-
res.flushHeaders?.()
77-
78-
const client = {
79-
id: Date.now(),
80-
res,
81-
}
82-
clients.add(client)
83-
84-
getStatus()
85-
.then((data) => {
86-
client.res.write(`data: ${JSON.stringify(data)}\n\n`)
87-
})
88-
.catch(console.error)
89-
90-
req.on("close", () => {
91-
clients.delete(client)
92-
})
93-
})
94-
95-
function broadcast(data) {
96-
clients.forEach((client) => {
97-
client.res.write(`data: ${JSON.stringify(data)}\n\n`)
48+
async function publishStatus() {
49+
const status = await getStatus()
50+
await publish("software-updater/status", status, null, {
51+
retain: true,
9852
})
9953
}
10054

10155
props.forEach((prop) => {
102-
watchProperty(rauc, prop).subscribe(async () => {
103-
const status = await getStatus()
104-
broadcast(status)
105-
})
56+
watchProperty(installer, prop).subscribe(() => publishStatus())
10657
})
10758

108-
// https://rauc.readthedocs.io/en/latest/reference.html#installbundle-method
109-
async function triggerInstall(path) {
110-
await rauc.InstallBundle(path, [])
111-
const current_operation = await readProperty(rauc, "Operation")
112-
if (current_operation !== "installing")
113-
throw new Error(`Current rauc operation is "${current_operation}".`)
114-
}
59+
await publishStatus()
60+
61+
await procedure("software-updater", async (data) => {
62+
if (data.action == "poll") {
63+
await poll()
64+
return
65+
}
11566

116-
// https://rauc.readthedocs.io/en/latest/reference.html#inspectbundle-method
117-
async function getBundleInfo(path) {
118-
const raw = await rauc.InspectBundle(path, [])
119-
const normalized = normalizeDbus(raw)
120-
const dictEntries = normalized[0]
121-
const result = Object.fromEntries(dictEntries)
67+
if (data.action == "install") {
68+
await triggerInstall(data.uri)
69+
return
70+
}
12271

123-
const { version, compatible, build } = result.update
72+
if (data.action == "info") {
73+
return getBundleInfo(data.uri)
74+
}
12475

125-
return {
126-
version,
127-
build,
128-
compatible,
129-
path,
76+
if (data.action == "reboot") {
77+
setTimeout(() => {
78+
reboot().catch(console.error)
79+
}, 1000)
80+
return
13081
}
82+
})
83+
84+
async function poll() {
85+
const bundle_info = await checkForUpdate()
86+
await publish(
87+
"software-updater/update-available",
88+
[!!bundle_info, bundle_info],
89+
null,
90+
{
91+
retain: true,
92+
},
93+
)
13194
}
13295

13396
if (import.meta.main) {
13497
console.log(await getStatus())
135-
// await triggerInstall("/data/tmp/fpp")
136-
// await getBundleInfo("/data/tmp/e5464c1b6a138ca358e9683cbe5d73f8")
98+
const bundle_info = await getBundleInfo(url)
99+
console.log(bundle_info)
137100
}

backend/src/update.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# software-updater service
2+
3+
backend automatically starts this software-updater service
4+
5+
### API
6+
7+
### Poll for an update:
8+
9+
**topic** `software-updater`
10+
11+
**payload:**
12+
```json
13+
{
14+
"action": "poll",
15+
}
16+
```
17+
18+
It will instruct the service to check for a new update.
19+
20+
See status below for watching availability of update.
21+
22+
### Info about an update:
23+
24+
**topic** `software-updater`
25+
26+
**payload:**
27+
```json
28+
{
29+
"action": "info",
30+
"uri": "..."
31+
}
32+
```
33+
34+
`uri` can be a http(s) URL or a path on disk.
35+
It will respond with something like:
36+
37+
```json
38+
{
39+
"version": "some-version-string",
40+
"build": "some-unique-build-id-string",
41+
"compatible": "PlanktoScope,rev1",
42+
"uri": "the uri passed to the payload"
43+
}
44+
```
45+
46+
### Install an update:
47+
48+
**topic** `software-updater`
49+
50+
**payload:**
51+
```json
52+
{
53+
"action": "install",
54+
"uri": "..."
55+
}
56+
```
57+
58+
`uri` can be a http(s) URL or a path on disk.
59+
60+
See status below for watching progress of install.
61+
62+
### Reboot:
63+
64+
**topic** `software-updater`
65+
66+
**payload:**
67+
```json
68+
{
69+
"action": "reboot"
70+
}
71+
```
72+
73+
Reboots the PlanktoScope.
74+
This is necessary after an update installation completes.
75+
76+
### status
77+
78+
**topic** `software-updater/status`
79+
80+
**payload:**
81+
```json
82+
{
83+
"Operation": "", // https://rauc.readthedocs.io/en/v1.15.2/reference.html#operation-property
84+
"LastError": "", // https://rauc.readthedocs.io/en/v1.15.2/reference.html#lasterror-property
85+
"Progress": "" // https://rauc.readthedocs.io/en/v1.15.2/reference.html#progress-property
86+
}
87+
```
88+
89+
**topic** `software-updater/update-available`
90+
91+
**payload when no update available:**
92+
93+
```json
94+
[false, null]
95+
```
96+
97+
**payload when update available:**
98+
99+
```json
100+
[true, {
101+
"version": "some-version-string",
102+
"build": "some-unique-build-id-string",
103+
"compatible": "PlanktoScope,rev1",
104+
"uri": "the uri passed to the payload"
105+
}]
106+
```

frontend/package-lock.json

Lines changed: 0 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"@solidjs/router": "^0.15.4",
1414
"array-shuffle": "^4.0.0",
1515
"autoprefixer": "^10.4.22",
16-
"eventsource-client": "^1.2.0",
1716
"medium-zoom": "^1.1.0",
1817
"mqtt": "^5.14.1",
1918
"observable-polyfill": "^0.0.29",

0 commit comments

Comments
 (0)