Update LICENSE
This commit is contained in:
1 parent
580e66f7ab
commit
924c7c12b9
43 files changed
+4850
-3
No files matched your search
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
BagTag Master Data
|
||||
Shared source of truth for species and substrate definitions.
|
||||
Used by generate_tables.py and decode.py
|
||||
"""
|
||||
|
||||
# ── SUBSTRATES ──────────────────────────────────────────────────────────────
|
||||
# Indexed by substrate slot (0-31)
|
||||
# Used for filename generation
|
||||
|
||||
SUBSTRATES = {
|
||||
0: "aio-0-flagship-mm",
|
||||
1: "aio-1-speed-mm",
|
||||
2: "aio-2-woody-mm",
|
||||
3: "aio-3-low-n-mm",
|
||||
4: "aio-4-hobbyist-cvg",
|
||||
5: "aio-5-economy-cvg",
|
||||
6: "aio-6-straw-millet",
|
||||
7: "aio-7-straw-barley",
|
||||
8: "aio-8-pure-wood",
|
||||
9: "aio-9-blend",
|
||||
10: "aio-10-adv-soy",
|
||||
11: "aio-11-adv-rye",
|
||||
12: "aio-12-waste-stream",
|
||||
13: "aio-13-dung",
|
||||
14: "aio-14-custom",
|
||||
15: "spawn-15-rye",
|
||||
16: "spawn-16-milo",
|
||||
17: "spawn-17-millet",
|
||||
18: "spawn-18-oats",
|
||||
19: "spawn-19-wheat",
|
||||
20: "spawn-20-corn",
|
||||
21: "spawn-21-wbs",
|
||||
22: "spawn-22-rice",
|
||||
23: "spawn-23-custom",
|
||||
24: "bulk-24-mm",
|
||||
25: "bulk-25-hwsd",
|
||||
26: "bulk-26-bran",
|
||||
27: "bulk-27-cvg",
|
||||
28: "bulk-28-straw",
|
||||
29: "bulk-29-soy",
|
||||
30: "bulk-30-dung",
|
||||
31: "bulk-31-custom"
|
||||
}
|
||||
|
||||
|
||||
# ── SUBSTRATE DETAILS ───────────────────────────────────────────────────────
|
||||
# Detailed metadata for each substrate slot
|
||||
# Used by decoder and for generating substrates.md documentation
|
||||
|
||||
SUBSTRATE_DETAILS = {
|
||||
# AIO Standard (slots 0-9)
|
||||
0: {"label": "Masters Mix + Millet", "type": "aio", "category": "AIO Standard", "bulk": "Masters Mix (HWSD/Soy Hull 50:50)", "grain": "Millet", "character": "Flagship. Broad species, high yield, fast colonization. Core Cultivar Select bag."},
|
||||
1: {"label": "Masters Mix + Rye Berry", "type": "aio", "category": "AIO Standard", "bulk": "Masters Mix (HWSD/Soy Hull 50:50)", "grain": "Rye Berry", "character": "Speed variant. Fastest colonization. Same broad species compatibility."},
|
||||
2: {"label": "Masters Mix + Wheat Berry", "type": "aio", "category": "AIO Standard", "bulk": "Masters Mix (HWSD/Soy Hull 50:50)", "grain": "Wheat Berry", "character": "Woody species variant. Slower colonization suits Shiitake, Maitake."},
|
||||
3: {"label": "MM Lite + Wheat Berry", "type": "aio", "category": "AIO Standard", "bulk": "HWSD/Wheat Bran 80:20", "grain": "Wheat Berry", "character": "Slow woody. Lower nitrogen for Reishi, Turkey Tail, Shiitake."},
|
||||
4: {"label": "CVG + Millet", "type": "aio", "category": "AIO Standard", "bulk": "CVG (Coco Coir + Verm + Gypsum)", "grain": "Millet", "character": "Clean, no smell alternative for dung loving species."},
|
||||
5: {"label": "CVG + Oats", "type": "aio", "category": "AIO Standard", "bulk": "CVG (Coco Coir + Verm + Gypsum)", "grain": "Oats", "character": "Budget Oyster. Economical."},
|
||||
6: {"label": "Pasteurized Straw + Millet", "type": "aio", "category": "AIO Standard", "bulk": "Pasteurized Straw", "grain": "Millet", "character": "Oyster dominant. Clean fruiting surface."},
|
||||
7: {"label": "Pasteurized Straw + Barley", "type": "aio", "category": "AIO Standard", "bulk": "Pasteurized Straw", "grain": "Barley", "character": "Oyster dominant with barley grain for robust spawn."},
|
||||
8: {"label": "HWSD 100% + Wheat Berry", "type": "aio", "category": "AIO Standard", "bulk": "Hardwood Sawdust 100%", "grain": "Wheat Berry", "character": "Pure wood for conk and polypore species."},
|
||||
9: {"label": "Rye + HWSD Blended 70:30", "type": "aio", "category": "AIO Standard", "bulk": "HWSD 30%", "grain": "Rye 70% (blended)", "character": "Economy blend with rye grain dominance."},
|
||||
# AIO Advanced (slots 10-14)
|
||||
10: {"label": "Soy Hull Heavy 60:40 + Millet", "type": "aio-adv", "category": "AIO Advanced", "bulk": "Soy Hull Heavy 60:40", "grain": "Millet", "character": "High yield variant. Soy hulls for Oyster acceleration."},
|
||||
11: {"label": "Soy Hull Heavy 60:40 + Rye Berry", "type": "aio-adv", "category": "AIO Advanced", "bulk": "Soy Hull Heavy 60:40", "grain": "Rye Berry", "character": "High yield speed variant."},
|
||||
12: {"label": "Cardboard + Coffee Grounds 50:50", "type": "aio-adv", "category": "AIO Advanced", "bulk": "Cardboard + Coffee Grounds 50:50", "grain": "None (LC inoculated)", "character": "Waste stream grow. Zero cost substrate for experiments."},
|
||||
13: {"label": "Manure Based + Rye Berry", "type": "aio-adv", "category": "AIO Advanced", "bulk": "Composted Manure", "grain": "Rye Berry", "character": "Button mushroom and dung lovers. Requires pasteurization."},
|
||||
14: {"label": "Custom AIO", "type": "aio-custom","category": "AIO Advanced", "bulk": None, "grain": None, "character": "User-defined substrate. Species 127 (Other) triggers manual configuration."},
|
||||
# Grain Spawn (slots 15-23)
|
||||
15: {"label": "Rye Berry Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "Rye Berry", "character": "Standard spawn. Best for most species."},
|
||||
16: {"label": "Millet Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "Millet", "character": "Small grain, fast colonization."},
|
||||
17: {"label": "Milo Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "Milo", "character": "Small grain, economical alternative."},
|
||||
18: {"label": "Whole Oats Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "Whole Oats", "character": "Large grain, good for Gourmets."},
|
||||
19: {"label": "Wheat Berry Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "Wheat Berry", "character": "Standard spawn. Affordable and widely compatible."},
|
||||
20: {"label": "Whole Corn Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "Whole Corn", "character": "Large grain for aggressive species."},
|
||||
21: {"label": "Wild Bird Seed Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "WBS (mixed)", "character": "Mixed grain for diverse nutrition."},
|
||||
22: {"label": "Brown Rice Spawn", "type": "spawn", "category": "Grain Spawn", "grain": "Brown Rice", "character": "Alternative grain for experiments."},
|
||||
23: {"label": "Custom Grain Spawn", "type": "spawn-custom", "category": "Grain Spawn", "grain": None, "character": "User-defined grain. Species 127 (Other) triggers manual configuration."},
|
||||
# Bulk Substrates (slots 24-31)
|
||||
24: {"label": "Masters Mix (bulk)", "type": "bulk", "category": "Bulk", "bulk": "Masters Mix (HWSD/Soy Hull 50:50)", "character": "Commercial bulk. Scale-out block production."},
|
||||
25: {"label": "Hardwood Sawdust 100%", "type": "bulk", "category": "Bulk", "bulk": "HWSD 100%", "character": "Pure wood for conks and polypores."},
|
||||
26: {"label": "HWSD/Wheat Bran 80:20", "type": "bulk", "category": "Bulk", "bulk": "HWSD/Wheat Bran 80:20", "character": "Standard wood lover bulk with nutrition boost."},
|
||||
27: {"label": "CVG (bulk)", "type": "bulk", "category": "Bulk", "bulk": "CVG (Coco Coir + Verm + Gypsum)", "character": "Clean bulk for indoor fruiting. No wood chips."},
|
||||
28: {"label": "Pasteurized Straw (bulk)", "type": "bulk", "category": "Bulk", "bulk": "Pasteurized Straw", "character": "Low cost Oyster production."},
|
||||
29: {"label": "Soy Hull Heavy 60:40 (bulk)", "type": "bulk", "category": "Bulk", "bulk": "Soy Hull Heavy 60:40", "character": "High yield Oyster bulk production."},
|
||||
30: {"label": "Manure Based (bulk)", "type": "bulk", "category": "Bulk", "bulk": "Composted Manure", "character": "Button and dung lovers. Requires full pasteurization."},
|
||||
31: {"label": "Custom Bulk", "type": "bulk-custom", "category": "Bulk", "bulk": None, "character": "User-defined bulk substrate. Species 127 (Other) triggers manual configuration."},
|
||||
}
|
||||
|
||||
|
||||
# ── SPECIES MASTER DATA ─────────────────────────────────────────────────────
|
||||
# Source of truth for species compatibility across all substrate types
|
||||
|
||||
SPECIES_MASTER = [
|
||||
# IDs 0-16: The Oyster Dynasty
|
||||
{"id": 0, "name": "Blue Oyster", "sci": "Pleurotus ostreatus", "optimal_aios": [6, 0], "permissive_aios": [4, 7, 9, 14], "optimal_bulk": [28, 24], "permissive_bulk": [27, 31], "spawn_subs": [15, 17, 21, 23]},
|
||||
{"id": 1, "name": "Pearl Oyster", "sci": "Pleurotus ostreatus", "optimal_aios": [6, 0], "permissive_aios": [7, 9, 14], "optimal_bulk": [28, 24], "permissive_bulk": [27, 31], "spawn_subs": [16, 17, 20, 23]},
|
||||
{"id": 2, "name": "Golden Oyster", "sci": "Pleurotus citrinopileatus", "optimal_aios": [6, 11], "permissive_aios": [0, 14], "optimal_bulk": [28, 29], "permissive_bulk": [27, 31], "spawn_subs": [16, 17, 23]},
|
||||
{"id": 3, "name": "Pink Oyster", "sci": "Pleurotus djamor", "optimal_aios": [6, 11], "permissive_aios": [0, 12, 14], "optimal_bulk": [28, 29], "permissive_bulk": [27, 31], "spawn_subs": [15, 17, 23]},
|
||||
{"id": 4, "name": "King Trumpet", "sci": "Pleurotus eryngii", "optimal_aios": [0, 10], "permissive_aios": [2, 11, 14], "optimal_bulk": [24, 29], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 5, "name": "Black Pearl King", "sci": "Hybrid", "optimal_aios": [0, 11], "permissive_aios": [2, 14], "optimal_bulk": [24, 29], "permissive_bulk": [26, 31], "spawn_subs": [15, 18, 23]},
|
||||
{"id": 6, "name": "Phoenix Oyster", "sci": "Pleurotus pulmonarius", "optimal_aios": [6, 0], "permissive_aios": [4, 9, 14], "optimal_bulk": [28, 24], "permissive_bulk": [27, 31], "spawn_subs": [15, 17, 23]},
|
||||
{"id": 7, "name": "Snow Oyster", "sci": "Pleurotus columbinus", "optimal_aios": [0, 4], "permissive_aios": [9, 14], "optimal_bulk": [24, 27], "permissive_bulk": [25, 31], "spawn_subs": [15, 18, 23]},
|
||||
{"id": 8, "name": "Abalone Mushroom", "sci": "Pleurotus cystidiosus", "optimal_aios": [0, 4], "permissive_aios": [9, 14], "optimal_bulk": [24, 27], "permissive_bulk": [25, 31], "spawn_subs": [15, 16, 23]},
|
||||
{"id": 9, "name": "Tarragon Oyster", "sci": "Pleurotus euosmus", "optimal_aios": [0, 4], "permissive_aios": [9, 14], "optimal_bulk": [24, 27], "permissive_bulk": [25, 31], "spawn_subs": [15, 17, 23]},
|
||||
{"id": 10, "name": "Florida Oyster", "sci": "Pleurotus floridanus", "optimal_aios": [6, 11], "permissive_aios": [0, 14], "optimal_bulk": [28, 29], "permissive_bulk": [24, 31], "spawn_subs": [16, 17, 23]},
|
||||
{"id": 11, "name": "Lung Oyster", "sci": "Pleurotus pulmonarius", "optimal_aios": [0, 4], "permissive_aios": [9, 14], "optimal_bulk": [24, 27], "permissive_bulk": [25, 31], "spawn_subs": [16, 17, 23]},
|
||||
{"id": 12, "name": "Late Oyster", "sci": "Sarcomyxa serotina", "optimal_aios": [8], "permissive_aios": [0, 2, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 13, "name": "Veiled Oyster", "sci": "Pleurotus dryinus", "optimal_aios": [8], "permissive_aios": [0, 2, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 14, "name": "Tree Oyster", "sci": "Pleurotus sapidus", "optimal_aios": [0, 9], "permissive_aios": [2, 14], "optimal_bulk": [24], "permissive_bulk": [25, 31], "spawn_subs": [15, 17, 23]},
|
||||
{"id": 15, "name": "Elm Oyster", "sci": "Hypsizygus ulmarius", "optimal_aios": [0, 2], "permissive_aios": [8, 9, 14], "optimal_bulk": [24, 25], "permissive_bulk": [26, 31], "spawn_subs": [15, 18, 19, 23]},
|
||||
{"id": 16, "name": "Indian Oyster", "sci": "Pleurotus sajor-caju", "optimal_aios": [6, 0], "permissive_aios": [4, 7, 14], "optimal_bulk": [28, 24], "permissive_bulk": [27, 31], "spawn_subs": [15, 17, 23]},
|
||||
# IDs 17-43: Wood Gourmets
|
||||
{"id": 17, "name": "Lion's Mane", "sci": "Hericium erinaceus", "optimal_aios": [0, 1], "permissive_aios": [2, 9, 10, 14], "optimal_bulk": [24, 28], "permissive_bulk": [25, 26, 31], "spawn_subs": [15, 16, 17, 23]},
|
||||
{"id": 18, "name": "Bear's Head", "sci": "Hericium americanum", "optimal_aios": [0, 1], "permissive_aios": [2, 9, 14], "optimal_bulk": [24, 28], "permissive_bulk": [25, 31], "spawn_subs": [15, 16, 23]},
|
||||
{"id": 19, "name": "Coral Tooth", "sci": "Hericium coralloides", "optimal_aios": [0, 11], "permissive_aios": [2, 14], "optimal_bulk": [24, 28], "permissive_bulk": [25, 31], "spawn_subs": [15, 17, 23]},
|
||||
{"id": 20, "name": "Shiitake (Fast)", "sci": "Lentinula edodes", "optimal_aios": [0, 1], "permissive_aios": [2, 9, 14], "optimal_bulk": [24], "permissive_bulk": [26, 31], "spawn_subs": [15, 18, 19, 23]},
|
||||
{"id": 21, "name": "Shiitake (Donko)", "sci": "Lentinula edodes", "optimal_aios": [8, 3], "permissive_aios": [2, 14], "optimal_bulk": [25, 26], "permissive_bulk": [24, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 22, "name": "Chestnut", "sci": "Pholiota adiposa", "optimal_aios": [0, 11], "permissive_aios": [2, 14], "optimal_bulk": [24, 28], "permissive_bulk": [26, 31], "spawn_subs": [15, 16, 23]},
|
||||
{"id": 23, "name": "Pioppino", "sci": "Cyclocybe aegerita", "optimal_aios": [0, 2], "permissive_aios": [9, 14], "optimal_bulk": [24, 25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 24, "name": "Nameko", "sci": "Pholiota nameko", "optimal_aios": [8, 3], "permissive_aios": [0, 14], "optimal_bulk": [25, 26], "permissive_bulk": [24, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 25, "name": "Maitake", "sci": "Grifola frondosa", "optimal_aios": [0, 2], "permissive_aios": [3, 14], "optimal_bulk": [24], "permissive_bulk": [25, 26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 26, "name": "Brown Beech", "sci": "Hypsizygus tessulatus", "optimal_aios": [10, 11], "permissive_aios": [0, 14], "optimal_bulk": [28, 24], "permissive_bulk": [26, 31], "spawn_subs": [15, 16, 23]},
|
||||
{"id": 27, "name": "White Beech", "sci": "Hypsizygus tessulatus", "optimal_aios": [10, 11], "permissive_aios": [0, 14], "optimal_bulk": [28, 24], "permissive_bulk": [26, 31], "spawn_subs": [15, 16, 23]},
|
||||
{"id": 28, "name": "Wood Ear", "sci": "Auricularia auricula-judae","optimal_aios": [6, 8], "permissive_aios": [0, 9, 14], "optimal_bulk": [28, 25], "permissive_bulk": [24, 31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 29, "name": "Black Fungus", "sci": "Auricularia polytricha", "optimal_aios": [8, 0], "permissive_aios": [9, 14], "optimal_bulk": [25, 28], "permissive_bulk": [24, 31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 30, "name": "Enoki (White)", "sci": "Flammulina velutipes", "optimal_aios": [10, 11], "permissive_aios": [0, 14], "optimal_bulk": [28], "permissive_bulk": [24, 31], "spawn_subs": [16, 18, 23]},
|
||||
{"id": 31, "name": "Enoki (Gold)", "sci": "Flammulina velutipes", "optimal_aios": [0, 2], "permissive_aios": [8, 14], "optimal_bulk": [24], "permissive_bulk": [25, 31], "spawn_subs": [16, 18, 23]},
|
||||
{"id": 32, "name": "Tiger Sawgill", "sci": "Lentinus tigrinus", "optimal_aios": [0, 9], "permissive_aios": [2, 14], "optimal_bulk": [24], "permissive_bulk": [25, 31], "spawn_subs": [15, 17, 23]},
|
||||
{"id": 33, "name": "Cauliflower Mushroom", "sci": "Sparassis crispa", "optimal_aios": [8, 2], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [24, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 34, "name": "Beefsteak Fungus", "sci": "Fistulina hepatica", "optimal_aios": [2], "permissive_aios": [8, 14], "optimal_bulk": [24], "permissive_bulk": [25, 31], "spawn_subs": [18, 23]},
|
||||
{"id": 35, "name": "Chicken of the Woods", "sci": "Laetiporus sulphureus", "optimal_aios": [8], "permissive_aios": [2, 14], "optimal_bulk": [25], "permissive_bulk": [24, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 36, "name": "Black Poplar", "sci": "Cyclocybe aegerita", "optimal_aios": [2, 3], "permissive_aios": [8, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 37, "name": "Dryad's Saddle", "sci": "Cerioporus squamosus", "optimal_aios": [8], "permissive_aios": [2, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 38, "name": "Velvet Foot (Wild)", "sci": "Flammulina velutipes", "optimal_aios": [0, 2], "permissive_aios": [8, 14], "optimal_bulk": [24], "permissive_bulk": [25, 31], "spawn_subs": [18, 23]},
|
||||
{"id": 39, "name": "Golden Needle", "sci": "Flammulina filiformis", "optimal_aios": [10, 11], "permissive_aios": [0, 14], "optimal_bulk": [28], "permissive_bulk": [24, 31], "spawn_subs": [18, 23]},
|
||||
{"id": 40, "name": "Lion's Mane (LM-01)", "sci": "Hericium erinaceus", "optimal_aios": [0], "permissive_aios": [1, 2, 14], "optimal_bulk": [24], "permissive_bulk": [28, 31], "spawn_subs": [15, 16, 23]},
|
||||
{"id": 41, "name": "Maitake (900)", "sci": "Grifola frondosa", "optimal_aios": [2], "permissive_aios": [0, 3, 14], "optimal_bulk": [24], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 42, "name": "White-pored Chicken", "sci": "Laetiporus cincinnatus", "optimal_aios": [8], "permissive_aios": [2, 14], "optimal_bulk": [25], "permissive_bulk": [24, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 43, "name": "Wine Cap", "sci": "Stropharia rugosoannulata", "optimal_aios": [13], "permissive_aios": [6, 12, 14], "optimal_bulk": [29, 27], "permissive_bulk": [26, 31], "spawn_subs": [17, 21, 23]},
|
||||
# IDs 44-59: Compost & Dung Lovers
|
||||
{"id": 44, "name": "White Button", "sci": "Agaricus bisporus", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [15, 20, 23]},
|
||||
{"id": 45, "name": "Cremini", "sci": "Agaricus bisporus", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [15, 20, 23]},
|
||||
{"id": 46, "name": "Portobello", "sci": "Agaricus bisporus", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [15, 20, 23]},
|
||||
{"id": 47, "name": "Almond Agaricus", "sci": "Agaricus subrufescens", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [15, 17, 23]},
|
||||
{"id": 48, "name": "Shaggy Mane", "sci": "Coprinus comatus", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [26, 31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 49, "name": "Horse Mushroom", "sci": "Agaricus arvensis", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [15, 20, 23]},
|
||||
{"id": 50, "name": "The Prince", "sci": "Agaricus augustus", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [15, 20, 23]},
|
||||
{"id": 51, "name": "Blewit", "sci": "Lepista nuda", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 52, "name": "Paddy Straw", "sci": "Volvariella volvacea", "optimal_aios": [6, 7], "permissive_aios": [13, 14], "optimal_bulk": [27], "permissive_bulk": [29, 31], "spawn_subs": [16, 17, 23]},
|
||||
{"id": 53, "name": "Milky Mushroom", "sci": "Calocybe indica", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [16, 20, 23]},
|
||||
{"id": 54, "name": "Bamboo Pith", "sci": "Phallus indusiatus", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [17, 23]},
|
||||
{"id": 55, "name": "Bamboo Fungus", "sci": "Phallus sp.", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [17, 23]},
|
||||
{"id": 56, "name": "Glistening Inky", "sci": "Coprinellus micaceus", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [29], "permissive_bulk": [31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 57, "name": "Ink Cap", "sci": "Coprinopsis lagopus", "optimal_aios": [12], "permissive_aios": [13, 14], "optimal_bulk": [26], "permissive_bulk": [29, 31], "spawn_subs": [17, 23]},
|
||||
{"id": 58, "name": "Summer Mushroom", "sci": "Agaricus bitorquis", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [30], "permissive_bulk": [31], "spawn_subs": [15, 20, 23]},
|
||||
{"id": 59, "name": "Field Mushroom", "sci": "Agaricus campestris", "optimal_aios": [13], "permissive_aios": [14], "optimal_bulk": [30], "permissive_bulk": [31], "spawn_subs": [15, 20, 23]},
|
||||
# IDs 60-84: Functional Polypores & Conks
|
||||
{"id": 60, "name": "Red Reishi", "sci": "Ganoderma lingzhi", "optimal_aios": [0, 3], "permissive_aios": [8, 14], "optimal_bulk": [24, 25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 61, "name": "Yellow Reishi", "sci": "Ganoderma curtisii", "optimal_aios": [0, 3], "permissive_aios": [9, 14], "optimal_bulk": [24, 25], "permissive_bulk": [26, 31], "spawn_subs": [15, 16, 23]},
|
||||
{"id": 62, "name": "Black Reishi", "sci": "Ganoderma sinense", "optimal_aios": [3, 8], "permissive_aios": [2, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 63, "name": "Oregon Reishi", "sci": "Ganoderma oregonense", "optimal_aios": [8], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 64, "name": "Southern Reishi", "sci": "Ganoderma curtisii", "optimal_aios": [0, 9], "permissive_aios": [3, 14], "optimal_bulk": [24], "permissive_bulk": [25, 31], "spawn_subs": [16, 23]},
|
||||
{"id": 65, "name": "Hemlock Varnish", "sci": "Ganoderma tsugae", "optimal_aios": [8], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 66, "name": "Reishi (G-W)", "sci": "Ganoderma lingzhi", "optimal_aios": [0, 3], "permissive_aios": [8, 14], "optimal_bulk": [24], "permissive_bulk": [25, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 67, "name": "Turkey Tail", "sci": "Trametes versicolor", "optimal_aios": [0, 8], "permissive_aios": [3, 9, 14], "optimal_bulk": [24, 25], "permissive_bulk": [26, 31], "spawn_subs": [16, 17, 21, 23]},
|
||||
{"id": 68, "name": "Turkey Tail (Wild)", "sci": "Trametes versicolor", "optimal_aios": [8, 0], "permissive_aios": [9, 14], "optimal_bulk": [25], "permissive_bulk": [24, 31], "spawn_subs": [21, 23]},
|
||||
{"id": 69, "name": "False Turkey Tail", "sci": "Stereum ostrea", "optimal_aios": [8, 12], "permissive_aios": [3, 14], "optimal_bulk": [25, 27], "permissive_bulk": [26, 31], "spawn_subs": [17, 23]},
|
||||
{"id": 70, "name": "Artist's Conk", "sci": "Ganoderma applanatum", "optimal_aios": [8], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 71, "name": "Agarikon", "sci": "Laricifomes officinalis", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [19, 23]},
|
||||
{"id": 72, "name": "Mesima", "sci": "Phellinus linteus", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [19, 23]},
|
||||
{"id": 73, "name": "Birch Polypore", "sci": "Fomitopsis betulina", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 74, "name": "Tinder Polypore", "sci": "Fomitopsis fomentarius", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 75, "name": "False Tinder Poly", "sci": "Phellinus igniarius", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 76, "name": "Hoof Fungus", "sci": "Fomitopsis fomentarius", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [19, 23]},
|
||||
{"id": 77, "name": "Amadou Sponge", "sci": "Fomitopsis fomentarius", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 78, "name": "Willow Bracket", "sci": "Phellinus igniarius", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [19, 23]},
|
||||
{"id": 79, "name": "Pine Conk", "sci": "Porodaedalea pini", "optimal_aios": [8], "permissive_aios": [14], "optimal_bulk": [25], "permissive_bulk": [31], "spawn_subs": [19, 23]},
|
||||
{"id": 80, "name": "Resinous Polypore", "sci": "Ischnoderma resinosum", "optimal_aios": [8], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 81, "name": "Cinnabar Polypore", "sci": "Pycnoporus cinnabarinus", "optimal_aios": [8], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 82, "name": "Violet Polypore", "sci": "Trichaptum biforme", "optimal_aios": [8], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 83, "name": "Purple-Gilled Poly", "sci": "Trichaptum biforme", "optimal_aios": [8], "permissive_aios": [3, 14], "optimal_bulk": [25], "permissive_bulk": [26, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 84, "name": "Bitter Oyster", "sci": "Panellus stipticus", "optimal_aios": [12], "permissive_aios": [8, 14], "optimal_bulk": [27], "permissive_bulk": [25, 31], "spawn_subs": [17, 23]},
|
||||
# IDs 85-104: Experimental & Commercial Strains
|
||||
{"id": 85, "name": "Cordyceps", "sci": "Cordyceps militaris", "optimal_aios": [14], "permissive_aios": [], "optimal_bulk": [], "permissive_bulk": [31], "spawn_subs": [22, 23]},
|
||||
{"id": 86, "name": "Silver Ear", "sci": "Tremella fuciformis", "optimal_aios": [14], "permissive_aios": [], "optimal_bulk": [], "permissive_bulk": [31], "spawn_subs": [22, 23]},
|
||||
{"id": 87, "name": "Split Gill", "sci": "Schizophyllum commune", "optimal_aios": [12], "permissive_aios": [8, 14], "optimal_bulk": [27], "permissive_bulk": [25, 31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 88, "name": "Diesel Oyster", "sci": "Pleurotus ostreatus", "optimal_aios": [12], "permissive_aios": [14], "optimal_bulk": [27], "permissive_bulk": [31], "spawn_subs": [17, 23]},
|
||||
{"id": 89, "name": "Sugar Cane Oyster", "sci": "Pleurotus djamor var.", "optimal_aios": [12], "permissive_aios": [14], "optimal_bulk": [28], "permissive_bulk": [31], "spawn_subs": [17, 23]},
|
||||
{"id": 90, "name": "Leather Fungus", "sci": "Ganoderma sp.", "optimal_aios": [10, 12], "permissive_aios": [0, 14], "optimal_bulk": [28], "permissive_bulk": [24, 31], "spawn_subs": [16, 17, 23]},
|
||||
{"id": 91, "name": "Packaging Myco", "sci": "Various", "optimal_aios": [12], "permissive_aios": [4, 14], "optimal_bulk": [27], "permissive_bulk": [28, 31], "spawn_subs": [17, 23]},
|
||||
{"id": 92, "name": "Insulation Myco", "sci": "Trametes pubescens", "optimal_aios": [12], "permissive_aios": [8, 14], "optimal_bulk": [27], "permissive_bulk": [25, 31], "spawn_subs": [17, 23]},
|
||||
{"id": 93, "name": "Textile Myco", "sci": "Schizophyllum commune", "optimal_aios": [12], "permissive_aios": [14], "optimal_bulk": [27], "permissive_bulk": [31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 94, "name": "Paper Myco", "sci": "Polyporus sp.", "optimal_aios": [12], "permissive_aios": [14], "optimal_bulk": [27], "permissive_bulk": [31], "spawn_subs": [17, 21, 23]},
|
||||
{"id": 95, "name": "Shiitake (LE-46)", "sci": "Lentinula edodes", "optimal_aios": [8, 3], "permissive_aios": [2, 14], "optimal_bulk": [25, 26], "permissive_bulk": [24, 31], "spawn_subs": [18, 19, 23]},
|
||||
{"id": 96, "name": "Blue Oyster (3015)", "sci": "Pleurotus ostreatus", "optimal_aios": [0, 1], "permissive_aios": [6, 14], "optimal_bulk": [24], "permissive_bulk": [28, 31], "spawn_subs": [15, 23]},
|
||||
{"id": 97, "name": "Pearl (P-77)", "sci": "Pleurotus ostreatus", "optimal_aios": [6, 7], "permissive_aios": [0, 14], "optimal_bulk": [28], "permissive_bulk": [24, 31], "spawn_subs": [16, 17, 23]},
|
||||
{"id": 98, "name": "Pink (V-22)", "sci": "Pleurotus djamor", "optimal_aios": [11, 6], "permissive_aios": [0, 14], "optimal_bulk": [29, 28], "permissive_bulk": [24, 31], "spawn_subs": [17, 23]},
|
||||
{"id": 99, "name": "Chestnut (CH-2)", "sci": "Pholiota adiposa", "optimal_aios": [11, 0], "permissive_aios": [2, 14], "optimal_bulk": [29, 24], "permissive_bulk": [26, 31], "spawn_subs": [15, 23]},
|
||||
{"id": 100,"name": "Enoki (Comm.)", "sci": "Flammulina velutipes", "optimal_aios": [10], "permissive_aios": [0, 14], "optimal_bulk": [28], "permissive_bulk": [24, 31], "spawn_subs": [18, 23]},
|
||||
{"id": 101,"name": "Black Pearl (Hy-X)", "sci": "Hybrid", "optimal_aios": [11, 0], "permissive_aios": [2, 14], "optimal_bulk": [29, 24], "permissive_bulk": [26, 31], "spawn_subs": [15, 18, 23]},
|
||||
{"id": 102,"name": "Abalone (Taiwan)", "sci": "Pleurotus cystidiosus", "optimal_aios": [11, 4], "permissive_aios": [0, 14], "optimal_bulk": [27], "permissive_bulk": [24, 31], "spawn_subs": [16, 23]},
|
||||
{"id": 103,"name": "Snow (Deep Blue)", "sci": "Pleurotus columbinus", "optimal_aios": [4, 9], "permissive_aios": [0, 14], "optimal_bulk": [27], "permissive_bulk": [24, 31], "spawn_subs": [18, 23]},
|
||||
]
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
BagTag v1 Reference Decoder
|
||||
Grove5 — Open source software, honest hardware
|
||||
https://grove5.com
|
||||
|
||||
Decodes an AprilTag41h12 integer ID into size, substrate, and species.
|
||||
Uses packed enumeration - only valid substrate+species combinations are assigned tag IDs.
|
||||
|
||||
Usage:
|
||||
python decode.py <tag_id>
|
||||
python decode.py 0
|
||||
python decode.py 0 --json
|
||||
python decode.py --tables
|
||||
python decode.py --stats
|
||||
python decode.py --ranges
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from bagtag_data import (
|
||||
SUBSTRATES as SUBSTRATE_NAMES,
|
||||
SUBSTRATE_DETAILS as SUBSTRATES,
|
||||
SPECIES_MASTER
|
||||
)
|
||||
|
||||
|
||||
# SPECIES_MASTER imported from bagtag_data.py
|
||||
OTHER_SPECIES = {"code": 127, "label": "Other", "common": "Other/prompt", "scientific": None}
|
||||
|
||||
|
||||
def _build_species_tables():
|
||||
"""Build species lookup tables from master data for all 32 substrate slots."""
|
||||
tables = {}
|
||||
for sub_id in range(32):
|
||||
tables[sub_id] = {}
|
||||
for spec in SPECIES_MASTER:
|
||||
# Combine optimal and permissive slots across all categories
|
||||
all_optimal = spec.get("optimal_aios", []) + spec.get("optimal_bulk", []) + spec.get("spawn_subs", [])
|
||||
all_permissive = spec.get("permissive_aios", []) + spec.get("permissive_bulk", [])
|
||||
if sub_id in all_optimal or sub_id in all_permissive:
|
||||
tables[sub_id][spec["id"]] = {"common": spec["name"], "scientific": spec["sci"]}
|
||||
tables[sub_id][127] = OTHER_SPECIES
|
||||
return tables
|
||||
|
||||
|
||||
# ── SPECIES TABLES ───────────────────────────────────────────────────────────
|
||||
# Generated dynamically from SPECIES_MASTER for all 32 substrate slots
|
||||
|
||||
SPECIES = _build_species_tables()
|
||||
|
||||
|
||||
|
||||
# ── TAG ID ASSIGNMENT ───────────────────────────────────────────────────────
|
||||
|
||||
def _build_tag_lookup():
|
||||
"""Build packed enumeration of all valid (size, substrate, species) combinations.
|
||||
|
||||
Tag IDs are assigned sequentially starting from 0:
|
||||
- Each valid substrate+species combo gets 2 consecutive tag IDs (small, then large)
|
||||
- Substrates are processed in order 0-31
|
||||
- Within each substrate, species are sorted by species ID
|
||||
- Max valid tag ID = 1,969 (1,970 valid combos total)
|
||||
- Tag IDs 1,970-2,114 are reserved for future use
|
||||
"""
|
||||
entries = [] # [(tag_id, size, substrate_slot, species_id, species_name, sci_name)]
|
||||
|
||||
for substrate_slot in range(32):
|
||||
if substrate_slot not in SUBSTRATES:
|
||||
continue
|
||||
if substrate_slot not in SPECIES:
|
||||
continue
|
||||
|
||||
substrate = SUBSTRATES[substrate_slot]
|
||||
species_table = SPECIES[substrate_slot]
|
||||
|
||||
# Get all species IDs for this substrate (excluding 63/Other)
|
||||
species_ids = sorted([sid for sid in species_table.keys() if sid != 63])
|
||||
|
||||
for species_id in species_ids:
|
||||
species = species_table[species_id]
|
||||
# Small = 0, Large = 1
|
||||
for size in [0, 1]:
|
||||
entries.append((
|
||||
len(entries), # sequential tag ID
|
||||
size, # 0=small, 1=large
|
||||
substrate_slot,
|
||||
species_id,
|
||||
species["common"],
|
||||
species["scientific"],
|
||||
substrate["label"],
|
||||
substrate["type"]
|
||||
))
|
||||
|
||||
# Build lookup tables
|
||||
decode_table = {} # tag_id -> entry
|
||||
encode_table = {} # (size, substrate_slot, species_id) -> tag_id
|
||||
|
||||
for entry in entries:
|
||||
tag_id = entry[0]
|
||||
size = entry[1]
|
||||
substrate_slot = entry[2]
|
||||
species_id = entry[3]
|
||||
decode_table[tag_id] = entry
|
||||
encode_table[(size, substrate_slot, species_id)] = tag_id
|
||||
|
||||
return decode_table, encode_table, len(entries)
|
||||
|
||||
|
||||
TAG_DECODE_TABLE, TAG_ENCODE_TABLE, MAX_VALID_TAG = _build_tag_lookup()
|
||||
VALID_TAG_COUNT = MAX_VALID_TAG # Excludes reserved 1970-2114
|
||||
|
||||
|
||||
# ── DECODER ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def decode(tag_id: int) -> dict:
|
||||
"""Decode an AprilTag41h12 integer ID into size, substrate, and species.
|
||||
|
||||
Uses packed enumeration - only pre-registered valid combinations decode successfully.
|
||||
Raises ValueError for tag IDs not assigned to valid bags or in reserved range.
|
||||
"""
|
||||
if not (0 <= tag_id <= 2114):
|
||||
raise ValueError(f"Tag ID {tag_id} out of range (0-2114)")
|
||||
|
||||
if tag_id >= MAX_VALID_TAG:
|
||||
raise ValueError(f"Tag ID {tag_id} is reserved (valid range: 0-{MAX_VALID_TAG - 1})")
|
||||
|
||||
if tag_id not in TAG_DECODE_TABLE:
|
||||
raise ValueError(f"Tag ID {tag_id} not assigned to any valid bag configuration")
|
||||
|
||||
entry = TAG_DECODE_TABLE[tag_id]
|
||||
size, substrate_slot, species_id, species_name, sci_name, sub_label, sub_type = entry[1:]
|
||||
|
||||
return {
|
||||
"tag_id": tag_id,
|
||||
"size": "small" if size == 0 else "large",
|
||||
"substrate_slot": substrate_slot,
|
||||
"substrate": {"label": sub_label, "type": sub_type},
|
||||
"species_id": species_id,
|
||||
"species": {"common": species_name, "scientific": sci_name},
|
||||
"valid": True,
|
||||
}
|
||||
|
||||
|
||||
def encode(size: str, substrate_slot: int, species_id: int) -> int:
|
||||
"""Encode a bag configuration to its tag ID.
|
||||
|
||||
Args:
|
||||
size: "small" or "large"
|
||||
substrate_slot: 0-31
|
||||
species_id: species code for that substrate
|
||||
|
||||
Returns:
|
||||
Tag ID (0-1969)
|
||||
|
||||
Raises:
|
||||
ValueError if the combination is not valid/registered
|
||||
"""
|
||||
size_bit = 0 if size == "small" else 1
|
||||
key = (size_bit, substrate_slot, species_id)
|
||||
|
||||
if key not in TAG_ENCODE_TABLE:
|
||||
raise ValueError(
|
||||
f"Invalid bag configuration: {size} bag, substrate {substrate_slot}, "
|
||||
f"species {species_id} is not a valid combination"
|
||||
)
|
||||
|
||||
return TAG_ENCODE_TABLE[key]
|
||||
|
||||
|
||||
def pretty(result: dict) -> str:
|
||||
"""Format decode result as human-readable string."""
|
||||
lines = [
|
||||
f"Tag ID: {result['tag_id']}",
|
||||
f"Size: {result['size']}",
|
||||
f"Substrate: [{result['substrate_slot']:02d}] {result['substrate'].get('label', '?')}",
|
||||
f"Species: [{result['species_id']:02d}] {result['species'].get('common', '?')}",
|
||||
]
|
||||
sci = result["species"].get("scientific")
|
||||
if sci:
|
||||
lines.append(f" ({sci})")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def print_species_tables():
|
||||
"""Print all species lookup tables with their assigned tag ID ranges."""
|
||||
print(f"# BagTag Species Tables\n")
|
||||
print(f"Total valid combinations: {VALID_TAG_COUNT} (tag IDs 0-{MAX_VALID_TAG - 1})")
|
||||
print(f"Reserved tag IDs: {2115 - MAX_VALID_TAG} (tag IDs {MAX_VALID_TAG}-2114)\n")
|
||||
|
||||
for slot in range(32):
|
||||
sub = SUBSTRATES.get(slot)
|
||||
if sub is None or slot not in SPECIES:
|
||||
continue
|
||||
|
||||
sub_name = sub['label']
|
||||
sub_type = sub['type']
|
||||
table = SPECIES.get(slot, {})
|
||||
species_ids = sorted([sid for sid in table.keys() if sid != 127])
|
||||
|
||||
# Find tag ID range for this substrate
|
||||
tag_ids = []
|
||||
for sid in species_ids:
|
||||
for size in [0, 1]:
|
||||
key = (size, slot, sid)
|
||||
if key in TAG_ENCODE_TABLE:
|
||||
tag_ids.append(TAG_ENCODE_TABLE[key])
|
||||
|
||||
if tag_ids:
|
||||
min_tag, max_tag = min(tag_ids), max(tag_ids)
|
||||
else:
|
||||
min_tag = max_tag = None
|
||||
|
||||
print(f"\n## Slot {slot}: {sub_name}")
|
||||
print(f"Type: {sub_type}")
|
||||
if min_tag is not None:
|
||||
print(f"Tag ID range: {min_tag}-{max_tag}")
|
||||
print()
|
||||
print("| Species ID | Species | Scientific | Small Tag | Large Tag |")
|
||||
print("|------------|---------|------------|-----------|-----------|")
|
||||
|
||||
for sid in species_ids:
|
||||
spec = table[sid]
|
||||
small_tag = TAG_ENCODE_TABLE.get((0, slot, sid), "—")
|
||||
large_tag = TAG_ENCODE_TABLE.get((1, slot, sid), "—")
|
||||
print(f"| {sid:02d} | {spec['common']} | {spec['scientific']} | {small_tag} | {large_tag} |")
|
||||
|
||||
print(f"\nTotal: {len(species_ids)} species ({len(species_ids) * 2} tag IDs)")
|
||||
|
||||
|
||||
def print_ranges():
|
||||
"""Print actual tag ID ranges assigned to each substrate."""
|
||||
print(f"BagTag v1 Tag ID Ranges by Substrate")
|
||||
print(f"=" * 60)
|
||||
print(f"Note: Tag IDs are assigned sequentially across all substrates.")
|
||||
print(f"Each substrate's tag IDs are interleaved, not contiguous.\n")
|
||||
print(f"{'Slot':>4} | {'Substrate':<35} | {'Species':>7} | {'Tag ID Ranges (small + large)':<30}")
|
||||
print(f"{'-'*4}-+-{'-'*35}-+-{'-'*7}-+-{'-'*30}")
|
||||
|
||||
for slot in range(32):
|
||||
if slot not in SUBSTRATES or slot not in SPECIES:
|
||||
continue
|
||||
|
||||
sub = SUBSTRATES[slot]
|
||||
table = SPECIES.get(slot, {})
|
||||
species_ids = sorted([sid for sid in table.keys() if sid != 127])
|
||||
|
||||
if not species_ids:
|
||||
continue
|
||||
|
||||
# Collect all tag IDs for this slot
|
||||
small_tags = []
|
||||
large_tags = []
|
||||
for sid in species_ids:
|
||||
key_small = (0, slot, sid)
|
||||
key_large = (1, slot, sid)
|
||||
if key_small in TAG_ENCODE_TABLE:
|
||||
small_tags.append(TAG_ENCODE_TABLE[key_small])
|
||||
if key_large in TAG_ENCODE_TABLE:
|
||||
large_tags.append(TAG_ENCODE_TABLE[key_large])
|
||||
|
||||
if not small_tags:
|
||||
continue
|
||||
|
||||
# Format ranges
|
||||
small_tags.sort()
|
||||
large_tags.sort()
|
||||
|
||||
def format_ranges(tags):
|
||||
if not tags:
|
||||
return "—"
|
||||
# Compress consecutive ranges
|
||||
ranges = []
|
||||
start = end = tags[0]
|
||||
for t in tags[1:]:
|
||||
if t == end + 1:
|
||||
end = t
|
||||
else:
|
||||
ranges.append(f"{start}" if start == end else f"{start}-{end}")
|
||||
start = end = t
|
||||
ranges.append(f"{start}" if start == end else f"{start}-{end}")
|
||||
return ", ".join(ranges[:3]) # Limit to first 3 ranges
|
||||
|
||||
small_range = format_ranges(small_tags)
|
||||
large_range = format_ranges(large_tags)
|
||||
|
||||
sub_name = sub['label'][:35]
|
||||
print(f"{slot:>4} | {sub_name:<35} | {len(species_ids):>7} | small: {small_range}")
|
||||
print(f" | {' '*35} | {' '*7} | large: {large_range}")
|
||||
|
||||
print(f"\nTotal valid tags: {VALID_TAG_COUNT}")
|
||||
|
||||
|
||||
def print_stats():
|
||||
"""Print namespace statistics."""
|
||||
print(f"BagTag v1 Namespace Statistics")
|
||||
print(f"=" * 40)
|
||||
print(f"AprilTag41h12 total IDs: 2,115")
|
||||
print(f"Valid bag configurations: {VALID_TAG_COUNT:,}")
|
||||
print(f"Reserved for future: {2115 - VALID_TAG_COUNT:,}")
|
||||
print(f"Utilization: {VALID_TAG_COUNT / 2115 * 100:.1f}%")
|
||||
print()
|
||||
print(f"Breakdown by substrate:")
|
||||
for slot in range(32):
|
||||
if slot not in SUBSTRATES or slot not in SPECIES:
|
||||
continue
|
||||
count = len([s for s in SPECIES[slot].keys() if s != 63])
|
||||
if count > 0:
|
||||
print(f" Slot {slot:2d}: {count:2d} species = {count * 2:3d} tag IDs")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
print(f"\nValid tag IDs: 0-{MAX_VALID_TAG - 1}")
|
||||
print(f"Reserved: {MAX_VALID_TAG}-2114")
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == "--tables":
|
||||
print_species_tables()
|
||||
sys.exit(0)
|
||||
|
||||
if sys.argv[1] == "--stats":
|
||||
print_stats()
|
||||
sys.exit(0)
|
||||
|
||||
if sys.argv[1] == "--ranges":
|
||||
print_ranges()
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
tag_id = int(sys.argv[1])
|
||||
result = decode(tag_id)
|
||||
print(pretty(result))
|
||||
|
||||
if "--json" in sys.argv:
|
||||
print()
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,168 @@
|
||||
import os
|
||||
from bagtag_data import SUBSTRATES, SUBSTRATE_DETAILS, SPECIES_MASTER
|
||||
|
||||
# --- 1. Master Data Definitions ---
|
||||
# Imported from bagtag_data.py (single source of truth)
|
||||
|
||||
def generate_tables(output_dir="../tables/species"):
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
for sub_id, sub_name in SUBSTRATES.items():
|
||||
compatible_species = []
|
||||
|
||||
for spec in SPECIES_MASTER:
|
||||
# Combine optimal and permissive slots across all categories
|
||||
all_optimal = spec.get('optimal_aios', []) + spec.get('optimal_bulk', []) + spec.get('spawn_subs', [])
|
||||
all_permissive = spec.get('permissive_aios', []) + spec.get('permissive_bulk', [])
|
||||
|
||||
# Check if this species is compatible with the current substrate slot
|
||||
if sub_id in all_optimal or sub_id in all_permissive:
|
||||
|
||||
# Determine its affinity for this specific slot
|
||||
affinity = "Optimal" if sub_id in all_optimal else "Permissive"
|
||||
|
||||
# Add it to our list for this table
|
||||
spec_data = spec.copy()
|
||||
spec_data['affinity'] = affinity
|
||||
compatible_species.append(spec_data)
|
||||
|
||||
file_path = os.path.join(output_dir, f"{sub_name}.md")
|
||||
|
||||
with open(file_path, "w") as f:
|
||||
f.write(f"# Species Table: {sub_name.upper()}\n\n")
|
||||
f.write(f"**Substrate Slot:** {sub_id}\n\n")
|
||||
f.write("| Bit Code | Common Name | Scientific Name | Vision Morphology | Affinity |\n")
|
||||
f.write("|:---:|:---|:---|:---|:---|\n")
|
||||
|
||||
# Write the compatible species
|
||||
for spec in compatible_species:
|
||||
# Clean up the morphology string for reading (e.g., "scaly_cap" -> "Scaly Cap")
|
||||
morph = spec.get('morphology', 'unknown').replace('_', ' ').title()
|
||||
|
||||
f.write(f"| {spec['id']} | {spec['name']} | *{spec['sci']}* | {morph} | {spec['affinity']} |\n")
|
||||
|
||||
# Add the Reserved buffer and Other/prompt at the bottom
|
||||
f.write("| 104-126 | Reserved | — | — | Reserved |\n")
|
||||
f.write("| 127 | Other/prompt | — | — | — |\n")
|
||||
|
||||
print(f"Generated: {file_path}")
|
||||
|
||||
|
||||
def generate_substrates_md(output_path="../tables/substrates.md"):
|
||||
"""Generate the substrates.md documentation from SUBSTRATE_DETAILS."""
|
||||
|
||||
# Group by category
|
||||
categories = {}
|
||||
for slot, details in SUBSTRATE_DETAILS.items():
|
||||
cat = details.get('category', 'Other')
|
||||
if cat not in categories:
|
||||
categories[cat] = []
|
||||
categories[cat].append((slot, details))
|
||||
|
||||
# Order categories
|
||||
category_order = ['AIO Standard', 'AIO Advanced', 'Grain Spawn', 'Bulk', 'Other']
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
f.write("# BagTag Substrate Slots\n\n")
|
||||
f.write("**Bits 1–5 · 32 slots · Table version 1.0**\n\n")
|
||||
f.write("The substrate slot is the most consequential field in the encoding. It determines the species lookup table, the Cultivar's baseline grow profile parameters, the biological efficiency baseline, and the workflow type.\n\n")
|
||||
f.write("---\n\n")
|
||||
|
||||
for cat in category_order:
|
||||
if cat not in categories:
|
||||
continue
|
||||
|
||||
# Map category to header slug
|
||||
slug = cat.lower().replace(' ', '-')
|
||||
f.write(f"## {cat} (Slots {categories[cat][0][0]}–{categories[cat][-1][0]})\n\n")
|
||||
|
||||
# Check if AIO type (has bulk + grain) or spawn/bulk type
|
||||
is_aio = cat in ['AIO Standard', 'AIO Advanced']
|
||||
|
||||
if is_aio:
|
||||
f.write("| Slot | Bulk Layer | Grain Layer | Character |\n")
|
||||
f.write("|------|-----------|-------------|-----------|\n")
|
||||
else:
|
||||
f.write("| Slot | Type | Character |\n")
|
||||
f.write("|------|------|-----------|\n")
|
||||
|
||||
for slot, details in sorted(categories[cat]):
|
||||
bulk = details.get('bulk', '—')
|
||||
grain = details.get('grain', '—')
|
||||
character = details.get('character', '')
|
||||
label = details.get('label', '')
|
||||
|
||||
if is_aio:
|
||||
f.write(f"| {slot} | {bulk} | {grain} | {character} |\n")
|
||||
else:
|
||||
f.write(f"| {slot} | {label} | {character} |\n")
|
||||
|
||||
f.write("\n---\n\n")
|
||||
|
||||
# Add notes section
|
||||
f.write("## Notes\n\n")
|
||||
f.write("- **AIO (All-In-One)**: Bags contain both sterile grain spawn layer and sterile bulk substrate layer. No mixing required by user.\n")
|
||||
f.write("- **Spawn**: Grain spawn only. User provides their own bulk substrate.\n")
|
||||
f.write("- **Bulk**: Bulk substrate only. User provides their own spawn.\n")
|
||||
f.write("- **Custom slots (14, 23, 31)**: Reserved for user-defined substrates. Species code 127 triggers manual configuration in Cultivar.\n")
|
||||
|
||||
print(f"Generated: {output_path}")
|
||||
|
||||
def generate_master_table(output_path="../BAGTAG_MASTER_TABLE.md"):
|
||||
"""Generate comprehensive master table of all tag IDs and their meanings."""
|
||||
|
||||
# Import decode functions
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from decode import TAG_DECODE_TABLE, MAX_VALID_TAG, SUBSTRATES
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
f.write("# BagTag Master Reference Table\n\n")
|
||||
f.write("**Complete listing of all valid BagTag IDs (0–1905)**\n\n")
|
||||
f.write("Generated from `tools/bagtag_data.py` — single source of truth.\n\n")
|
||||
f.write("---\n\n")
|
||||
f.write("## Summary\n\n")
|
||||
f.write(f"- **Valid tag IDs:** 0–{MAX_VALID_TAG - 1}\n")
|
||||
f.write(f"- **Reserved:** {MAX_VALID_TAG}–2114\n")
|
||||
f.write(f"- **Total valid bags:** {len(TAG_DECODE_TABLE)}\n")
|
||||
f.write("- **Encoding:** Packed enumeration (size, substrate, species)\n\n")
|
||||
f.write("---\n\n")
|
||||
f.write("## Tag ID Reference\n\n")
|
||||
f.write("| Tag ID | Size | Substrate Slot | Substrate Name | Species ID | Species Name |\n")
|
||||
f.write("|--------|------|----------------|----------------|------------|--------------|\n")
|
||||
|
||||
for tag_id in range(MAX_VALID_TAG):
|
||||
if tag_id not in TAG_DECODE_TABLE:
|
||||
continue
|
||||
|
||||
entry = TAG_DECODE_TABLE[tag_id]
|
||||
size = "small" if entry[1] == 0 else "large"
|
||||
sub_slot = entry[2]
|
||||
species_id = entry[3]
|
||||
species_name = entry[4]
|
||||
|
||||
sub = SUBSTRATES.get(sub_slot, {})
|
||||
sub_name = sub.get('label', f'Unknown (slot {sub_slot})')
|
||||
|
||||
f.write(f"| {tag_id} | {size} | {sub_slot} | {sub_name} | {species_id} | {species_name} |\n")
|
||||
|
||||
f.write("\n---\n\n")
|
||||
f.write("## Legend\n\n")
|
||||
f.write("- **Size:** `small` = 3lb, `large` = 5lb (configurable in Cultivar firmware)\n")
|
||||
f.write("- **Substrate Slot:** 0–31, selects species lookup table\n")
|
||||
f.write("- **Species ID:** 0–103 = specific species, 127 = Other/prompt (manual entry)\n")
|
||||
f.write("- **Tag ID:** Sequential index into packed enumeration\n\n")
|
||||
f.write("---\n\n")
|
||||
f.write("## Generation\n\n")
|
||||
f.write("```bash\n")
|
||||
f.write("python tools/generate_tables.py\n")
|
||||
f.write("```\n")
|
||||
|
||||
print(f"Generated: {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_tables()
|
||||
generate_substrates_md()
|
||||
generate_master_table()
|
||||
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
BagTag Marker Generator
|
||||
|
||||
Generates PNG images for BagTag AprilTag41h12 markers.
|
||||
Requires: apriltag Python package (pip install apriltag)
|
||||
|
||||
Usage:
|
||||
python generate_tags.py --tag-id 0 --output bagtag_0.png
|
||||
python generate_tags.py --range 0 100 --output-dir ../tags/
|
||||
python generate_tags.py --all --output-dir ../tags/ --scale 10
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("Error: numpy and Pillow required. Install with: pip install numpy Pillow")
|
||||
sys.exit(1)
|
||||
|
||||
# BagTag valid tag range
|
||||
BAGTAG_MIN = 0
|
||||
BAGTAG_MAX = 1905 # From decode.py MAX_VALID_TAG - 1
|
||||
|
||||
|
||||
def generate_tag_image(tag_id: int, scale: int = 1, border_bits: int = 1) -> Image.Image:
|
||||
"""Generate a BagTag AprilTag41h12 marker image.
|
||||
|
||||
Uses a simple bit pattern generation for the 41h12 family.
|
||||
For production use, this should use the official AprilTag library.
|
||||
"""
|
||||
# AprilTag 41h12 parameters
|
||||
# Total size: 9x9 data bits + 1 bit black border on all sides = 11x11
|
||||
# With the standard 2-bit white quiet zone outside = 13x13 total
|
||||
|
||||
# For now, generate a placeholder that indicates the tag ID
|
||||
# In production, this would encode the actual 41h12 bit pattern
|
||||
|
||||
size = 11 * scale # Base tag size (9 data + 2 border)
|
||||
|
||||
# Create white background
|
||||
img = Image.new('L', (size, size), 255)
|
||||
pixels = img.load()
|
||||
|
||||
# Draw black border (inner border of AprilTag)
|
||||
for x in range(scale, size - scale):
|
||||
for y in range(scale, size - scale):
|
||||
# Simple placeholder pattern - black inner area with tag ID encoded
|
||||
# In real implementation, this would be the actual 41h12 encoding
|
||||
if (x // scale + y // scale) % 2 == 0:
|
||||
pixels[x, y] = 0
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def generate_tag_with_apriltag(tag_id: int, scale: int = 10) -> Image.Image:
|
||||
"""Generate tag using apriltag library if available."""
|
||||
try:
|
||||
from apriltag import apriltag
|
||||
|
||||
# Create detector to get tag pattern (workaround for generation)
|
||||
# For actual generation, we need to use the C library or pre-rendered images
|
||||
# This is a placeholder implementation
|
||||
|
||||
# Generate checkerboard pattern representing the tag
|
||||
size = 11 * scale # 9 data + 2 border
|
||||
img = np.ones((size, size), dtype=np.uint8) * 255
|
||||
|
||||
# Draw black border
|
||||
img[scale:size-scale, scale:size-scale] = 0
|
||||
|
||||
# Encode tag_id in center (simplified)
|
||||
center_start = 4 * scale
|
||||
center_end = 7 * scale
|
||||
for i in range(center_start, center_end):
|
||||
for j in range(center_start, center_end):
|
||||
if (i + j + tag_id) % 2 == 0:
|
||||
img[i, j] = 255
|
||||
else:
|
||||
img[i, j] = 0
|
||||
|
||||
return Image.fromarray(img)
|
||||
|
||||
except ImportError:
|
||||
return generate_tag_image(tag_id, scale)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate BagTag AprilTag41h12 marker images"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tag-id", type=int,
|
||||
help="Generate specific tag ID (0-1905)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--range", nargs=2, type=int, metavar=("START", "END"),
|
||||
help="Generate range of tag IDs (inclusive)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all", action="store_true",
|
||||
help="Generate all valid BagTag IDs (0-1905)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scale", type=int, default=10,
|
||||
help="Scale factor for output image (default: 10)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o", type=str,
|
||||
help="Output filename (for single tag)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir", "-d", type=str, default="../tags/",
|
||||
help="Output directory for generated tags"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate arguments
|
||||
if sum([bool(args.tag_id is not None), bool(args.range), bool(args.all)]) != 1:
|
||||
parser.error("Specify exactly one of: --tag-id, --range, or --all")
|
||||
|
||||
# Create output directory
|
||||
if args.output_dir:
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
|
||||
# Determine tag IDs to generate
|
||||
if args.tag_id is not None:
|
||||
if not (BAGTAG_MIN <= args.tag_id <= BAGTAG_MAX):
|
||||
parser.error(f"Tag ID must be between {BAGTAG_MIN} and {BAGTAG_MAX}")
|
||||
tag_ids = [args.tag_id]
|
||||
elif args.range:
|
||||
start, end = args.range
|
||||
if start > end or start < BAGTAG_MIN or end > BAGTAG_MAX:
|
||||
parser.error(f"Invalid range. Must be within {BAGTAG_MIN}-{BAGTAG_MAX}")
|
||||
tag_ids = list(range(start, end + 1))
|
||||
else: # --all
|
||||
tag_ids = list(range(BAGTAG_MIN, BAGTAG_MAX + 1))
|
||||
|
||||
print(f"Generating {len(tag_ids)} BagTag markers (scale={args.scale}x)...")
|
||||
|
||||
for tag_id in tag_ids:
|
||||
img = generate_tag_with_apriltag(tag_id, args.scale)
|
||||
|
||||
if args.output and len(tag_ids) == 1:
|
||||
output_path = args.output
|
||||
else:
|
||||
output_path = os.path.join(
|
||||
args.output_dir,
|
||||
f"bagtag_{tag_id:04d}.png"
|
||||
)
|
||||
|
||||
img.save(output_path)
|
||||
print(f" Generated: {output_path}")
|
||||
|
||||
print(f"\nDone! Generated {len(tag_ids)} tags.")
|
||||
print(f"\nNote: These are placeholder patterns. For production tags,")
|
||||
print(f"use the official AprilTag library or pre-generated 41h12 images.")
|
||||
print(f"See: https://github.com/AprilRobotics/apriltag-imgs")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user