@@ -1505,6 +1505,123 @@ def verify(self, db: TigerTagDB = None) -> SignatureResult:
15051505
15061506 # ── Output ────────────────────────────────────────────────────────────────
15071507
1508+ def describe (self , db : TigerTagDB = None ) -> str :
1509+ """
1510+ Return a concise natural-language description of the tag.
1511+
1512+ Designed for injection into LLM prompts. Contains all material data
1513+ in plain English, without protocol jargon.
1514+
1515+ Args:
1516+ db : Optional pre-loaded TigerTagDB. Uses self.db by default.
1517+
1518+ Returns:
1519+ A single paragraph (no newlines) describing the tag content.
1520+
1521+ Example:
1522+ prompt = f"Given this material: {tag.describe()}\\ nWhat nozzle temp should I use?"
1523+ """
1524+ _db = db or self .db
1525+ mat = _db .material (self .id_material ) or {}
1526+ rec = mat .get ("recommended" , {})
1527+
1528+ material = TigerTagDB .label (_db .material (self .id_material ))
1529+ type_ = TigerTagDB .label (_db .type_ (self .id_type ))
1530+ diameter = TigerTagDB .label (_db .diameter (self .id_diameter ))
1531+ brand = TigerTagDB .label (_db .brand (self .id_brand ))
1532+ unit = TigerTagDB .label (_db .unit (self .id_unit ))
1533+ density = mat .get ("density" )
1534+ stock = self .stock_percent
1535+
1536+ # Check aspect_2 first for multi-color mode (Bicolor/Tricolor/Rainbow),
1537+ # then fall back to aspect_1. color_count drives how many colors are active.
1538+ asp2_entry = _db .aspect (self .id_aspect_2 )
1539+ asp1_entry = _db .aspect (self .id_aspect_1 )
1540+ color_count = 1
1541+ if asp2_entry and asp2_entry .get ("color_count" , 1 ) > 1 :
1542+ color_count = asp2_entry ["color_count" ]
1543+ elif asp1_entry and asp1_entry .get ("color_count" , 1 ) > 1 :
1544+ color_count = asp1_entry ["color_count" ]
1545+
1546+ aspect1_label = TigerTagDB .label (asp1_entry )
1547+ aspect2_label = TigerTagDB .label (asp2_entry )
1548+
1549+ parts : List [str ] = []
1550+
1551+ # Identity
1552+ parts .append (
1553+ f"TigerTag RFID chip read successfully."
1554+ f" Material: { material } { type_ } "
1555+ + (f" ({ diameter } mm diameter)" if diameter not in ("Unknown" , "-" ) else "" )
1556+ + (f", density { density } g/cm³" if density else "" )
1557+ + (f", by { brand } " if brand not in ("Unknown" , "-" ) else "" )
1558+ + "."
1559+ )
1560+
1561+ # Aspect / finish
1562+ finish_parts = [l for l in (aspect1_label , aspect2_label ) if l not in ("Unknown" , "-" , "None" )]
1563+ if finish_parts :
1564+ parts .append (f"Finish: { ' + ' .join (finish_parts )} ." )
1565+
1566+ # Color — respect color_count from aspect DB
1567+ color_parts = [f"primary { self .color1_hex } " ]
1568+ if color_count >= 2 :
1569+ color_parts .append (f"secondary { self .color2_hex } " )
1570+ if color_count >= 3 :
1571+ color_parts .append (f"tertiary { self .color3_hex } " )
1572+ parts .append ("Color: " + ", " .join (color_parts ) + "." )
1573+
1574+ # Temperatures from chip
1575+ parts .append (
1576+ f"Print settings (on chip): nozzle { self .nozzle_temp_min } –{ self .nozzle_temp_max } °C,"
1577+ f" bed { self .bed_temp_min } –{ self .bed_temp_max } °C,"
1578+ f" drying { self .dry_temp } °C for { self .dry_time } h."
1579+ )
1580+
1581+ # DB-recommended temps (if available)
1582+ if rec .get ("nozzleTempMin" ) is not None :
1583+ parts .append (
1584+ f"Database recommended settings: nozzle { rec ['nozzleTempMin' ]} –{ rec ['nozzleTempMax' ]} °C,"
1585+ f" bed { rec .get ('bedTempMin' , '?' )} –{ rec .get ('bedTempMax' , '?' )} °C,"
1586+ f" drying { rec .get ('dryTemp' , '?' )} °C for { rec .get ('dryTime' , '?' )} h."
1587+ )
1588+
1589+ # Quantity
1590+ if self .measure > 0 :
1591+ parts .append (
1592+ f"Quantity: { self .measure_available } { unit } remaining"
1593+ f" out of { self .measure } { unit } initial"
1594+ + (f" ({ stock } %)." if stock is not None else "." )
1595+ )
1596+
1597+ # HueForge
1598+ if self .td_raw != 0 :
1599+ parts .append (f"HueForge TD: { self .td_value :.1f} ." )
1600+
1601+ # Traceability
1602+ parts .append (f"Manufactured: { self .manufacturing_date .strftime ('%Y-%m-%d' )} ." )
1603+ if self .custom_message :
1604+ parts .append (f"Custom message on chip: \" { self .custom_message } \" ." )
1605+ if self .uid_hex :
1606+ parts .append (f"Chip UID: { self .uid_hex } ." )
1607+
1608+ # TigerTag+ links
1609+ if self .product_page_url :
1610+ parts .append (
1611+ f"Product page: { self .product_page_url } — "
1612+ f"API JSON: { self .api_url } "
1613+ )
1614+
1615+ # Authentication
1616+ if self .is_signed :
1617+ parts .append (
1618+ "Tag carries an ECDSA-P256 signature — call tag.verify() to confirm authenticity."
1619+ )
1620+ else :
1621+ parts .append ("Tag is not ECDSA-signed." )
1622+
1623+ return " " .join (parts )
1624+
15081625 def to_raw_dict (self ) -> Dict [str , Any ]:
15091626 """
15101627 Return protocol fields exactly as stored on the chip — no label resolution,
0 commit comments