hwupdate: PawletOS hardware/peripheral firmware update subsystem
Adds pawlethwd, a native C++ system_ext daemon (coredomain) that detects peripherals, checks the hardware update catalog (ota.php?mode=hardware), verifies payloads, and flashes firmware via authorized handler plugins. - daemon/: manifest parse, libcurl HTTP + sha256, sysfs USB detection, catalog client, dlopen handler registry (fixed path + ABI + allowlist), state persistence, orchestration engine, AIDL service, main. - aidl/: IPawletHardwareUpdate (getComponents/getPendingUpdates/checkNow/...). - include/pawlet/hwh_abi.h: stable C plugin ABI. - handlers/usbdfu/: libpawlethwh_usbdfu.so, DFU 1.1 download over libusb. - sepolicy/: pawlethwd domain, pawlet_hw_data_file, service type. - schemas/, etc/supported_hardware.default.json, README design doc. - hwupdate.mk wires packages + ro.pawlet.hardware.manifest + sepolicy dir; inherited from config/common.mk. Trust: a server-named installer handler is honored only if allowlisted in the immutable supported_hardware.json, maps to a fixed dm-verity-protected .so path, is ABI/id-checked, and every payload is sha256-verified before a handler runs.
This commit is contained in:
@@ -111,6 +111,11 @@ BOARD_SEPOLICY_DIRS += packages/apps/BgUpd/sepolicy
|
||||
# ---------------------------------------------------------------------------
|
||||
$(call inherit-product, vendor/pawlet/config/microg.mk)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hardware update subsystem (pawlethwd) — peripheral firmware updates
|
||||
# ---------------------------------------------------------------------------
|
||||
$(call inherit-product, vendor/pawlet/hwupdate/hwupdate.mk)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PawletOS build-time framework resource overlay (adds new drawables)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2026 oxmc / PawletOS
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Shared handler ABI header, used by the daemon and every handler plugin.
|
||||
cc_library_headers {
|
||||
name: "pawlet_hwh_headers",
|
||||
export_include_dirs: ["include"],
|
||||
}
|
||||
|
||||
// Default image-local hardware manifest. A device tree may install its own
|
||||
// supported_hardware.json to override this. The schemas/ files are
|
||||
// documentation/CI artifacts and are intentionally not shipped on-device.
|
||||
prebuilt_etc {
|
||||
name: "pawlet_supported_hardware_default",
|
||||
src: "etc/supported_hardware.default.json",
|
||||
filename: "supported_hardware.json",
|
||||
sub_dir: "pawlet",
|
||||
system_ext_specific: true,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<!--
|
||||
Copyright (C) 2026 oxmc / PawletOS
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
# PawletOS hardware update subsystem (`pawlethwd`)
|
||||
|
||||
Detects integrated and attached peripherals (displays, touch controllers, docks,
|
||||
MCUs, HATs, automotive hardware, …) and updates their firmware over
|
||||
device-specific transports (USB, i2c, spi, uart, gpio, can). This is separate
|
||||
from the Android system OTA (`me.pawlet.updater`), which updates the OS image.
|
||||
|
||||
## Components
|
||||
|
||||
| Piece | What | Installs to |
|
||||
|-------|------|-------------|
|
||||
| `pawlethwd` | native C++ system daemon: detect → check → verify → flash → state | `/system_ext/bin/pawlethwd` |
|
||||
| `IPawletHardwareUpdate` | AIDL: list components, trigger a check, read state | `pawlet_hwupdate` service |
|
||||
| handler plugins | per-transport `.so` flashers, `dlopen`ed by the daemon | `/system_ext/lib{,64}/pawlet/hwh/libpawlethwh_<id>.so` |
|
||||
| `supported_hardware.json` | image-local authoritative manifest (what this image supports) | `/system_ext/etc/pawlet/supported_hardware.json` |
|
||||
|
||||
Even though the source lives in the vendor repo, the daemon is a **system-side
|
||||
(`system_ext`, `coredomain`)** component — it needs network (firmware download)
|
||||
and registers a binder service, both of which a `/vendor` daemon cannot do
|
||||
without violating AOSP neverallows.
|
||||
|
||||
## Trust model
|
||||
|
||||
The server catalog (`device-updates.json`) only *describes* releases; it never
|
||||
selects executable code. Root of trust is the read-only, dm-verity-protected
|
||||
`/system_ext` partition:
|
||||
|
||||
1. A server-named `installer.handler` is honored **only** if that handler id is
|
||||
in the component's `installer_handlers` allowlist in the local, immutable
|
||||
`supported_hardware.json`.
|
||||
2. The allowlisted id maps to exactly
|
||||
`/system_ext/lib64/pawlet/hwh/libpawlethwh_<id>.so`; the daemon refuses any
|
||||
other path and refuses a plugin whose reported `abi_version`/`handler_id`
|
||||
don't match.
|
||||
3. Every payload is SHA-256 verified against the catalog before a handler runs.
|
||||
|
||||
So the server can only request handlers already baked into the signed image.
|
||||
|
||||
## Flow
|
||||
|
||||
```
|
||||
boot / hotplug / AIDL checkNow
|
||||
│
|
||||
▼
|
||||
read supported_hardware.json ──▶ fetch catalog (ota.php?mode=hardware&target=catalog)
|
||||
│ │
|
||||
│ detect present hardware (USB VID/PID, DT compatible, …)
|
||||
│ │
|
||||
└── for each detected component ALSO supported by the local manifest:
|
||||
read installed version (handler get_version)
|
||||
query ota.php?mode=hardware&target=update&… ─▶ newest compatible or 204
|
||||
download + sha256 verify
|
||||
dlopen allowlisted handler .so → flash()
|
||||
persist installed version / retry state
|
||||
```
|
||||
|
||||
## Handler plugin ABI
|
||||
|
||||
See `include/pawlet/hwh_abi.h`. A handler is a `.so` exporting a single symbol
|
||||
`pawlet_hwh_entry()` returning a `const pawlet_hwh_api*` vtable
|
||||
(`abi_version`, `handler_id`, `detect`, `get_version`, `flash`). The USB DFU
|
||||
handler (`handlers/usbdfu`, `libpawlethwh_usbdfu.so`) is the first
|
||||
implementation.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
hwupdate/
|
||||
aidl/ IPawletHardwareUpdate + parcelables
|
||||
include/pawlet/ hwh_abi.h (shared by daemon + handlers)
|
||||
daemon/ pawlethwd sources
|
||||
handlers/usbdfu/ libpawlethwh_usbdfu.so
|
||||
sepolicy/ pawlethwd domain
|
||||
etc/ init.pawlethwd.rc, supported_hardware.default.json
|
||||
schemas/ JSON schemas (CI validation)
|
||||
hwupdate.mk product inclusion (prop + manifest + modules)
|
||||
```
|
||||
|
||||
Per-device manifests may override the default by installing their own
|
||||
`supported_hardware.json` from the device tree.
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2026 oxmc / PawletOS
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
aidl_interface {
|
||||
name: "me.pawlet.hardware.update",
|
||||
unstable: true,
|
||||
srcs: ["me/pawlet/hardware/update/*.aidl"],
|
||||
backend: {
|
||||
cpp: {
|
||||
enabled: true,
|
||||
},
|
||||
ndk: {
|
||||
enabled: true,
|
||||
},
|
||||
java: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package me.pawlet.hardware.update;
|
||||
|
||||
/** Status of a hardware component known to the image manifest. */
|
||||
parcelable HwComponent {
|
||||
/** Stable component id from supported_hardware.json. */
|
||||
@utf8InCpp String componentId;
|
||||
/** Owning product id. */
|
||||
@utf8InCpp String productId;
|
||||
/** Component class, e.g. "microcontroller", "touch-controller". */
|
||||
@utf8InCpp String componentClass;
|
||||
/** Role of this instance, e.g. "primary-touch" (may be empty). */
|
||||
@utf8InCpp String role;
|
||||
/** Transport, e.g. "usb", "i2c". */
|
||||
@utf8InCpp String transport;
|
||||
/** Whether the component was detected as present on the device. */
|
||||
boolean present;
|
||||
/** Installed firmware version if known (may be empty). */
|
||||
@utf8InCpp String installedVersion;
|
||||
/** Detected hardware revision if known (may be empty). */
|
||||
@utf8InCpp String hardwareRevision;
|
||||
/** Support level from the manifest: full/experimental/limited/deprecated. */
|
||||
@utf8InCpp String support;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package me.pawlet.hardware.update;
|
||||
|
||||
/** A firmware update available (or last applied) for a component. */
|
||||
parcelable HwUpdateInfo {
|
||||
@utf8InCpp String componentId;
|
||||
/** True if a newer compatible update is available for this component. */
|
||||
boolean available;
|
||||
/** Offered firmware version (may be empty when none available). */
|
||||
@utf8InCpp String version;
|
||||
/** Payload size in bytes. */
|
||||
long sizeBytes;
|
||||
/** Optional changelog URL. */
|
||||
@utf8InCpp String changelogUrl;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package me.pawlet.hardware.update;
|
||||
|
||||
import me.pawlet.hardware.update.HwComponent;
|
||||
import me.pawlet.hardware.update.HwUpdateInfo;
|
||||
|
||||
/**
|
||||
* Binder interface to the PawletOS hardware update daemon (pawlethwd).
|
||||
* Registered as "pawlet_hwupdate". Read/trigger only — the daemon performs all
|
||||
* detection, download, verification, and flashing itself.
|
||||
*/
|
||||
interface IPawletHardwareUpdate {
|
||||
/** Components known to the image manifest with current detection state. */
|
||||
HwComponent[] getComponents();
|
||||
|
||||
/** Updates currently available for detected, supported components. */
|
||||
HwUpdateInfo[] getPendingUpdates();
|
||||
|
||||
/**
|
||||
* Trigger a detect + check (+ apply, per policy) cycle. Returns immediately;
|
||||
* the cycle runs asynchronously. Use getLastResult()/getComponents() to
|
||||
* observe progress.
|
||||
*/
|
||||
void checkNow();
|
||||
|
||||
/** Whether a check/apply cycle is currently running. */
|
||||
boolean isBusy();
|
||||
|
||||
/** Result code of the last completed cycle (0 == success). */
|
||||
int getLastResult();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2026 oxmc / PawletOS
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
cc_binary {
|
||||
name: "pawlethwd",
|
||||
system_ext_specific: true,
|
||||
srcs: ["*.cpp"],
|
||||
local_include_dirs: ["."],
|
||||
header_libs: ["pawlet_hwh_headers"],
|
||||
shared_libs: [
|
||||
"libbase",
|
||||
"liblog",
|
||||
"libbinder",
|
||||
"libutils",
|
||||
"libcutils",
|
||||
"libcurl",
|
||||
"libcrypto",
|
||||
"libjsoncpp",
|
||||
"me.pawlet.hardware.update-cpp",
|
||||
],
|
||||
cflags: [
|
||||
"-Wall",
|
||||
"-Werror",
|
||||
"-Wextra",
|
||||
],
|
||||
init_rc: ["init.pawlethwd.rc"],
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "catalog.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <json/json.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
#include "http.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
namespace {
|
||||
|
||||
std::string Escape(CURL* c, const std::string& s) {
|
||||
char* e = curl_easy_escape(c, s.c_str(), static_cast<int>(s.size()));
|
||||
std::string out = e ? e : s;
|
||||
if (e) curl_free(e);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string BuildUrl(const std::string& baseUrl, const CheckRequest& r) {
|
||||
CURL* c = curl_easy_init();
|
||||
std::ostringstream u;
|
||||
u << baseUrl;
|
||||
// baseUrl already contains "?mode=hardware"; append remaining params.
|
||||
u << "&target=update";
|
||||
u << "&device=" << Escape(c, r.device);
|
||||
u << "&pawlet_version=" << Escape(c, r.pawletVersion);
|
||||
u << "&hardware_id=" << Escape(c, r.hardwareId);
|
||||
u << "&component_id=" << Escape(c, r.componentId);
|
||||
if (!r.hardwareRevision.empty()) u << "&hardware_revision=" << Escape(c, r.hardwareRevision);
|
||||
if (!r.currentVersion.empty()) u << "¤t_version=" << Escape(c, r.currentVersion);
|
||||
if (!r.bootloaderVersion.empty()) u << "&bootloader_version=" << Escape(c, r.bootloaderVersion);
|
||||
if (c) curl_easy_cleanup(c);
|
||||
return u.str();
|
||||
}
|
||||
|
||||
std::optional<UpdateEntry> ParseUpdate(const std::string& body) {
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder b;
|
||||
std::string errs;
|
||||
std::istringstream in(body);
|
||||
if (!Json::parseFromStream(b, in, &root, &errs)) {
|
||||
LOG(WARNING) << "hwupdate: bad update response: " << errs;
|
||||
return std::nullopt;
|
||||
}
|
||||
const Json::Value& u = root["update"];
|
||||
if (!u.isObject()) return std::nullopt;
|
||||
|
||||
UpdateEntry e;
|
||||
e.version = u.get("version", "").asString();
|
||||
e.datetime = u.get("datetime", 0).asInt64();
|
||||
e.filename = u.get("filename", "").asString();
|
||||
e.id = u.get("id", "").asString();
|
||||
e.sizeBytes = u.get("size_bytes", 0).asInt64();
|
||||
e.url = u.get("url", "").asString();
|
||||
e.checksumSha256 = u.get("checksum_sha256", "").asString();
|
||||
e.changelogUrl = u.get("changelog_url", "").asString();
|
||||
const Json::Value& inst = u["installer"];
|
||||
e.installerType = inst.get("type", "").asString();
|
||||
e.installerHandler = inst.get("handler", "").asString();
|
||||
|
||||
if (e.url.empty() || e.checksumSha256.empty() || e.installerHandler.empty()) {
|
||||
LOG(WARNING) << "hwupdate: update entry missing required fields";
|
||||
return std::nullopt;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<UpdateEntry> CheckUpdate(const std::string& baseUrl, const CheckRequest& req) {
|
||||
std::string url = BuildUrl(baseUrl, req);
|
||||
std::string body;
|
||||
long code = 0;
|
||||
if (!http::Get(url, &body, &code)) return std::nullopt;
|
||||
|
||||
if (code == 204) return std::nullopt; // up to date
|
||||
if (code != 200) {
|
||||
LOG(WARNING) << "hwupdate: update check http=" << code << " for " << req.componentId;
|
||||
return std::nullopt;
|
||||
}
|
||||
return ParseUpdate(body);
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
struct CheckRequest {
|
||||
std::string device;
|
||||
std::string pawletVersion;
|
||||
std::string hardwareId;
|
||||
std::string componentId;
|
||||
std::string hardwareRevision;
|
||||
std::string currentVersion;
|
||||
std::string bootloaderVersion; // optional
|
||||
};
|
||||
|
||||
// Query ota.php?mode=hardware&target=update for the newest compatible update.
|
||||
// Returns the update on HTTP 200, nullopt on 204 (up to date) or any error. The
|
||||
// server performs compatibility/version filtering; the daemon re-checks trust.
|
||||
std::optional<UpdateEntry> CheckUpdate(const std::string& baseUrl, const CheckRequest& req);
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "detect.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
|
||||
#include <android-base/file.h>
|
||||
#include <android-base/logging.h>
|
||||
#include <android-base/strings.h>
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct UsbDev {
|
||||
std::string vid; // lowercase hex, e.g. "1209"
|
||||
std::string pid; // lowercase hex
|
||||
std::string node; // /dev/bus/usb/BBB/DDD
|
||||
};
|
||||
|
||||
std::string ReadTrimmed(const std::string& path) {
|
||||
std::string s;
|
||||
if (!android::base::ReadFileToString(path, &s)) return {};
|
||||
return android::base::Trim(s);
|
||||
}
|
||||
|
||||
// Enumerate connected USB devices from sysfs.
|
||||
std::vector<UsbDev> EnumUsb() {
|
||||
std::vector<UsbDev> out;
|
||||
const std::string base = "/sys/bus/usb/devices";
|
||||
std::error_code ec;
|
||||
if (!fs::exists(base, ec)) return out;
|
||||
for (const auto& entry : fs::directory_iterator(base, ec)) {
|
||||
const std::string dir = entry.path().string();
|
||||
std::string vid = ReadTrimmed(dir + "/idVendor");
|
||||
std::string pid = ReadTrimmed(dir + "/idProduct");
|
||||
if (vid.empty() || pid.empty()) continue; // hubs/interfaces, skip
|
||||
|
||||
UsbDev d;
|
||||
d.vid = android::base::Trim(vid);
|
||||
d.pid = android::base::Trim(pid);
|
||||
std::string bus = ReadTrimmed(dir + "/busnum");
|
||||
std::string dev = ReadTrimmed(dir + "/devnum");
|
||||
if (!bus.empty() && !dev.empty()) {
|
||||
char node[64];
|
||||
snprintf(node, sizeof(node), "/dev/bus/usb/%03d/%03d", atoi(bus.c_str()),
|
||||
atoi(dev.c_str()));
|
||||
d.node = node;
|
||||
}
|
||||
out.push_back(std::move(d));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool UsbMatch(const std::vector<UsbDev>& devs, const std::string& vid, const std::string& pid,
|
||||
std::string* node) {
|
||||
for (const auto& d : devs) {
|
||||
if (android::base::EqualsIgnoreCase(d.vid, vid) &&
|
||||
android::base::EqualsIgnoreCase(d.pid, pid)) {
|
||||
if (node) *node = d.node;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<DetectedComponent> Detect(const Manifest& manifest) {
|
||||
std::vector<UsbDev> usb = EnumUsb();
|
||||
std::vector<DetectedComponent> out;
|
||||
|
||||
for (const auto& prod : manifest.products) {
|
||||
for (const auto& comp : prod.components) {
|
||||
DetectedComponent dc;
|
||||
dc.product = ∏
|
||||
dc.comp = ∁
|
||||
|
||||
if (!comp.usbVendorId.empty() && !comp.usbProductId.empty()) {
|
||||
std::string node;
|
||||
if (UsbMatch(usb, comp.usbVendorId, comp.usbProductId, &node)) {
|
||||
dc.present = true;
|
||||
dc.transport = "usb";
|
||||
dc.usbVendorId = comp.usbVendorId;
|
||||
dc.usbProductId = comp.usbProductId;
|
||||
dc.devPath = node;
|
||||
}
|
||||
}
|
||||
// TODO: device-tree / i2c / spi detectors go here.
|
||||
|
||||
if (dc.present) {
|
||||
LOG(INFO) << "hwupdate: detected " << comp.id << " on " << dc.transport;
|
||||
}
|
||||
out.push_back(std::move(dc));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
// Detect which manifest components are physically present. For USB components
|
||||
// this enumerates /sys/bus/usb/devices and matches idVendor/idProduct; other
|
||||
// transports report not-present until their detector is added. Pointers in the
|
||||
// returned entries reference into the passed-in manifest, which must outlive
|
||||
// the result.
|
||||
std::vector<DetectedComponent> Detect(const Manifest& manifest);
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "engine.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
#include "catalog.h"
|
||||
#include "detect.h"
|
||||
#include "http.h"
|
||||
#include "manifest.h"
|
||||
#include "pawlet/hwh_abi.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
bool Contains(const std::vector<std::string>& v, const std::string& s) {
|
||||
return std::find(v.begin(), v.end(), s) != v.end();
|
||||
}
|
||||
|
||||
void ProgressCb(int percent, void* user) {
|
||||
const char* id = static_cast<const char*>(user);
|
||||
LOG(INFO) << "hwupdate: flashing " << (id ? id : "?") << " " << percent << "%";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Engine::Engine(EngineConfig cfg) : cfg_(std::move(cfg)), state_(cfg_.stateDir + "/state.json") {}
|
||||
|
||||
Engine::~Engine() { Stop(); }
|
||||
|
||||
void Engine::Start() {
|
||||
std::error_code ec;
|
||||
fs::create_directories(cfg_.stateDir, ec);
|
||||
fs::create_directories(cfg_.stateDir + "/downloads", ec);
|
||||
worker_ = std::thread([this] { WorkerLoop(); });
|
||||
CheckNow(); // initial cycle at boot
|
||||
}
|
||||
|
||||
void Engine::Stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(reqMu_);
|
||||
if (stop_) return;
|
||||
stop_ = true;
|
||||
reqCv_.notify_all();
|
||||
}
|
||||
if (worker_.joinable()) worker_.join();
|
||||
}
|
||||
|
||||
void Engine::CheckNow() {
|
||||
std::lock_guard<std::mutex> lk(reqMu_);
|
||||
requested_ = true;
|
||||
reqCv_.notify_all();
|
||||
}
|
||||
|
||||
void Engine::WorkerLoop() {
|
||||
for (;;) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(reqMu_);
|
||||
reqCv_.wait(lk, [this] { return requested_ || stop_; });
|
||||
if (stop_) return;
|
||||
requested_ = false;
|
||||
}
|
||||
busy_.store(true);
|
||||
int rc = RunCycle();
|
||||
lastResult_.store(rc);
|
||||
busy_.store(false);
|
||||
}
|
||||
}
|
||||
|
||||
int Engine::RunCycle() {
|
||||
auto manifest = LoadManifest(cfg_.manifestPath);
|
||||
if (!manifest) return -1;
|
||||
|
||||
state_.Load();
|
||||
std::vector<DetectedComponent> detected = Detect(*manifest);
|
||||
|
||||
std::vector<ComponentStatus> comps;
|
||||
std::vector<PendingUpdate> pend;
|
||||
|
||||
for (auto& dc : detected) {
|
||||
const ManifestProduct* prod = dc.product;
|
||||
const ManifestComponent* comp = dc.comp;
|
||||
|
||||
ComponentStatus cs;
|
||||
cs.componentId = comp->id;
|
||||
cs.productId = prod->id;
|
||||
cs.componentClass = comp->cls;
|
||||
cs.role = comp->role;
|
||||
cs.transport = dc.transport;
|
||||
cs.present = dc.present;
|
||||
cs.hardwareRevision = dc.hardwareRev;
|
||||
cs.support = comp->support;
|
||||
|
||||
if (!dc.present) {
|
||||
comps.push_back(std::move(cs));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve a usable handler + read the installed version.
|
||||
const pawlet_hwh_api* handler = nullptr;
|
||||
for (const auto& hid : comp->installerHandlers) {
|
||||
handler = handlers_.Get(hid);
|
||||
if (handler) break;
|
||||
}
|
||||
|
||||
pawlet_hw_ctx ctx{};
|
||||
ctx.component_id = comp->id.c_str();
|
||||
ctx.transport = dc.transport.c_str();
|
||||
ctx.usb_vendor_id = dc.usbVendorId.c_str();
|
||||
ctx.usb_product_id = dc.usbProductId.c_str();
|
||||
ctx.dev_path = dc.devPath.c_str();
|
||||
ctx.hardware_rev = dc.hardwareRev.c_str();
|
||||
|
||||
std::string installed = state_.InstalledVersion(comp->id);
|
||||
if (handler) {
|
||||
char ver[128] = {0};
|
||||
if (handler->get_version(&ctx, ver, sizeof(ver)) == PAWLET_HWH_OK && ver[0]) {
|
||||
installed = ver;
|
||||
}
|
||||
}
|
||||
cs.installedVersion = installed;
|
||||
|
||||
// Ask the server for the newest compatible update.
|
||||
CheckRequest req;
|
||||
req.device = manifest->device;
|
||||
req.pawletVersion = manifest->buildVersion;
|
||||
req.hardwareId = prod->id;
|
||||
req.componentId = comp->id;
|
||||
req.hardwareRevision = dc.hardwareRev;
|
||||
req.currentVersion = installed;
|
||||
|
||||
auto update = CheckUpdate(cfg_.serverBaseUrl, req);
|
||||
|
||||
PendingUpdate pu;
|
||||
pu.componentId = comp->id;
|
||||
if (update && update->version != installed) {
|
||||
pu.available = true;
|
||||
pu.version = update->version;
|
||||
pu.sizeBytes = update->sizeBytes;
|
||||
pu.changelogUrl = update->changelogUrl;
|
||||
|
||||
// Trust boundary: the server-named handler MUST be allowlisted for
|
||||
// this component in the immutable manifest.
|
||||
if (!Contains(comp->installerHandlers, update->installerHandler)) {
|
||||
LOG(ERROR) << "hwupdate: refusing non-allowlisted handler '"
|
||||
<< update->installerHandler << "' for " << comp->id;
|
||||
} else if (cfg_.autoApply) {
|
||||
if (state_.RetryCount(comp->id) >= cfg_.maxRetries) {
|
||||
LOG(WARNING) << "hwupdate: " << comp->id << " exceeded retries; skipping";
|
||||
} else {
|
||||
const pawlet_hwh_api* flashHandler = handlers_.Get(update->installerHandler);
|
||||
std::string dl = cfg_.stateDir + "/downloads/" + update->id + "-" +
|
||||
update->filename;
|
||||
bool ok = flashHandler && http::Download(update->url, dl);
|
||||
if (ok) {
|
||||
std::string sum = util::Sha256File(dl);
|
||||
if (!util::IEquals(sum, update->checksumSha256)) {
|
||||
LOG(ERROR) << "hwupdate: checksum mismatch for " << comp->id;
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
int r = flashHandler->flash(&ctx, dl.c_str(), ProgressCb,
|
||||
const_cast<char*>(comp->id.c_str()));
|
||||
if (r == PAWLET_HWH_OK) {
|
||||
LOG(INFO) << "hwupdate: flashed " << comp->id << " -> "
|
||||
<< update->version;
|
||||
state_.SetInstalledVersion(comp->id, update->version);
|
||||
state_.SetRetryCount(comp->id, 0);
|
||||
cs.installedVersion = update->version;
|
||||
pu.available = false; // applied
|
||||
} else {
|
||||
LOG(ERROR) << "hwupdate: flash failed rc=" << r << " for " << comp->id;
|
||||
state_.SetRetryCount(comp->id, state_.RetryCount(comp->id) + 1);
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
state_.SetRetryCount(comp->id, state_.RetryCount(comp->id) + 1);
|
||||
}
|
||||
std::error_code ec;
|
||||
fs::remove(dl, ec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pu.available) pend.push_back(std::move(pu));
|
||||
comps.push_back(std::move(cs));
|
||||
}
|
||||
|
||||
state_.Save();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(snapMu_);
|
||||
components_ = std::move(comps);
|
||||
pending_ = std::move(pend);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<ComponentStatus> Engine::Components() const {
|
||||
std::lock_guard<std::mutex> lk(snapMu_);
|
||||
return components_;
|
||||
}
|
||||
|
||||
std::vector<PendingUpdate> Engine::Pending() const {
|
||||
std::lock_guard<std::mutex> lk(snapMu_);
|
||||
return pending_;
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "handlers.h"
|
||||
#include "state.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
// Thread-safe snapshots exposed to the AIDL layer.
|
||||
struct ComponentStatus {
|
||||
std::string componentId;
|
||||
std::string productId;
|
||||
std::string componentClass;
|
||||
std::string role;
|
||||
std::string transport;
|
||||
bool present = false;
|
||||
std::string installedVersion;
|
||||
std::string hardwareRevision;
|
||||
std::string support;
|
||||
};
|
||||
|
||||
struct PendingUpdate {
|
||||
std::string componentId;
|
||||
bool available = false;
|
||||
std::string version;
|
||||
int64_t sizeBytes = 0;
|
||||
std::string changelogUrl;
|
||||
};
|
||||
|
||||
struct EngineConfig {
|
||||
std::string manifestPath;
|
||||
std::string serverBaseUrl; // ".../ota.php?mode=hardware"
|
||||
std::string stateDir; // e.g. /data/pawlet/hw
|
||||
int maxRetries = 3;
|
||||
bool autoApply = true;
|
||||
};
|
||||
|
||||
class Engine {
|
||||
public:
|
||||
explicit Engine(EngineConfig cfg);
|
||||
~Engine();
|
||||
|
||||
void Start(); // spawns worker, runs an initial cycle
|
||||
void Stop();
|
||||
|
||||
// Request an asynchronous detect+check(+apply) cycle.
|
||||
void CheckNow();
|
||||
|
||||
// Snapshots (safe to call from binder threads).
|
||||
std::vector<ComponentStatus> Components() const;
|
||||
std::vector<PendingUpdate> Pending() const;
|
||||
bool IsBusy() const { return busy_.load(); }
|
||||
int LastResult() const { return lastResult_.load(); }
|
||||
|
||||
private:
|
||||
void WorkerLoop();
|
||||
int RunCycle(); // returns 0 on success
|
||||
|
||||
EngineConfig cfg_;
|
||||
HandlerRegistry handlers_;
|
||||
State state_;
|
||||
|
||||
mutable std::mutex snapMu_;
|
||||
std::vector<ComponentStatus> components_;
|
||||
std::vector<PendingUpdate> pending_;
|
||||
|
||||
std::thread worker_;
|
||||
std::mutex reqMu_;
|
||||
std::condition_variable reqCv_;
|
||||
bool requested_ = false;
|
||||
bool stop_ = false;
|
||||
std::atomic<bool> busy_{false};
|
||||
std::atomic<int> lastResult_{0};
|
||||
};
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "handlers.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
#include "util.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
namespace {
|
||||
|
||||
constexpr char kHandlerDir[] = "/system_ext/lib64/pawlet/hwh/";
|
||||
|
||||
// Handler ids come from the local allowlist; still, only accept a conservative
|
||||
// character set so the id can never escape kHandlerDir.
|
||||
bool ValidId(const std::string& id) {
|
||||
if (id.empty() || id.size() > 64) return false;
|
||||
for (char c : id) {
|
||||
bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-';
|
||||
if (!ok) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const pawlet_hwh_api* HandlerRegistry::Get(const std::string& id) {
|
||||
if (auto it = cache_.find(id); it != cache_.end()) return it->second;
|
||||
|
||||
if (!ValidId(id)) {
|
||||
LOG(ERROR) << "hwupdate: rejecting invalid handler id '" << id << "'";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string path = std::string(kHandlerDir) + "libpawlethwh_" + id + ".so";
|
||||
void* dl = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (!dl) {
|
||||
LOG(ERROR) << "hwupdate: dlopen(" << path << ") failed: " << dlerror();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto entry = reinterpret_cast<const pawlet_hwh_api* (*)()>(dlsym(dl, "pawlet_hwh_entry"));
|
||||
if (!entry) {
|
||||
LOG(ERROR) << "hwupdate: " << path << " missing pawlet_hwh_entry";
|
||||
dlclose(dl);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const pawlet_hwh_api* api = entry();
|
||||
if (!api || api->abi_version != PAWLET_HWH_ABI || !api->handler_id ||
|
||||
!util::IEquals(api->handler_id, id) || !api->detect || !api->get_version ||
|
||||
!api->flash) {
|
||||
LOG(ERROR) << "hwupdate: " << path << " failed ABI/id validation";
|
||||
dlclose(dl);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LOG(INFO) << "hwupdate: loaded handler '" << id << "' (abi " << api->abi_version << ")";
|
||||
// Intentionally keep dl open for the daemon's lifetime.
|
||||
cache_[id] = api;
|
||||
return api;
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "pawlet/hwh_abi.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
// Loads handler plugins by id from the fixed, dm-verity-protected directory
|
||||
// /system_ext/lib64/pawlet/hwh/libpawlethwh_<id>.so. The id is validated by the
|
||||
// caller against the component's installer_handlers allowlist BEFORE calling
|
||||
// Get(); this class additionally verifies the ABI version and that the plugin's
|
||||
// self-reported handler_id matches the requested id. It never derives a path
|
||||
// from untrusted input other than the (allowlisted) id.
|
||||
class HandlerRegistry {
|
||||
public:
|
||||
// Returns the handler vtable for id, or nullptr if it cannot be loaded or
|
||||
// fails validation. Loaded handlers are cached for the daemon's lifetime.
|
||||
const pawlet_hwh_api* Get(const std::string& id);
|
||||
|
||||
private:
|
||||
std::map<std::string, const pawlet_hwh_api*> cache_;
|
||||
};
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "http.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
namespace pawlet::hwupdate::http {
|
||||
namespace {
|
||||
|
||||
constexpr long kConnectTimeoutSec = 15;
|
||||
constexpr long kTransferTimeoutSec = 300;
|
||||
constexpr char kUserAgent[] = "pawlethwd/1.0";
|
||||
|
||||
size_t WriteToString(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
auto* out = static_cast<std::string*>(userdata);
|
||||
out->append(ptr, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
size_t WriteToFile(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
auto* f = static_cast<FILE*>(userdata);
|
||||
return fwrite(ptr, size, nmemb, f) * size;
|
||||
}
|
||||
|
||||
void ApplyCommon(CURL* c, const std::string& url) {
|
||||
curl_easy_setopt(c, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, kUserAgent);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_MAXREDIRS, 5L);
|
||||
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, kConnectTimeoutSec);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT, kTransferTimeoutSec);
|
||||
curl_easy_setopt(c, CURLOPT_FAILONERROR, 0L);
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
// HTTPS only, verified against the system CA store.
|
||||
curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(c, CURLOPT_CAPATH, "/system/etc/security/cacerts");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Get(const std::string& url, std::string* body, long* httpCode) {
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) return false;
|
||||
body->clear();
|
||||
ApplyCommon(c, url);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, WriteToString);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, body);
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
if (rc == CURLE_OK && httpCode) {
|
||||
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, httpCode);
|
||||
}
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) {
|
||||
LOG(WARNING) << "hwupdate: GET failed: " << curl_easy_strerror(rc);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Download(const std::string& url, const std::string& destPath) {
|
||||
FILE* f = fopen(destPath.c_str(), "wb");
|
||||
if (!f) {
|
||||
LOG(ERROR) << "hwupdate: cannot open " << destPath << " for write";
|
||||
return false;
|
||||
}
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
ApplyCommon(c, url);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, WriteToFile);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, f);
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
long code = 0;
|
||||
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code);
|
||||
curl_easy_cleanup(c);
|
||||
fclose(f);
|
||||
|
||||
if (rc != CURLE_OK || code != 200) {
|
||||
LOG(WARNING) << "hwupdate: download failed rc=" << curl_easy_strerror(rc)
|
||||
<< " http=" << code;
|
||||
remove(destPath.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate::http
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace pawlet::hwupdate::http {
|
||||
|
||||
// GET url. On success returns true and sets body + httpCode. Network/transport
|
||||
// failures return false.
|
||||
bool Get(const std::string& url, std::string* body, long* httpCode);
|
||||
|
||||
// Download url to destPath. Returns true only on HTTP 200 with the body fully
|
||||
// written to destPath.
|
||||
bool Download(const std::string& url, const std::string& destPath);
|
||||
|
||||
} // namespace pawlet::hwupdate::http
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright (C) 2026 oxmc / PawletOS
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
on post-fs-data
|
||||
mkdir /data/pawlet 0771 root system
|
||||
mkdir /data/pawlet/hw 0770 root system
|
||||
|
||||
service pawlethwd /system_ext/bin/pawlethwd
|
||||
class late_start
|
||||
user root
|
||||
group root system inet
|
||||
# Long-running binder service; init restarts it if it exits.
|
||||
# Detects peripherals, checks the hardware update catalog, and flashes
|
||||
# firmware via authorized handler plugins. Starts after /data is mounted.
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <binder/IPCThreadState.h>
|
||||
#include <binder/IServiceManager.h>
|
||||
#include <binder/ProcessState.h>
|
||||
#include <utils/String16.h>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
#include "engine.h"
|
||||
#include "service.h"
|
||||
#include "util.h"
|
||||
|
||||
using namespace pawlet::hwupdate;
|
||||
|
||||
namespace {
|
||||
constexpr char kServiceName[] = "pawlet_hwupdate";
|
||||
constexpr char kDefaultManifest[] = "/system_ext/etc/pawlet/supported_hardware.json";
|
||||
constexpr char kDefaultServer[] = "https://oxmc.me/apis/aosp/ota.php?mode=hardware";
|
||||
constexpr char kStateDir[] = "/data/pawlet/hw";
|
||||
} // namespace
|
||||
|
||||
int main(int, char**) {
|
||||
android::base::InitLogging(nullptr, android::base::LogdLogger());
|
||||
LOG(INFO) << "pawlethwd starting";
|
||||
|
||||
if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) {
|
||||
LOG(ERROR) << "pawlethwd: curl_global_init failed";
|
||||
return 1;
|
||||
}
|
||||
|
||||
EngineConfig cfg;
|
||||
cfg.manifestPath = util::Prop("ro.pawlet.hardware.manifest", kDefaultManifest);
|
||||
cfg.serverBaseUrl = util::Prop("ro.pawlet.hwupdate.uri", kDefaultServer);
|
||||
cfg.stateDir = kStateDir;
|
||||
cfg.autoApply = util::Prop("persist.pawlet.hwupdate.auto_apply", "true") != "false";
|
||||
|
||||
Engine engine(cfg);
|
||||
engine.Start();
|
||||
|
||||
android::sp<HwUpdateService> service = android::sp<HwUpdateService>::make(&engine);
|
||||
android::status_t st = android::defaultServiceManager()->addService(
|
||||
android::String16(kServiceName), service);
|
||||
if (st != android::OK) {
|
||||
LOG(ERROR) << "pawlethwd: failed to register service: " << st;
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG(INFO) << "pawlethwd: service registered as " << kServiceName;
|
||||
android::ProcessState::self()->startThreadPool();
|
||||
android::IPCThreadState::self()->joinThreadPool();
|
||||
|
||||
engine.Stop();
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "manifest.h"
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
namespace {
|
||||
|
||||
std::vector<std::string> StrArray(const Json::Value& v) {
|
||||
std::vector<std::string> out;
|
||||
if (v.isArray()) {
|
||||
for (const auto& e : v) {
|
||||
if (e.isString()) out.push_back(e.asString());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void ReadIdentifiers(const Json::Value& ids, std::string& usbVid, std::string& usbPid,
|
||||
std::vector<std::string>& dtCompat) {
|
||||
if (!ids.isObject()) return;
|
||||
usbVid = ids.get("usb_vendor_id", "").asString();
|
||||
usbPid = ids.get("usb_product_id", "").asString();
|
||||
dtCompat = StrArray(ids["device_tree_compatible"]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<Manifest> LoadManifest(const std::string& path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) {
|
||||
LOG(ERROR) << "hwupdate: cannot open manifest " << path;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder b;
|
||||
std::string errs;
|
||||
if (!Json::parseFromStream(b, in, &root, &errs)) {
|
||||
LOG(ERROR) << "hwupdate: manifest parse error: " << errs;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Manifest m;
|
||||
m.schemaVersion = root.get("schema_version", 0).asInt();
|
||||
if (m.schemaVersion != 1) {
|
||||
LOG(ERROR) << "hwupdate: unsupported manifest schema_version " << m.schemaVersion;
|
||||
return std::nullopt;
|
||||
}
|
||||
m.device = root.get("device", "").asString();
|
||||
m.buildVersion = root.get("build_version", "").asString();
|
||||
|
||||
for (const auto& p : root["supported_hardware"]) {
|
||||
ManifestProduct prod;
|
||||
prod.id = p.get("id", "").asString();
|
||||
prod.manufacturer = p.get("manufacturer", "").asString();
|
||||
prod.cls = p.get("class", "").asString();
|
||||
prod.support = p.get("support", "full").asString();
|
||||
prod.supportedRevisions = StrArray(p["supported_revisions"]);
|
||||
ReadIdentifiers(p["identifiers"], prod.usbVendorId, prod.usbProductId, prod.dtCompatible);
|
||||
|
||||
for (const auto& c : p["components"]) {
|
||||
ManifestComponent comp;
|
||||
comp.id = c.get("id", "").asString();
|
||||
comp.cls = c.get("class", "").asString();
|
||||
comp.role = c.get("role", "").asString();
|
||||
comp.support = c.get("support", "full").asString();
|
||||
comp.supportedRevisions = StrArray(c["supported_revisions"]);
|
||||
comp.installerHandlers = StrArray(c["installer_handlers"]);
|
||||
ReadIdentifiers(c["identifiers"], comp.usbVendorId, comp.usbProductId, comp.dtCompatible);
|
||||
// Fall back to the product's identifiers if the component omits them.
|
||||
if (comp.usbVendorId.empty()) comp.usbVendorId = prod.usbVendorId;
|
||||
if (comp.usbProductId.empty()) comp.usbProductId = prod.usbProductId;
|
||||
if (comp.dtCompatible.empty()) comp.dtCompatible = prod.dtCompatible;
|
||||
prod.components.push_back(std::move(comp));
|
||||
}
|
||||
m.products.push_back(std::move(prod));
|
||||
}
|
||||
|
||||
LOG(INFO) << "hwupdate: loaded manifest device=" << m.device
|
||||
<< " products=" << m.products.size();
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
// Parse the image-local supported_hardware.json at path. Returns nullopt on
|
||||
// read/parse error or unsupported schema version.
|
||||
std::optional<Manifest> LoadManifest(const std::string& path);
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "service.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
namespace aidl = ::me::pawlet::hardware::update;
|
||||
using ::android::binder::Status;
|
||||
|
||||
Status HwUpdateService::getComponents(::std::vector<aidl::HwComponent>* out) {
|
||||
out->clear();
|
||||
for (const auto& c : engine_->Components()) {
|
||||
aidl::HwComponent hc;
|
||||
hc.componentId = c.componentId;
|
||||
hc.productId = c.productId;
|
||||
hc.componentClass = c.componentClass;
|
||||
hc.role = c.role;
|
||||
hc.transport = c.transport;
|
||||
hc.present = c.present;
|
||||
hc.installedVersion = c.installedVersion;
|
||||
hc.hardwareRevision = c.hardwareRevision;
|
||||
hc.support = c.support;
|
||||
out->push_back(std::move(hc));
|
||||
}
|
||||
return Status::ok();
|
||||
}
|
||||
|
||||
Status HwUpdateService::getPendingUpdates(::std::vector<aidl::HwUpdateInfo>* out) {
|
||||
out->clear();
|
||||
for (const auto& p : engine_->Pending()) {
|
||||
aidl::HwUpdateInfo hu;
|
||||
hu.componentId = p.componentId;
|
||||
hu.available = p.available;
|
||||
hu.version = p.version;
|
||||
hu.sizeBytes = p.sizeBytes;
|
||||
hu.changelogUrl = p.changelogUrl;
|
||||
out->push_back(std::move(hu));
|
||||
}
|
||||
return Status::ok();
|
||||
}
|
||||
|
||||
Status HwUpdateService::checkNow() {
|
||||
engine_->CheckNow();
|
||||
return Status::ok();
|
||||
}
|
||||
|
||||
Status HwUpdateService::isBusy(bool* out) {
|
||||
*out = engine_->IsBusy();
|
||||
return Status::ok();
|
||||
}
|
||||
|
||||
Status HwUpdateService::getLastResult(int32_t* out) {
|
||||
*out = engine_->LastResult();
|
||||
return Status::ok();
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <me/pawlet/hardware/update/BnPawletHardwareUpdate.h>
|
||||
|
||||
#include "engine.h"
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
// Binder front-end for the daemon; delegates to Engine. Registered as
|
||||
// "pawlet_hwupdate".
|
||||
class HwUpdateService : public me::pawlet::hardware::update::BnPawletHardwareUpdate {
|
||||
public:
|
||||
explicit HwUpdateService(Engine* engine) : engine_(engine) {}
|
||||
|
||||
::android::binder::Status getComponents(
|
||||
::std::vector<::me::pawlet::hardware::update::HwComponent>* out) override;
|
||||
::android::binder::Status getPendingUpdates(
|
||||
::std::vector<::me::pawlet::hardware::update::HwUpdateInfo>* out) override;
|
||||
::android::binder::Status checkNow() override;
|
||||
::android::binder::Status isBusy(bool* out) override;
|
||||
::android::binder::Status getLastResult(int32_t* out) override;
|
||||
|
||||
private:
|
||||
Engine* engine_;
|
||||
};
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "state.h"
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
State::State(std::string path) : path_(std::move(path)) {}
|
||||
|
||||
void State::Load() {
|
||||
entries_.clear();
|
||||
std::ifstream in(path_, std::ios::binary);
|
||||
if (!in) return; // first run
|
||||
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder b;
|
||||
std::string errs;
|
||||
if (!Json::parseFromStream(b, in, &root, &errs)) {
|
||||
LOG(WARNING) << "hwupdate: state parse error: " << errs;
|
||||
return;
|
||||
}
|
||||
const Json::Value& comps = root["components"];
|
||||
for (const auto& id : comps.getMemberNames()) {
|
||||
Entry e;
|
||||
e.installedVersion = comps[id].get("installed_version", "").asString();
|
||||
e.retryCount = comps[id].get("retry_count", 0).asInt();
|
||||
entries_[id] = e;
|
||||
}
|
||||
}
|
||||
|
||||
bool State::Save() const {
|
||||
Json::Value root;
|
||||
root["schema_version"] = 1;
|
||||
Json::Value comps(Json::objectValue);
|
||||
for (const auto& [id, e] : entries_) {
|
||||
Json::Value c(Json::objectValue);
|
||||
c["installed_version"] = e.installedVersion;
|
||||
c["retry_count"] = e.retryCount;
|
||||
comps[id] = c;
|
||||
}
|
||||
root["components"] = comps;
|
||||
|
||||
// Write atomically: temp file + rename.
|
||||
std::string tmp = path_ + ".tmp";
|
||||
{
|
||||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||||
if (!out) {
|
||||
LOG(ERROR) << "hwupdate: cannot write state " << tmp;
|
||||
return false;
|
||||
}
|
||||
Json::StreamWriterBuilder w;
|
||||
out << Json::writeString(w, root);
|
||||
}
|
||||
if (rename(tmp.c_str(), path_.c_str()) != 0) {
|
||||
LOG(ERROR) << "hwupdate: cannot rename state into place";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string State::InstalledVersion(const std::string& componentId) const {
|
||||
auto it = entries_.find(componentId);
|
||||
return it == entries_.end() ? std::string() : it->second.installedVersion;
|
||||
}
|
||||
|
||||
void State::SetInstalledVersion(const std::string& componentId, const std::string& version) {
|
||||
entries_[componentId].installedVersion = version;
|
||||
}
|
||||
|
||||
int State::RetryCount(const std::string& componentId) const {
|
||||
auto it = entries_.find(componentId);
|
||||
return it == entries_.end() ? 0 : it->second.retryCount;
|
||||
}
|
||||
|
||||
void State::SetRetryCount(const std::string& componentId, int count) {
|
||||
entries_[componentId].retryCount = count;
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
// Persistent per-component state: last successfully installed firmware version
|
||||
// and a retry counter. Backed by a JSON file under /data/pawlet/hw.
|
||||
class State {
|
||||
public:
|
||||
explicit State(std::string path);
|
||||
|
||||
void Load();
|
||||
bool Save() const;
|
||||
|
||||
std::string InstalledVersion(const std::string& componentId) const;
|
||||
void SetInstalledVersion(const std::string& componentId, const std::string& version);
|
||||
|
||||
int RetryCount(const std::string& componentId) const;
|
||||
void SetRetryCount(const std::string& componentId, int count);
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
std::string installedVersion;
|
||||
int retryCount = 0;
|
||||
};
|
||||
std::string path_;
|
||||
std::map<std::string, Entry> entries_;
|
||||
};
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace pawlet::hwupdate {
|
||||
|
||||
// ---- image-local manifest (supported_hardware.json) ----
|
||||
|
||||
struct ManifestComponent {
|
||||
std::string id;
|
||||
std::string cls;
|
||||
std::string role;
|
||||
std::string support;
|
||||
std::vector<std::string> supportedRevisions;
|
||||
std::vector<std::string> installerHandlers;
|
||||
// detection identifiers (optional; may also come from the catalog)
|
||||
std::string usbVendorId;
|
||||
std::string usbProductId;
|
||||
std::vector<std::string> dtCompatible;
|
||||
};
|
||||
|
||||
struct ManifestProduct {
|
||||
std::string id;
|
||||
std::string manufacturer;
|
||||
std::string cls;
|
||||
std::string support;
|
||||
std::vector<std::string> supportedRevisions;
|
||||
std::string usbVendorId;
|
||||
std::string usbProductId;
|
||||
std::vector<std::string> dtCompatible;
|
||||
std::vector<ManifestComponent> components;
|
||||
};
|
||||
|
||||
struct Manifest {
|
||||
int schemaVersion = 0;
|
||||
std::string device;
|
||||
std::string buildVersion;
|
||||
std::vector<ManifestProduct> products;
|
||||
};
|
||||
|
||||
// ---- server update entry (ota.php?mode=hardware&target=update) ----
|
||||
|
||||
struct UpdateEntry {
|
||||
std::string version;
|
||||
int64_t datetime = 0;
|
||||
std::string filename;
|
||||
std::string id;
|
||||
int64_t sizeBytes = 0;
|
||||
std::string url;
|
||||
std::string checksumSha256;
|
||||
std::string changelogUrl;
|
||||
std::string installerType; // usb/i2c/...
|
||||
std::string installerHandler; // handler id named by the server
|
||||
};
|
||||
|
||||
// ---- runtime detection result ----
|
||||
|
||||
struct DetectedComponent {
|
||||
const ManifestProduct* product = nullptr;
|
||||
const ManifestComponent* comp = nullptr;
|
||||
bool present = false;
|
||||
std::string installedVersion;
|
||||
std::string hardwareRev;
|
||||
std::string transport; // resolved transport for this component
|
||||
std::string usbVendorId;
|
||||
std::string usbProductId;
|
||||
std::string devPath;
|
||||
};
|
||||
|
||||
} // namespace pawlet::hwupdate
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "util.h"
|
||||
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
|
||||
#include <android-base/properties.h>
|
||||
|
||||
namespace pawlet::hwupdate::util {
|
||||
|
||||
std::string Sha256File(const std::string& path) {
|
||||
FILE* f = fopen(path.c_str(), "rb");
|
||||
if (!f) return {};
|
||||
|
||||
SHA256_CTX ctx;
|
||||
SHA256_Init(&ctx);
|
||||
std::array<unsigned char, 65536> buf;
|
||||
size_t n;
|
||||
while ((n = fread(buf.data(), 1, buf.size(), f)) > 0) {
|
||||
SHA256_Update(&ctx, buf.data(), n);
|
||||
}
|
||||
bool ok = feof(f) != 0;
|
||||
fclose(f);
|
||||
if (!ok) return {};
|
||||
|
||||
unsigned char digest[SHA256_DIGEST_LENGTH];
|
||||
SHA256_Final(digest, &ctx);
|
||||
|
||||
static const char* hex = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(SHA256_DIGEST_LENGTH * 2);
|
||||
for (unsigned char c : digest) {
|
||||
out.push_back(hex[c >> 4]);
|
||||
out.push_back(hex[c & 0xf]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool IEquals(const std::string& a, const std::string& b) {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (size_t i = 0; i < a.size(); ++i) {
|
||||
if (std::tolower(static_cast<unsigned char>(a[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(b[i]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Prop(const std::string& key, const std::string& def) {
|
||||
return android::base::GetProperty(key, def);
|
||||
}
|
||||
|
||||
} // namespace pawlet::hwupdate::util
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace pawlet::hwupdate::util {
|
||||
|
||||
// Lowercase hex SHA-256 of a file's contents, or empty on error.
|
||||
std::string Sha256File(const std::string& path);
|
||||
|
||||
// Case-insensitive equality (for hex checksums / handler ids).
|
||||
bool IEquals(const std::string& a, const std::string& b);
|
||||
|
||||
// Read an Android system property with a default.
|
||||
std::string Prop(const std::string& key, const std::string& def = "");
|
||||
|
||||
} // namespace pawlet::hwupdate::util
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"device": "rpi4",
|
||||
"build_version": "1.0",
|
||||
"supported_hardware": [
|
||||
{
|
||||
"id": "pawlet-usb-dfu-reference",
|
||||
"manufacturer": "pawlet",
|
||||
"class": "peripheral",
|
||||
"support": "experimental",
|
||||
"supported_revisions": ["A", "B"],
|
||||
"identifiers": {
|
||||
"usb_vendor_id": "1209",
|
||||
"usb_product_id": "0001"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"id": "pawlet-usb-dfu-reference-mcu",
|
||||
"class": "microcontroller",
|
||||
"role": "peripheral-mcu",
|
||||
"support": "experimental",
|
||||
"supported_revisions": ["A", "B"],
|
||||
"installer_handlers": ["usbdfu"],
|
||||
"identifiers": {
|
||||
"usb_vendor_id": "1209",
|
||||
"usb_product_id": "0001"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2026 oxmc / PawletOS
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
cc_library_shared {
|
||||
name: "libpawlethwh_usbdfu",
|
||||
system_ext_specific: true,
|
||||
compile_multilib: "64",
|
||||
relative_install_path: "pawlet/hwh",
|
||||
srcs: ["usbdfu.cpp"],
|
||||
header_libs: ["pawlet_hwh_headers"],
|
||||
shared_libs: [
|
||||
"libbase",
|
||||
"liblog",
|
||||
"libusb",
|
||||
],
|
||||
cflags: [
|
||||
"-Wall",
|
||||
"-Werror",
|
||||
"-Wextra",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* USB DFU (Device Firmware Upgrade, class 0xFE / subclass 0x01) handler plugin.
|
||||
* Implements the plain DFU 1.1 download sequence over libusb. DfuSe/ST vendor
|
||||
* extensions are out of scope for this handler.
|
||||
*/
|
||||
#include <libusb/libusb.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
|
||||
#include "pawlet/hwh_abi.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// DFU class requests.
|
||||
constexpr uint8_t DFU_DNLOAD = 1;
|
||||
constexpr uint8_t DFU_GETSTATUS = 3;
|
||||
constexpr uint8_t DFU_CLRSTATUS = 4;
|
||||
constexpr uint8_t DFU_ABORT = 6;
|
||||
|
||||
// DFU states.
|
||||
constexpr uint8_t dfuIDLE = 2;
|
||||
constexpr uint8_t dfuDNLOAD_IDLE = 5;
|
||||
constexpr uint8_t dfuERROR = 10;
|
||||
|
||||
constexpr uint8_t OUT_IFACE = 0x21; // host->device | class | interface
|
||||
constexpr uint8_t IN_IFACE = 0xA1; // device->host | class | interface
|
||||
|
||||
constexpr uint16_t DEFAULT_TRANSFER_SIZE = 1024;
|
||||
constexpr unsigned CTRL_TIMEOUT_MS = 5000;
|
||||
|
||||
struct DfuStatus {
|
||||
uint8_t bStatus;
|
||||
uint32_t bwPollTimeout;
|
||||
uint8_t bState;
|
||||
uint8_t iString;
|
||||
};
|
||||
|
||||
uint16_t ParseHex16(const char* s) {
|
||||
if (!s || !*s) return 0;
|
||||
return static_cast<uint16_t>(strtoul(s, nullptr, 16));
|
||||
}
|
||||
|
||||
bool ReadFile(const char* path, std::vector<uint8_t>* out) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (len < 0) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
out->resize(static_cast<size_t>(len));
|
||||
size_t n = fread(out->data(), 1, out->size(), f);
|
||||
fclose(f);
|
||||
return n == out->size();
|
||||
}
|
||||
|
||||
libusb_device_handle* OpenByCtx(libusb_context* ctx, const pawlet_hw_ctx* c) {
|
||||
uint16_t vid = ParseHex16(c->usb_vendor_id);
|
||||
uint16_t pid = ParseHex16(c->usb_product_id);
|
||||
if (!vid || !pid) return nullptr;
|
||||
return libusb_open_device_with_vid_pid(ctx, vid, pid);
|
||||
}
|
||||
|
||||
int GetStatus(libusb_device_handle* h, int iface, DfuStatus* st) {
|
||||
uint8_t buf[6] = {0};
|
||||
int r = libusb_control_transfer(h, IN_IFACE, DFU_GETSTATUS, 0, iface, buf, sizeof(buf),
|
||||
CTRL_TIMEOUT_MS);
|
||||
if (r != 6) return PAWLET_HWH_IO;
|
||||
st->bStatus = buf[0];
|
||||
st->bwPollTimeout = buf[1] | (buf[2] << 8) | (buf[3] << 16);
|
||||
st->bState = buf[4];
|
||||
st->iString = buf[5];
|
||||
return PAWLET_HWH_OK;
|
||||
}
|
||||
|
||||
// Locate the DFU interface number and its wTransferSize from the config.
|
||||
bool FindDfuInterface(libusb_device* dev, int* ifaceOut, uint16_t* transferSize) {
|
||||
libusb_config_descriptor* cfg = nullptr;
|
||||
if (libusb_get_active_config_descriptor(dev, &cfg) != 0 || !cfg) return false;
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < cfg->bNumInterfaces && !found; ++i) {
|
||||
const libusb_interface& intf = cfg->interface[i];
|
||||
for (int a = 0; a < intf.num_altsetting; ++a) {
|
||||
const libusb_interface_descriptor& d = intf.altsetting[a];
|
||||
if (d.bInterfaceClass == 0xFE && d.bInterfaceSubClass == 0x01) {
|
||||
*ifaceOut = d.bInterfaceNumber;
|
||||
*transferSize = DEFAULT_TRANSFER_SIZE;
|
||||
// DFU functional descriptor (bDescriptorType 0x21) carries
|
||||
// wTransferSize at offset 5..6.
|
||||
const uint8_t* extra = d.extra;
|
||||
int len = d.extra_length;
|
||||
while (len >= 2) {
|
||||
uint8_t blen = extra[0];
|
||||
uint8_t btype = extra[1];
|
||||
if (blen < 2 || blen > len) break;
|
||||
if (btype == 0x21 && blen >= 7) {
|
||||
uint16_t ts = extra[5] | (extra[6] << 8);
|
||||
if (ts > 0) *transferSize = ts;
|
||||
}
|
||||
extra += blen;
|
||||
len -= blen;
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
libusb_free_config_descriptor(cfg);
|
||||
return found;
|
||||
}
|
||||
|
||||
// ---- ABI entry points ----
|
||||
|
||||
int Detect(const pawlet_hw_ctx* ctx) {
|
||||
libusb_context* c = nullptr;
|
||||
if (libusb_init(&c) != 0) return PAWLET_HWH_ERR;
|
||||
libusb_device_handle* h = OpenByCtx(c, ctx);
|
||||
int rc = h ? PAWLET_HWH_OK : PAWLET_HWH_NOT_PRESENT;
|
||||
if (h) libusb_close(h);
|
||||
libusb_exit(c);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int GetVersion(const pawlet_hw_ctx* ctx, char* out, size_t out_size) {
|
||||
if (!out || out_size == 0) return PAWLET_HWH_ERR;
|
||||
libusb_context* c = nullptr;
|
||||
if (libusb_init(&c) != 0) return PAWLET_HWH_ERR;
|
||||
libusb_device_handle* h = OpenByCtx(c, ctx);
|
||||
if (!h) {
|
||||
libusb_exit(c);
|
||||
return PAWLET_HWH_NOT_PRESENT;
|
||||
}
|
||||
libusb_device_descriptor desc{};
|
||||
int rc = PAWLET_HWH_UNSUPPORTED;
|
||||
if (libusb_get_device_descriptor(libusb_get_device(h), &desc) == 0) {
|
||||
// Report bcdDevice as the installed firmware version (common for DFU MCUs).
|
||||
snprintf(out, out_size, "%u.%u", desc.bcdDevice >> 8, desc.bcdDevice & 0xff);
|
||||
rc = PAWLET_HWH_OK;
|
||||
}
|
||||
libusb_close(h);
|
||||
libusb_exit(c);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int Flash(const pawlet_hw_ctx* ctx, const char* path, pawlet_progress_cb progress, void* user) {
|
||||
std::vector<uint8_t> fw;
|
||||
if (!ReadFile(path, &fw) || fw.empty()) return PAWLET_HWH_IO;
|
||||
|
||||
libusb_context* c = nullptr;
|
||||
if (libusb_init(&c) != 0) return PAWLET_HWH_ERR;
|
||||
libusb_device_handle* h = OpenByCtx(c, ctx);
|
||||
if (!h) {
|
||||
libusb_exit(c);
|
||||
return PAWLET_HWH_NOT_PRESENT;
|
||||
}
|
||||
|
||||
int iface = 0;
|
||||
uint16_t transfer = DEFAULT_TRANSFER_SIZE;
|
||||
if (!FindDfuInterface(libusb_get_device(h), &iface, &transfer)) {
|
||||
LOG(ERROR) << "usbdfu: no DFU interface";
|
||||
libusb_close(h);
|
||||
libusb_exit(c);
|
||||
return PAWLET_HWH_UNSUPPORTED;
|
||||
}
|
||||
|
||||
if (libusb_kernel_driver_active(h, iface) == 1) libusb_detach_kernel_driver(h, iface);
|
||||
if (libusb_claim_interface(h, iface) != 0) {
|
||||
LOG(ERROR) << "usbdfu: claim_interface failed";
|
||||
libusb_close(h);
|
||||
libusb_exit(c);
|
||||
return PAWLET_HWH_IO;
|
||||
}
|
||||
|
||||
int result = PAWLET_HWH_OK;
|
||||
DfuStatus st{};
|
||||
|
||||
// Ensure a clean idle state.
|
||||
if (GetStatus(h, iface, &st) == PAWLET_HWH_OK && st.bState == dfuERROR) {
|
||||
libusb_control_transfer(h, OUT_IFACE, DFU_CLRSTATUS, 0, iface, nullptr, 0, CTRL_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
size_t sent = 0;
|
||||
uint16_t block = 0;
|
||||
while (sent < fw.size()) {
|
||||
uint16_t chunk = static_cast<uint16_t>(std::min<size_t>(transfer, fw.size() - sent));
|
||||
int r = libusb_control_transfer(h, OUT_IFACE, DFU_DNLOAD, block, iface, fw.data() + sent,
|
||||
chunk, CTRL_TIMEOUT_MS);
|
||||
if (r < 0 || static_cast<uint16_t>(r) != chunk) {
|
||||
LOG(ERROR) << "usbdfu: DNLOAD block " << block << " failed: " << r;
|
||||
result = PAWLET_HWH_IO;
|
||||
break;
|
||||
}
|
||||
// Wait for the device to program the block.
|
||||
if (GetStatus(h, iface, &st) != PAWLET_HWH_OK) {
|
||||
result = PAWLET_HWH_IO;
|
||||
break;
|
||||
}
|
||||
if (st.bStatus != 0) {
|
||||
LOG(ERROR) << "usbdfu: device error status=" << (int)st.bStatus;
|
||||
result = PAWLET_HWH_IO;
|
||||
break;
|
||||
}
|
||||
if (st.bwPollTimeout) usleep(st.bwPollTimeout * 1000);
|
||||
|
||||
sent += chunk;
|
||||
++block;
|
||||
if (progress) progress(static_cast<int>(sent * 100 / fw.size()), user);
|
||||
}
|
||||
|
||||
if (result == PAWLET_HWH_OK) {
|
||||
// Zero-length DNLOAD signals end-of-transfer; then manifest.
|
||||
libusb_control_transfer(h, OUT_IFACE, DFU_DNLOAD, block, iface, nullptr, 0, CTRL_TIMEOUT_MS);
|
||||
GetStatus(h, iface, &st);
|
||||
if (st.bwPollTimeout) usleep(st.bwPollTimeout * 1000);
|
||||
// Poll to idle / manifest complete.
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (GetStatus(h, iface, &st) != PAWLET_HWH_OK) break;
|
||||
if (st.bState == dfuIDLE || st.bState == dfuDNLOAD_IDLE) break;
|
||||
if (st.bState == dfuERROR) {
|
||||
result = PAWLET_HWH_VERIFY;
|
||||
break;
|
||||
}
|
||||
usleep((st.bwPollTimeout ? st.bwPollTimeout : 100) * 1000);
|
||||
}
|
||||
} else {
|
||||
libusb_control_transfer(h, OUT_IFACE, DFU_ABORT, 0, iface, nullptr, 0, CTRL_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
libusb_release_interface(h, iface);
|
||||
libusb_close(h);
|
||||
libusb_exit(c);
|
||||
return result;
|
||||
}
|
||||
|
||||
const pawlet_hwh_api kApi = {
|
||||
/* abi_version */ PAWLET_HWH_ABI,
|
||||
/* handler_id */ "usbdfu",
|
||||
/* detect */ Detect,
|
||||
/* get_version */ GetVersion,
|
||||
/* flash */ Flash,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" const pawlet_hwh_api* pawlet_hwh_entry(void) { return &kApi; }
|
||||
@@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright (C) 2026 oxmc / PawletOS
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# PawletOS hardware update subsystem. Include from a product makefile:
|
||||
# $(call inherit-product, vendor/pawlet/hwupdate/hwupdate.mk)
|
||||
#
|
||||
|
||||
PRODUCT_PACKAGES += \
|
||||
pawlethwd \
|
||||
libpawlethwh_usbdfu \
|
||||
pawlet_supported_hardware_default
|
||||
|
||||
# Path to the image-local supported-hardware manifest (daemon default matches
|
||||
# this; set explicitly so it is discoverable/overridable).
|
||||
PRODUCT_PRODUCT_PROPERTIES += \
|
||||
ro.pawlet.hardware.manifest=/system_ext/etc/pawlet/supported_hardware.json
|
||||
|
||||
# Daemon SELinux policy (system_ext side).
|
||||
SYSTEM_EXT_PRIVATE_SEPOLICY_DIRS += vendor/pawlet/hwupdate/sepolicy
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2026 oxmc / PawletOS
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Stable C ABI for PawletOS hardware-update handler plugins.
|
||||
*
|
||||
* A handler is a shared library installed at
|
||||
* /system_ext/lib{,64}/pawlet/hwh/libpawlethwh_<id>.so
|
||||
* that exports exactly one symbol:
|
||||
*
|
||||
* const pawlet_hwh_api* pawlet_hwh_entry(void);
|
||||
*
|
||||
* The daemon dlopen()s the library only when <id> appears in the component's
|
||||
* installer_handlers allowlist in the immutable supported_hardware.json, and
|
||||
* refuses a vtable whose abi_version or handler_id do not match. Keep this ABI
|
||||
* backwards compatible; bump PAWLET_HWH_ABI only on a breaking change.
|
||||
*/
|
||||
#ifndef PAWLET_HWH_ABI_H
|
||||
#define PAWLET_HWH_ABI_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PAWLET_HWH_ABI 1u
|
||||
|
||||
/* Return codes for handler entry points. */
|
||||
enum {
|
||||
PAWLET_HWH_OK = 0,
|
||||
PAWLET_HWH_ERR = -1, /* generic failure */
|
||||
PAWLET_HWH_NOT_PRESENT = -2, /* detect(): device not found */
|
||||
PAWLET_HWH_UNSUPPORTED = -3, /* operation not supported for this device */
|
||||
PAWLET_HWH_IO = -4, /* transport I/O error */
|
||||
PAWLET_HWH_VERIFY = -5 /* post-flash verification failed */
|
||||
};
|
||||
|
||||
/*
|
||||
* Context describing the target component for a handler call. Fields not
|
||||
* relevant to a given transport may be empty. Owned by the daemon; valid only
|
||||
* for the duration of the call.
|
||||
*/
|
||||
typedef struct {
|
||||
const char* component_id; /* stable component id from the manifest */
|
||||
const char* transport; /* "usb" | "i2c" | "spi" | "uart" | "gpio" | "can" */
|
||||
const char* usb_vendor_id; /* hex string, e.g. "1209" (may be empty) */
|
||||
const char* usb_product_id; /* hex string, e.g. "0001" (may be empty) */
|
||||
const char* dev_path; /* transport device node, e.g. /dev/i2c-1 (may be empty) */
|
||||
const char* hardware_rev; /* detected revision (may be empty) */
|
||||
} pawlet_hw_ctx;
|
||||
|
||||
/* Progress callback: percent is 0..100. user is passed through from flash(). */
|
||||
typedef void (*pawlet_progress_cb)(int percent, void* user);
|
||||
|
||||
typedef struct {
|
||||
uint32_t abi_version; /* must equal PAWLET_HWH_ABI */
|
||||
const char* handler_id; /* must match the <id> in the .so filename / allowlist */
|
||||
|
||||
/* Probe whether the target device is present/reachable on its transport.
|
||||
* Returns PAWLET_HWH_OK if present, PAWLET_HWH_NOT_PRESENT otherwise. */
|
||||
int (*detect)(const pawlet_hw_ctx* ctx);
|
||||
|
||||
/* Read the currently installed firmware version into out (NUL-terminated,
|
||||
* up to out_size). Returns PAWLET_HWH_OK on success. May return
|
||||
* PAWLET_HWH_UNSUPPORTED if the device cannot report its version. */
|
||||
int (*get_version)(const pawlet_hw_ctx* ctx, char* out, size_t out_size);
|
||||
|
||||
/* Flash the verified firmware file at path to the device. progress may be
|
||||
* NULL. Returns PAWLET_HWH_OK on success. */
|
||||
int (*flash)(const pawlet_hw_ctx* ctx, const char* path,
|
||||
pawlet_progress_cb progress, void* user);
|
||||
} pawlet_hwh_api;
|
||||
|
||||
/* The single exported symbol every handler .so must provide. */
|
||||
const pawlet_hwh_api* pawlet_hwh_entry(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PAWLET_HWH_ABI_H */
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://pawletos.oxmc.me/schemas/device-updates.schema.json",
|
||||
"title": "PawletOS hardware update catalog",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "manufacturers"],
|
||||
"properties": {
|
||||
"schema_version": { "type": "integer", "const": 1 },
|
||||
"manufacturers": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/definitions/manufacturer" }
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"manufacturer": {
|
||||
"type": "object",
|
||||
"required": ["name", "classes"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"classes": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": ["items"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/definitions/product" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"product": {
|
||||
"type": "object",
|
||||
"required": ["name", "class", "components"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"model": { "type": "string" },
|
||||
"class": { "type": "string" },
|
||||
"connections": { "type": "object", "additionalProperties": { "type": "string" } },
|
||||
"identifiers": { "type": "object" },
|
||||
"components": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/definitions/component" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"required": ["id", "class", "updates"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"class": { "type": "string" },
|
||||
"role": { "type": "string" },
|
||||
"identifiers": { "type": "object" },
|
||||
"updates": { "type": "array", "items": { "$ref": "#/definitions/update" } }
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"type": "object",
|
||||
"required": ["version", "datetime", "filename", "id", "size_bytes", "url", "checksum_sha256", "installer"],
|
||||
"properties": {
|
||||
"version": { "type": "string" },
|
||||
"datetime": { "type": "integer" },
|
||||
"filename": { "type": "string" },
|
||||
"id": { "type": "string" },
|
||||
"size_bytes": { "type": "integer", "minimum": 0 },
|
||||
"url": { "type": "string", "format": "uri" },
|
||||
"checksum_sha256": { "type": "string", "pattern": "^[0-9a-fA-F]{64}$" },
|
||||
"changelog_url": { "type": "string" },
|
||||
"compatibility": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hardware_revisions": { "type": "array", "items": { "type": "string" } },
|
||||
"pawlet_devices": { "type": "array", "items": { "type": "string" } },
|
||||
"minimum_pawlet_version": { "type": "string" },
|
||||
"maximum_pawlet_version": { "type": "string" },
|
||||
"minimum_bootloader_version": { "type": "string" },
|
||||
"maximum_bootloader_version": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"installer": {
|
||||
"type": "object",
|
||||
"required": ["type", "handler"],
|
||||
"properties": {
|
||||
"type": { "enum": ["usb", "i2c", "spi", "uart", "gpio", "can"] },
|
||||
"handler": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://pawletos.oxmc.me/schemas/supported-hardware.schema.json",
|
||||
"title": "PawletOS image-local supported hardware manifest",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "device", "build_version", "supported_hardware"],
|
||||
"properties": {
|
||||
"schema_version": { "type": "integer", "const": 1 },
|
||||
"device": { "type": "string", "minLength": 1 },
|
||||
"build_version": { "type": "string", "minLength": 1 },
|
||||
"supported_hardware": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/product" }
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"support": { "enum": ["full", "experimental", "limited", "deprecated"] },
|
||||
"product": {
|
||||
"type": "object",
|
||||
"required": ["id", "manufacturer", "class", "support", "components"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"manufacturer": { "type": "string", "minLength": 1 },
|
||||
"class": { "type": "string", "minLength": 1 },
|
||||
"support": { "$ref": "#/definitions/support" },
|
||||
"supported_revisions": { "type": "array", "items": { "type": "string" } },
|
||||
"identifiers": { "$ref": "#/definitions/identifiers" },
|
||||
"components": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/component" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"required": ["id", "class", "support", "installer_handlers"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"class": { "type": "string", "minLength": 1 },
|
||||
"role": { "type": "string" },
|
||||
"support": { "$ref": "#/definitions/support" },
|
||||
"supported_revisions": { "type": "array", "items": { "type": "string" } },
|
||||
"installer_handlers": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"identifiers": { "$ref": "#/definitions/identifiers" }
|
||||
}
|
||||
},
|
||||
"identifiers": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"device_tree_compatible": { "type": "array", "items": { "type": "string" } },
|
||||
"usb_vendor_id": { "type": "string" },
|
||||
"usb_product_id": { "type": "string" },
|
||||
"hardware_revisions": { "type": "array", "items": { "type": "string" } }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (C) 2026 oxmc / PawletOS
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/system_ext/bin/pawlethwd u:object_r:pawlethwd_exec:s0
|
||||
/data/pawlet(/.*)? u:object_r:pawlet_hw_data_file:s0
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright (C) 2026 oxmc / PawletOS
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# PawletOS hardware update daemon: detects peripherals, downloads firmware, and
|
||||
# flashes them via authorized handler plugins. Runtime SELinux is permissive on
|
||||
# PawletOS, but this policy is written to pass build-time neverallow checks.
|
||||
|
||||
type pawlethwd, domain, coredomain;
|
||||
type pawlethwd_exec, exec_type, system_file_type, file_type;
|
||||
init_daemon_domain(pawlethwd)
|
||||
|
||||
# Binder service.
|
||||
type pawlet_hwupdate_service, service_manager_type;
|
||||
binder_use(pawlethwd)
|
||||
add_service(pawlethwd, pawlet_hwupdate_service)
|
||||
allow pawlethwd servicemanager:service_manager list;
|
||||
|
||||
# Persistent state under /data/pawlet (created by init; see init.pawlethwd.rc).
|
||||
# init labels /data/pawlet via file_contexts; files the daemon creates under it
|
||||
# inherit pawlet_hw_data_file by default.
|
||||
type pawlet_hw_data_file, file_type, data_file_type, core_data_file_type;
|
||||
allow pawlethwd pawlet_hw_data_file:dir create_dir_perms;
|
||||
allow pawlethwd pawlet_hw_data_file:file create_file_perms;
|
||||
|
||||
# Network access to download firmware from the update server.
|
||||
net_domain(pawlethwd)
|
||||
|
||||
# Read the image-local manifest and dlopen handler plugins from /system_ext.
|
||||
allow pawlethwd system_file:dir r_dir_perms;
|
||||
allow pawlethwd system_file:file { r_file_perms execute map };
|
||||
|
||||
# USB enumeration (sysfs) + device access for detection and flashing.
|
||||
allow pawlethwd sysfs:dir r_dir_perms;
|
||||
allow pawlethwd sysfs:file r_file_perms;
|
||||
allow pawlethwd usb_device:dir r_dir_perms;
|
||||
allow pawlethwd usb_device:chr_file rw_file_perms;
|
||||
|
||||
# Read system properties.
|
||||
get_prop(pawlethwd, default_prop)
|
||||
|
||||
# Clients that may look up the service (daemon-only surface for now; system
|
||||
# apps/Settings can be added later).
|
||||
allow system_server pawlet_hwupdate_service:service_manager find;
|
||||
userdebug_or_eng(`
|
||||
allow shell pawlet_hwupdate_service:service_manager find;
|
||||
')
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2026 oxmc / PawletOS
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pawlet_hwupdate u:object_r:pawlet_hwupdate_service:s0
|
||||
Reference in New Issue
Block a user