@@ -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);
0 commit comments