Skip to content

Commit c77518b

Browse files
Tom Deweytdewey-rpi
authored andcommitted
Enhance device data structure and UI for USB topology
- Added new fields to the device data structure, including productName and model, to improve device identification. - Implemented logic to read manufacturing data from a database for enhanced device type inference. - Updated the inferModelGeneration function to prioritize model information from the product name and manufacturing data. - Introduced a relational view in the UI with tabs for better navigation and visualization of USB topology. - Added CSS styles for the new relational view and improved overall UI layout for device representation.
1 parent ca92cc6 commit c77518b

File tree

3 files changed

+342
-28
lines changed

3 files changed

+342
-28
lines changed

provisioner-service/src/devices.cpp

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,14 @@ namespace provisioner {
9393
std::string parentId; // parent path id or empty for root
9494
std::string vendor; // idVendor
9595
std::string product; // idProduct
96+
std::string productName; // textual product name (e.g., BCM2712 Boot)
9697
std::string serial; // serial (if available)
9798
bool isHub{false};
9899
// Optional provisioning info
99100
std::string state;
100101
std::string image;
101102
std::string ip;
103+
std::string model; // device model/type (e.g., CM5, 4B, Zero 2 W)
102104
int portCount{0}; // number of ports if hub (from maxchild)
103105
bool isPlaceholder{false};
104106
};
@@ -179,6 +181,10 @@ namespace provisioner {
179181
const std::string p = readFileTrimmed(entry.path()/"idProduct");
180182
if (!v.empty()) node.vendor = v;
181183
if (!p.empty()) node.product = p;
184+
const std::string pn = readFileTrimmed(entry.path()/"product");
185+
if (!pn.empty()) node.productName = pn;
186+
// Default model hint from product name; refined later by manufacturing data
187+
if (node.model.empty() && !pn.empty()) node.model = pn;
182188
const std::string s = readFileTrimmed(entry.path()/"serial");
183189
if (!s.empty()) node.serial = s;
184190
}
@@ -269,6 +275,9 @@ namespace provisioner {
269275
// Build latest record per endpoint (descending ts ensures first seen is newest)
270276
struct DbRecord { std::string serial, state, image, ip; };
271277
std::unordered_map<std::string, DbRecord> latestByEndpoint;
278+
// Also capture latest manufacturing info by serial (boardname/processor)
279+
struct MfgRecord { std::string boardname, processor; };
280+
std::unordered_map<std::string, MfgRecord> latestMfgBySerial;
272281

273282
sqlite3* db;
274283
int rc = sqlite3_open("/srv/rpi-sb-provisioner/state.db", &db);
@@ -304,6 +313,34 @@ namespace provisioner {
304313
sqlite3_finalize(stmt);
305314
sqlite3_close(db);
306315

316+
// Read manufacturing database if present for device-type inference
317+
{
318+
sqlite3* mdb = nullptr;
319+
int rc2 = sqlite3_open("/srv/rpi-sb-provisioner/manufacturing.db", &mdb);
320+
if (rc2 == SQLITE_OK) {
321+
const char* msql = "SELECT serial, boardname, processor FROM devices ORDER BY provision_ts DESC";
322+
sqlite3_stmt* mstmt = nullptr;
323+
rc2 = sqlite3_prepare_v2(mdb, msql, -1, &mstmt, nullptr);
324+
if (rc2 == SQLITE_OK) {
325+
while (sqlite3_step(mstmt) == SQLITE_ROW) {
326+
const unsigned char* serial = sqlite3_column_text(mstmt, 0);
327+
const unsigned char* boardname = sqlite3_column_text(mstmt, 1);
328+
const unsigned char* processor = sqlite3_column_text(mstmt, 2);
329+
if (!serial) continue;
330+
std::string s = reinterpret_cast<const char*>(serial);
331+
if (latestMfgBySerial.find(s) == latestMfgBySerial.end()) {
332+
latestMfgBySerial.emplace(s, MfgRecord{
333+
boardname ? reinterpret_cast<const char*>(boardname) : std::string{},
334+
processor ? reinterpret_cast<const char*>(processor) : std::string{}
335+
});
336+
}
337+
}
338+
}
339+
if (mstmt) sqlite3_finalize(mstmt);
340+
sqlite3_close(mdb);
341+
}
342+
}
343+
307344
// Apply only when: (1) port is connected (non-placeholder), (2) we have a latest record for this endpoint
308345
for (const auto &p : latestByEndpoint) {
309346
const std::string &endpoint = p.first;
@@ -314,17 +351,66 @@ namespace provisioner {
314351
if (n.isPlaceholder) continue; // not connected
315352
if (n.isHub) continue; // never apply provisioning state to hubs
316353
n.state = rec.state;
317-
n.image = rec.image;
354+
// Do not clobber existing non-empty image (used for model inference) with empty DB values
355+
if (!rec.image.empty()) {
356+
n.image = rec.image;
357+
}
318358
n.ip = rec.ip;
359+
// If we also have a manufacturing record for this device by serial, use it to annotate image/model
360+
if (!n.serial.empty()) {
361+
auto mit = latestMfgBySerial.find(n.serial);
362+
if (mit != latestMfgBySerial.end()) {
363+
const auto &mr = mit->second;
364+
// Prefer model from manufacturing boardname (e.g., CM5, 4B)
365+
if (!mr.boardname.empty()) n.model = mr.boardname;
366+
else if (!mr.processor.empty() && n.model.empty()) n.model = mr.processor;
367+
// Keep image for OS image name only; if image was being used as model before, do not overwrite unless set
368+
if (n.image.empty() && !mr.boardname.empty()) n.image = n.image; // no-op placeholder to emphasize separation
369+
}
370+
}
319371
}
320372
}
321373

322374
int inferModelGeneration(const UsbNode &n) {
323-
if (n.image.empty()) return 0;
324-
std::string img = n.image;
325-
std::transform(img.begin(), img.end(), img.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
326-
if (img.find("2712") != std::string::npos || img.find("rpi5") != std::string::npos || img.find("pi5") != std::string::npos) return 5;
327-
if (img.find("2711") != std::string::npos || img.find("rpi4") != std::string::npos || img.find("pi4") != std::string::npos) return 4;
375+
// Prefer explicit image tag if available
376+
if (!n.model.empty()) {
377+
std::string img = n.model;
378+
std::transform(img.begin(), img.end(), img.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
379+
if (img.find("2712") != std::string::npos ||
380+
img.find("rpi5") != std::string::npos ||
381+
img.find("pi5") != std::string::npos ||
382+
img.find("cm5") != std::string::npos ||
383+
img.find("compute module 5") != std::string::npos)
384+
{
385+
return 5;
386+
}
387+
if (img.find("2711") != std::string::npos ||
388+
img.find("rpi4") != std::string::npos ||
389+
img.find("pi4") != std::string::npos ||
390+
img.find("cm4") != std::string::npos ||
391+
img.find("compute module 4") != std::string::npos ||
392+
img.find("pi 400") != std::string::npos)
393+
{
394+
return 4;
395+
}
396+
}
397+
if (!n.image.empty()) {
398+
std::string img = n.image;
399+
std::transform(img.begin(), img.end(), img.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
400+
if (img.find("2712") != std::string::npos || img.find("rpi5") != std::string::npos || img.find("pi5") != std::string::npos) return 5;
401+
if (img.find("2711") != std::string::npos || img.find("rpi4") != std::string::npos || img.find("pi4") != std::string::npos) return 4;
402+
}
403+
// Fall back to USB product name (e.g., "BCM2712 Boot") seen at connect time
404+
if (!n.productName.empty()) {
405+
std::string pn = n.productName;
406+
std::transform(pn.begin(), pn.end(), pn.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
407+
if (pn.find("2712") != std::string::npos) return 5;
408+
if (pn.find("2711") != std::string::npos) return 4;
409+
}
410+
// Fall back to endpoint-derived hints: if image was populated from manufacturing processor earlier
411+
if (n.image.empty() && !n.product.empty()) {
412+
// In some flows we set image to BCM2711/BCM2712 in enrichment; if not present at call time, skip.
413+
}
328414
return 0;
329415
}
330416

@@ -348,9 +434,11 @@ namespace provisioner {
348434
j["isHub"] = n.isHub;
349435
if (!n.vendor.empty()) j["vendor"] = n.vendor;
350436
if (!n.product.empty()) j["product"] = n.product;
437+
if (!n.productName.empty()) j["productName"] = n.productName;
351438
if (!n.serial.empty()) j["serial"] = n.serial;
352439
if (!n.state.empty()) j["state"] = n.state;
353440
if (!n.image.empty()) j["image"] = n.image;
441+
if (!n.model.empty()) j["model"] = n.model;
354442
if (!n.ip.empty()) j["ip"] = n.ip;
355443
if (n.isPlaceholder) j["placeholder"] = true;
356444
int gen = inferModelGeneration(n);

provisioner-service/src/images.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ class SHA256WebSocketController : public drogon::WebSocketController<SHA256WebSo
112112
if (request.isMember("action") && request["action"].asString() == "get_sha256" &&
113113
request.isMember("image_name")) {
114114
std::string imageName = request["image_name"].asString();
115+
// Refuse sidecar requests up-front
116+
try {
117+
if (std::filesystem::path(imageName).extension() == ".sha256") {
118+
Json::Value response;
119+
response["image_name"] = imageName;
120+
response["status"] = "error";
121+
response["error"] = "Refused: .sha256 sidecar files are not hashable";
122+
wsConnPtr->send(response.toStyledString());
123+
return;
124+
}
125+
} catch (...) {}
115126

116127
// Register this connection as interested in this image
117128
{
@@ -569,6 +580,22 @@ namespace provisioner {
569580

570581
// Request a SHA256 calculation
571582
void requestSHA256Calculation(const std::string& imageName) {
583+
// Reject hashing for sidecar checksum files and notify listeners if any
584+
try {
585+
if (std::filesystem::path(imageName).extension() == ".sha256") {
586+
std::lock_guard<std::mutex> lock(SHA256WebSocketController::connectionsMutex);
587+
auto it = SHA256WebSocketController::activeConnections.find(imageName);
588+
if (it != SHA256WebSocketController::activeConnections.end()) {
589+
Json::Value response;
590+
response["image_name"] = imageName;
591+
response["status"] = "error";
592+
response["error"] = "Refused: .sha256 sidecar files are not hashable";
593+
const std::string msg = response.toStyledString();
594+
for (auto &conn : it->second) { if (conn && conn->connected()) conn->send(msg); }
595+
}
596+
return;
597+
}
598+
} catch (...) { /* ignore */ }
572599
bool needsCalculation = false;
573600

574601
// Get file size to calculate appropriate timeout
@@ -708,6 +735,10 @@ namespace provisioner {
708735
try {
709736
for (const auto& entry : std::filesystem::directory_iterator(IMAGES_PATH)) {
710737
if (entry.is_regular_file()) {
738+
// Skip .sha256 sidecar files
739+
if (entry.path().extension() == ".sha256") {
740+
continue;
741+
}
711742
std::string imageName = entry.path().filename().string();
712743
LOG_INFO << "Queuing SHA256 calculation for existing image: " << imageName;
713744
requestSHA256Calculation(imageName);
@@ -782,6 +813,11 @@ namespace provisioner {
782813
filePath /= filename;
783814

784815
if (std::filesystem::exists(filePath) && std::filesystem::is_regular_file(filePath)) {
816+
// Skip sidecar checksum files
817+
if (filePath.extension() == ".sha256") {
818+
i += EVENT_SIZE + event->len;
819+
continue;
820+
}
785821
LOG_INFO << "Queuing SHA256 calculation for new image: " << filename;
786822
requestSHA256Calculation(filename);
787823
}
@@ -969,6 +1005,10 @@ namespace provisioner {
9691005
LOG_INFO << "Found entry: " << entry.path().string();
9701006
if (entry.is_regular_file()) {
9711007
std::filesystem::path imagePath = entry.path();
1008+
// Skip SHA256 sidecar files from the listing
1009+
if (imagePath.extension() == ".sha256") {
1010+
continue;
1011+
}
9721012
ImageInfo info;
9731013
info.name = imagePath.filename().string();
9741014

0 commit comments

Comments
 (0)