modm_data.html.stmicro.datasheet

  1# Copyright 2022, Niklas Hauser
  2# SPDX-License-Identifier: MPL-2.0
  3
  4import re
  5import itertools
  6from pathlib import Path
  7from functools import cached_property
  8from collections import defaultdict
  9
 10from .helper import split_device_filter, split_package
 11from ...html.text import ReDict
 12from ...owl import DeviceIdentifier
 13from ...owl.stmicro import did_from_string
 14import modm_data.html as html
 15
 16
 17class DatasheetMicro(html.Document):
 18    def __init__(self, path: str):
 19        super().__init__(path)
 20        self._id = {}
 21        self._devices = {}
 22
 23    def __repr__(self) -> str:
 24        return f"DSµC({self.fullname})"
 25
 26    @cached_property
 27    def device_family(self) -> str:
 28        return re.search(r"STM32\w\w", self.chapter("chapter 0").headings()[0].text(br=" ")).group(0)
 29
 30    @cached_property
 31    def devices(self) -> list[DeviceIdentifier]:
 32        # Find the device summary table or use the heading
 33        chapter = self.chapter("chapter 0")
 34        ndevices = []
 35        if tables := chapter.tables("Device summary"):
 36            for row in tables[0].cell_rows("number"):
 37                ndevices.extend(c.text(br=" ", sup=" ").replace(",", " ").strip()
 38                               for cells in row.values() for c in cells)
 39            ndevices = [d for dev in ndevices for d in dev.split(" ") if "STM32" in d]
 40        else:
 41            # Check all uncaptioned tables for "Product summary" domain
 42            for table in chapter.tables():
 43                if table.domains_x("Product +summary"):
 44                    for row in table.cell_rows():
 45                        for cells in row.values():
 46                            ndevices.extend(html.listify(cells[-1].text(br=" ", sup=" ")))
 47                    break
 48            else:
 49                # print(chapter._path.relative_to(Path().cwd()))
 50                ndevices = chapter.headings()[0].text(br=" ").replace(",", " ").strip()
 51                ndevices = [d.replace("-", "xx") for d in ndevices.split(" ") if "STM32" in d]
 52        # print(ndevices)
 53
 54        # Split combo strings into individual devices
 55        devices = []
 56        for device in ndevices:
 57            device = device.replace("STM32479", "STM32F479").replace("STM32469", "STM32F469")
 58            if not re.match(r"STM32[A-Z][0-57BL][0-9ABCERSPQ][0-9AB]", device):
 59                raise ValueError(f"Filter ID '{device}' does not match schema!")
 60            devices.extend(split_device_filter(device))
 61
 62        # Find the Ordering Information to complete the identifiers (x=?)
 63        did = defaultdict(dict)
 64        current = ""
 65        chapter = self.chapter("ordering|numbering")
 66        for heading, texts in chapter.heading_texts("ordering|numbering"):
 67            for text in texts:
 68                for para in text.text(br="\n").split("\n"):
 69                    if "=" in para or ":" in para:
 70                        key, value = para.split("=" if "=" in para else ":", 1)
 71                        did[current][key.strip()] = value.strip()
 72                    else:
 73                        current = para
 74        self._id = did = ReDict(did)
 75        # print(did)
 76        did_map = [
 77            ("count", "pin"),
 78            ("size", "size"),
 79            ("package", "package"),
 80            ("temperature", "temperature"),
 81            ("option|identification|dedicated +pinout|power pairs|specificity|packing", "variant"),
 82        ]
 83        sdids = [(dname, {}) for dname in set(dev[:9] for dev in devices)]
 84        for pattern, name in did_map:
 85            if keys := did.match_keys(pattern):
 86                new_ids = []
 87                if name == "variant":
 88                    for oid in sdids:
 89                        new_ids.append((oid[0], oid[1] | {f"{name}": "normal"}))
 90                for value, descr in did[keys[0]].items():
 91                    value = re.sub(r"\(\d\)", "", value)
 92                    if len(value) > 1:
 93                        if name == "variant" and (match := re.search(r"x += +([NA])", descr)):
 94                            # We need to specially deal with the -N variants
 95                            value = match.group(1)
 96                        else:
 97                            value = ""
 98                            if name == "variant":
 99                                continue
100                    for oid in sdids:
101                        new_ids.append((oid[0] + value, oid[1] | {f"{name}": descr}))
102                sdids = new_ids
103            elif name != "variant":
104                raise KeyError(f"Cannot find '{pattern}' in {did.keys()}!")
105
106        # Some datasheets have the specific table at the very end
107        if tables := chapter.tables("order codes"):
108            for row in tables[0].cell_rows("code"):
109                for cells in row.values():
110                    for cell in cells:
111                        devices.extend(html.listify(cell.text()))
112
113        # convert to proper device identifiers
114        dids = set()
115        for string, descr in sdids:
116            did = did_from_string(string)
117            # print(did)
118            # filter through the device strings:
119            for device in devices:
120                if re.match(device.replace("x", ".").lower(), did.string):
121                    dids.add(did)
122                    break
123
124        # print(devices)
125        if not dids:
126            raise ValueError(f"Could not find devices for {self.fullname}!")
127        return list(dids)
128
129    @property
130    def _chapter_pins(self):
131        return self.chapter("pin description|pinout")
132
133    @property
134    def _table_pinout(self):
135        tables = self._chapter_pins.tables(
136                "pin( +and +ball|/ball| +assignment +and)? +(definition|description)", br=" ")
137        if not tables:
138            raise ValueError(f"Unable to find pin definition table in chapter! {self._chapter_pins._relpath}")
139        return tables[0]
140
141    @property
142    def _tables_alternate_functions(self):
143        tables = self._chapter_pins.tables("alternate +function", br=" ")
144        if not tables and "STM32F1" not in self.device_family:
145            raise ValueError(f"Unable to find alternate function table in chapter! {self._chapter_pins._relpath}")
146        return tables
147
148    @cached_property
149    def packages(self):
150        domains = self._table_pinout.domains_x(r"num(<br>)?ber|pins?:|pin/ball +name|:WLCSP.*?", br="<br>")
151        if not domains:
152            raise ValueError(f"Unable to find package domains in table! {self._chapter_pins._relpath} {tables[0].caption()}")
153        packages = []
154        for domain in domains:
155            if domain == "Pin (UFQFPN48):Number": continue
156            ndpackage = domain.split(":")[-1].replace("UFBGA/TFBGA64", "UFBGA64/TFBGA64")
157            ndpackage = ndpackage.replace("LPQF", "LQFP").replace("TSSPOP", "TSSOP")
158            ndpackage = ndpackage.replace("UFBG100", "UFBGA100").replace("UBGA", "UFBGA")
159            ndpackage = ndpackage.replace("UQFN", "UFQFPN").replace("WLCSP20L", "WLCSP20")
160            ndpackage = ndpackage.replace("UFQFPN48E", "UFQFPN48+E").replace("UFQFN", "UFQFPN")
161            ndpackage = ndpackage.replace("LQFP48 SMPS<br>UFQFPN48 SMPS", "LQFP48/UFQFPN48+SMPS")
162            ndpackage = ndpackage.replace("LQFP<br>64", "LQFP64").replace("LQFP<br>48", "LQFP48")
163            ndpackage = ndpackage.replace("UFQFPN<br>32", "UFQFPN32").replace("UFQFP<br>N48", "UFQFPN48")
164            ndpackage = ndpackage.replace("WLCSP<br>25", "WLCSP25")
165            ndpackage = re.sub(r"^BGA", "UFBGA", ndpackage)
166            ndpackage = re.sub(r"\(\d\)| ", "", ndpackage)
167            ndpackage = re.sub(r"[Ee]xt-?/?", "", ndpackage)
168            ndpackage = re.sub(r"<br>SMPS", "SMPS", ndpackage)
169            ndpackage = re.sub(r"SMSP", "SMPS", ndpackage)
170            ndpackage = re.sub(r"with|-|_", "+", ndpackage)
171            ndpackage = re.sub(r"or", "/", ndpackage)
172            ndpackage = re.sub(r"\(STM32L0.*?UxSonly\)", "+S", ndpackage)
173            ndpackage = re.sub(r"(\d+)SMPS", r"\1+SMPS", ndpackage)
174            ndpackage = re.sub(r"\+GP", "", ndpackage)
175            if "DS13311" in self.name or "DS13312" in self.name:
176                ndpackage = ndpackage.replace("+SMPS", "")
177            if (match := re.search(r":(STM32.*?):", domain)) is not None:
178                devs = html.listify(match.group(1))
179                ndpackage += "+" + "|".join(d.replace("x", ".") for d in devs)
180            ndpackage, *filters = ndpackage.split("+")
181            filters = ("+" + "+".join(filters)) if filters else ""
182            spackage = [p for p in re.split(r"[/,]| or |<br>", ndpackage) if p]
183            # print(domain, spackage)
184            for package in spackage:
185                if (pack := split_package(package)) is not None:
186                    packages.append((domain, package + filters, *pack))
187                else:
188                    print(f"Unknown package {package}!")
189        if not packages:
190            if "DS13259" in self.name:
191                packages = [("Pin:Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
192            elif "DS13047" in self.name:
193                packages = [("Pin (UFQFPN48):Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
194            else:
195                raise ValueError(f"Unable to find packages! {self._chapter_pins._relpath} {domains}")
196        return packages
197
198    @property
199    def packages_pins(self):
200        # Add pinouts and signals
201        pin_replace = {r"sup| |D?NC|/$|\(.*?\)|.*?connected.*": "", "–": "-", r"VREF_\+": "VREF+"}
202        add_replace = {r"[- ]|\(.*?\)|.*?selection.*|.*?reset.*": "",
203                       r"OPAMP": ",OPAMP", r"COMP": ",COMP", r"OPAMP,1": "OPAMP1"}
204        afs_replace = {r"[- ]|\(.*?\)": "", "LCD_SEG": ",LCD_SEG"}
205        pos_replace = {r'[“”\-"]|NC|\(.*?\)': ""}
206        sig_replace = {r"[- ]|\(.*?\)": "", r"(MOSI|SCK|NSS)I2S": r"\1,I2S", "_µM": "_M",
207                       r"(CH\d)TIM": r"\1,TIM", r"_([RT]XD\d?|DV|EN|CLK)ETH_": r"_\1,ETH_"}
208
209        data_packages = defaultdict(list)
210        data_pins = defaultdict(dict)
211
212        packages = set((d[0], d[1]) for d in self.packages)
213        # Import the pin definitions incl. additional function
214        for row in self._table_pinout.cell_rows(br="<br>"):
215            pin_name = row.match_value("pin +name|:name")[0].text(**pin_replace).strip()
216            if not pin_name: continue
217            ios = row.match_value("I ?/ ?O")[0].text(**{"-":""})
218            ptype = row.match_value("type")[0].text()
219            # Hack to make fix the PD0/PD1 pins
220            if pin_name.startswith("OSC") and (remap := row.match_value("remap")):
221                if (pin := remap[0].text()).startswith("P"):
222                    pin_name = f"{pin}-{pin_name}"
223
224            data_pin = data_pins[pin_name]
225            if ios: data_pin["structure"] = ios
226            if ptype: data_pin["type"] = ptype
227            if ptype == "I/O" and "STM32F1" not in self.device_family:
228                signals = html.listify(row.match_value("additional")[0].text(**add_replace))
229                data_pin["additional"] = set(signals)
230
231            for domain, package_name in packages:
232                if ppos := html.listify(row[domain][0].text(**pos_replace)):
233                    data_packages[package_name].append( (pin_name, ppos) )
234
235        # Import the alternate functions
236        for table in self._tables_alternate_functions:
237            cells = table.domains("port|pin +name").cells(r"AF(IO)?\d+")
238            for pin, afs in cells.items():
239                name = html.replace(pin.split(":")[-1], **pin_replace)
240                data_pin = data_pins[name]
241                if "alternate" not in data_pin:
242                    data_pin["alternate"] = defaultdict(list)
243                for af, csignals in afs.items():
244                    af = int(re.search(r"AF(IO)?(\d{1,2})", af).group(2))
245                    for csignal in csignals:
246                        signals = html.listify(csignal.text(**sig_replace))
247                        data_pin["alternate"][af].extend(signals)
248
249        return data_packages, data_pins
250
251
252class DatasheetSensor(html.Document):
253    def __init__(self, path: str):
254        super().__init__(path)
255
256    def __repr__(self) -> str:
257        return f"DSsens({self.fullname})"
class DatasheetMicro(modm_data.html.document.Document):
 18class DatasheetMicro(html.Document):
 19    def __init__(self, path: str):
 20        super().__init__(path)
 21        self._id = {}
 22        self._devices = {}
 23
 24    def __repr__(self) -> str:
 25        return f"DSµC({self.fullname})"
 26
 27    @cached_property
 28    def device_family(self) -> str:
 29        return re.search(r"STM32\w\w", self.chapter("chapter 0").headings()[0].text(br=" ")).group(0)
 30
 31    @cached_property
 32    def devices(self) -> list[DeviceIdentifier]:
 33        # Find the device summary table or use the heading
 34        chapter = self.chapter("chapter 0")
 35        ndevices = []
 36        if tables := chapter.tables("Device summary"):
 37            for row in tables[0].cell_rows("number"):
 38                ndevices.extend(c.text(br=" ", sup=" ").replace(",", " ").strip()
 39                               for cells in row.values() for c in cells)
 40            ndevices = [d for dev in ndevices for d in dev.split(" ") if "STM32" in d]
 41        else:
 42            # Check all uncaptioned tables for "Product summary" domain
 43            for table in chapter.tables():
 44                if table.domains_x("Product +summary"):
 45                    for row in table.cell_rows():
 46                        for cells in row.values():
 47                            ndevices.extend(html.listify(cells[-1].text(br=" ", sup=" ")))
 48                    break
 49            else:
 50                # print(chapter._path.relative_to(Path().cwd()))
 51                ndevices = chapter.headings()[0].text(br=" ").replace(",", " ").strip()
 52                ndevices = [d.replace("-", "xx") for d in ndevices.split(" ") if "STM32" in d]
 53        # print(ndevices)
 54
 55        # Split combo strings into individual devices
 56        devices = []
 57        for device in ndevices:
 58            device = device.replace("STM32479", "STM32F479").replace("STM32469", "STM32F469")
 59            if not re.match(r"STM32[A-Z][0-57BL][0-9ABCERSPQ][0-9AB]", device):
 60                raise ValueError(f"Filter ID '{device}' does not match schema!")
 61            devices.extend(split_device_filter(device))
 62
 63        # Find the Ordering Information to complete the identifiers (x=?)
 64        did = defaultdict(dict)
 65        current = ""
 66        chapter = self.chapter("ordering|numbering")
 67        for heading, texts in chapter.heading_texts("ordering|numbering"):
 68            for text in texts:
 69                for para in text.text(br="\n").split("\n"):
 70                    if "=" in para or ":" in para:
 71                        key, value = para.split("=" if "=" in para else ":", 1)
 72                        did[current][key.strip()] = value.strip()
 73                    else:
 74                        current = para
 75        self._id = did = ReDict(did)
 76        # print(did)
 77        did_map = [
 78            ("count", "pin"),
 79            ("size", "size"),
 80            ("package", "package"),
 81            ("temperature", "temperature"),
 82            ("option|identification|dedicated +pinout|power pairs|specificity|packing", "variant"),
 83        ]
 84        sdids = [(dname, {}) for dname in set(dev[:9] for dev in devices)]
 85        for pattern, name in did_map:
 86            if keys := did.match_keys(pattern):
 87                new_ids = []
 88                if name == "variant":
 89                    for oid in sdids:
 90                        new_ids.append((oid[0], oid[1] | {f"{name}": "normal"}))
 91                for value, descr in did[keys[0]].items():
 92                    value = re.sub(r"\(\d\)", "", value)
 93                    if len(value) > 1:
 94                        if name == "variant" and (match := re.search(r"x += +([NA])", descr)):
 95                            # We need to specially deal with the -N variants
 96                            value = match.group(1)
 97                        else:
 98                            value = ""
 99                            if name == "variant":
100                                continue
101                    for oid in sdids:
102                        new_ids.append((oid[0] + value, oid[1] | {f"{name}": descr}))
103                sdids = new_ids
104            elif name != "variant":
105                raise KeyError(f"Cannot find '{pattern}' in {did.keys()}!")
106
107        # Some datasheets have the specific table at the very end
108        if tables := chapter.tables("order codes"):
109            for row in tables[0].cell_rows("code"):
110                for cells in row.values():
111                    for cell in cells:
112                        devices.extend(html.listify(cell.text()))
113
114        # convert to proper device identifiers
115        dids = set()
116        for string, descr in sdids:
117            did = did_from_string(string)
118            # print(did)
119            # filter through the device strings:
120            for device in devices:
121                if re.match(device.replace("x", ".").lower(), did.string):
122                    dids.add(did)
123                    break
124
125        # print(devices)
126        if not dids:
127            raise ValueError(f"Could not find devices for {self.fullname}!")
128        return list(dids)
129
130    @property
131    def _chapter_pins(self):
132        return self.chapter("pin description|pinout")
133
134    @property
135    def _table_pinout(self):
136        tables = self._chapter_pins.tables(
137                "pin( +and +ball|/ball| +assignment +and)? +(definition|description)", br=" ")
138        if not tables:
139            raise ValueError(f"Unable to find pin definition table in chapter! {self._chapter_pins._relpath}")
140        return tables[0]
141
142    @property
143    def _tables_alternate_functions(self):
144        tables = self._chapter_pins.tables("alternate +function", br=" ")
145        if not tables and "STM32F1" not in self.device_family:
146            raise ValueError(f"Unable to find alternate function table in chapter! {self._chapter_pins._relpath}")
147        return tables
148
149    @cached_property
150    def packages(self):
151        domains = self._table_pinout.domains_x(r"num(<br>)?ber|pins?:|pin/ball +name|:WLCSP.*?", br="<br>")
152        if not domains:
153            raise ValueError(f"Unable to find package domains in table! {self._chapter_pins._relpath} {tables[0].caption()}")
154        packages = []
155        for domain in domains:
156            if domain == "Pin (UFQFPN48):Number": continue
157            ndpackage = domain.split(":")[-1].replace("UFBGA/TFBGA64", "UFBGA64/TFBGA64")
158            ndpackage = ndpackage.replace("LPQF", "LQFP").replace("TSSPOP", "TSSOP")
159            ndpackage = ndpackage.replace("UFBG100", "UFBGA100").replace("UBGA", "UFBGA")
160            ndpackage = ndpackage.replace("UQFN", "UFQFPN").replace("WLCSP20L", "WLCSP20")
161            ndpackage = ndpackage.replace("UFQFPN48E", "UFQFPN48+E").replace("UFQFN", "UFQFPN")
162            ndpackage = ndpackage.replace("LQFP48 SMPS<br>UFQFPN48 SMPS", "LQFP48/UFQFPN48+SMPS")
163            ndpackage = ndpackage.replace("LQFP<br>64", "LQFP64").replace("LQFP<br>48", "LQFP48")
164            ndpackage = ndpackage.replace("UFQFPN<br>32", "UFQFPN32").replace("UFQFP<br>N48", "UFQFPN48")
165            ndpackage = ndpackage.replace("WLCSP<br>25", "WLCSP25")
166            ndpackage = re.sub(r"^BGA", "UFBGA", ndpackage)
167            ndpackage = re.sub(r"\(\d\)| ", "", ndpackage)
168            ndpackage = re.sub(r"[Ee]xt-?/?", "", ndpackage)
169            ndpackage = re.sub(r"<br>SMPS", "SMPS", ndpackage)
170            ndpackage = re.sub(r"SMSP", "SMPS", ndpackage)
171            ndpackage = re.sub(r"with|-|_", "+", ndpackage)
172            ndpackage = re.sub(r"or", "/", ndpackage)
173            ndpackage = re.sub(r"\(STM32L0.*?UxSonly\)", "+S", ndpackage)
174            ndpackage = re.sub(r"(\d+)SMPS", r"\1+SMPS", ndpackage)
175            ndpackage = re.sub(r"\+GP", "", ndpackage)
176            if "DS13311" in self.name or "DS13312" in self.name:
177                ndpackage = ndpackage.replace("+SMPS", "")
178            if (match := re.search(r":(STM32.*?):", domain)) is not None:
179                devs = html.listify(match.group(1))
180                ndpackage += "+" + "|".join(d.replace("x", ".") for d in devs)
181            ndpackage, *filters = ndpackage.split("+")
182            filters = ("+" + "+".join(filters)) if filters else ""
183            spackage = [p for p in re.split(r"[/,]| or |<br>", ndpackage) if p]
184            # print(domain, spackage)
185            for package in spackage:
186                if (pack := split_package(package)) is not None:
187                    packages.append((domain, package + filters, *pack))
188                else:
189                    print(f"Unknown package {package}!")
190        if not packages:
191            if "DS13259" in self.name:
192                packages = [("Pin:Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
193            elif "DS13047" in self.name:
194                packages = [("Pin (UFQFPN48):Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
195            else:
196                raise ValueError(f"Unable to find packages! {self._chapter_pins._relpath} {domains}")
197        return packages
198
199    @property
200    def packages_pins(self):
201        # Add pinouts and signals
202        pin_replace = {r"sup| |D?NC|/$|\(.*?\)|.*?connected.*": "", "–": "-", r"VREF_\+": "VREF+"}
203        add_replace = {r"[- ]|\(.*?\)|.*?selection.*|.*?reset.*": "",
204                       r"OPAMP": ",OPAMP", r"COMP": ",COMP", r"OPAMP,1": "OPAMP1"}
205        afs_replace = {r"[- ]|\(.*?\)": "", "LCD_SEG": ",LCD_SEG"}
206        pos_replace = {r'[“”\-"]|NC|\(.*?\)': ""}
207        sig_replace = {r"[- ]|\(.*?\)": "", r"(MOSI|SCK|NSS)I2S": r"\1,I2S", "_µM": "_M",
208                       r"(CH\d)TIM": r"\1,TIM", r"_([RT]XD\d?|DV|EN|CLK)ETH_": r"_\1,ETH_"}
209
210        data_packages = defaultdict(list)
211        data_pins = defaultdict(dict)
212
213        packages = set((d[0], d[1]) for d in self.packages)
214        # Import the pin definitions incl. additional function
215        for row in self._table_pinout.cell_rows(br="<br>"):
216            pin_name = row.match_value("pin +name|:name")[0].text(**pin_replace).strip()
217            if not pin_name: continue
218            ios = row.match_value("I ?/ ?O")[0].text(**{"-":""})
219            ptype = row.match_value("type")[0].text()
220            # Hack to make fix the PD0/PD1 pins
221            if pin_name.startswith("OSC") and (remap := row.match_value("remap")):
222                if (pin := remap[0].text()).startswith("P"):
223                    pin_name = f"{pin}-{pin_name}"
224
225            data_pin = data_pins[pin_name]
226            if ios: data_pin["structure"] = ios
227            if ptype: data_pin["type"] = ptype
228            if ptype == "I/O" and "STM32F1" not in self.device_family:
229                signals = html.listify(row.match_value("additional")[0].text(**add_replace))
230                data_pin["additional"] = set(signals)
231
232            for domain, package_name in packages:
233                if ppos := html.listify(row[domain][0].text(**pos_replace)):
234                    data_packages[package_name].append( (pin_name, ppos) )
235
236        # Import the alternate functions
237        for table in self._tables_alternate_functions:
238            cells = table.domains("port|pin +name").cells(r"AF(IO)?\d+")
239            for pin, afs in cells.items():
240                name = html.replace(pin.split(":")[-1], **pin_replace)
241                data_pin = data_pins[name]
242                if "alternate" not in data_pin:
243                    data_pin["alternate"] = defaultdict(list)
244                for af, csignals in afs.items():
245                    af = int(re.search(r"AF(IO)?(\d{1,2})", af).group(2))
246                    for csignal in csignals:
247                        signals = html.listify(csignal.text(**sig_replace))
248                        data_pin["alternate"][af].extend(signals)
249
250        return data_packages, data_pins
DatasheetMicro(path: str)
19    def __init__(self, path: str):
20        super().__init__(path)
21        self._id = {}
22        self._devices = {}
device_family: str
27    @cached_property
28    def device_family(self) -> str:
29        return re.search(r"STM32\w\w", self.chapter("chapter 0").headings()[0].text(br=" ")).group(0)
devices: list[modm_data.owl.identifier.DeviceIdentifier]
 31    @cached_property
 32    def devices(self) -> list[DeviceIdentifier]:
 33        # Find the device summary table or use the heading
 34        chapter = self.chapter("chapter 0")
 35        ndevices = []
 36        if tables := chapter.tables("Device summary"):
 37            for row in tables[0].cell_rows("number"):
 38                ndevices.extend(c.text(br=" ", sup=" ").replace(",", " ").strip()
 39                               for cells in row.values() for c in cells)
 40            ndevices = [d for dev in ndevices for d in dev.split(" ") if "STM32" in d]
 41        else:
 42            # Check all uncaptioned tables for "Product summary" domain
 43            for table in chapter.tables():
 44                if table.domains_x("Product +summary"):
 45                    for row in table.cell_rows():
 46                        for cells in row.values():
 47                            ndevices.extend(html.listify(cells[-1].text(br=" ", sup=" ")))
 48                    break
 49            else:
 50                # print(chapter._path.relative_to(Path().cwd()))
 51                ndevices = chapter.headings()[0].text(br=" ").replace(",", " ").strip()
 52                ndevices = [d.replace("-", "xx") for d in ndevices.split(" ") if "STM32" in d]
 53        # print(ndevices)
 54
 55        # Split combo strings into individual devices
 56        devices = []
 57        for device in ndevices:
 58            device = device.replace("STM32479", "STM32F479").replace("STM32469", "STM32F469")
 59            if not re.match(r"STM32[A-Z][0-57BL][0-9ABCERSPQ][0-9AB]", device):
 60                raise ValueError(f"Filter ID '{device}' does not match schema!")
 61            devices.extend(split_device_filter(device))
 62
 63        # Find the Ordering Information to complete the identifiers (x=?)
 64        did = defaultdict(dict)
 65        current = ""
 66        chapter = self.chapter("ordering|numbering")
 67        for heading, texts in chapter.heading_texts("ordering|numbering"):
 68            for text in texts:
 69                for para in text.text(br="\n").split("\n"):
 70                    if "=" in para or ":" in para:
 71                        key, value = para.split("=" if "=" in para else ":", 1)
 72                        did[current][key.strip()] = value.strip()
 73                    else:
 74                        current = para
 75        self._id = did = ReDict(did)
 76        # print(did)
 77        did_map = [
 78            ("count", "pin"),
 79            ("size", "size"),
 80            ("package", "package"),
 81            ("temperature", "temperature"),
 82            ("option|identification|dedicated +pinout|power pairs|specificity|packing", "variant"),
 83        ]
 84        sdids = [(dname, {}) for dname in set(dev[:9] for dev in devices)]
 85        for pattern, name in did_map:
 86            if keys := did.match_keys(pattern):
 87                new_ids = []
 88                if name == "variant":
 89                    for oid in sdids:
 90                        new_ids.append((oid[0], oid[1] | {f"{name}": "normal"}))
 91                for value, descr in did[keys[0]].items():
 92                    value = re.sub(r"\(\d\)", "", value)
 93                    if len(value) > 1:
 94                        if name == "variant" and (match := re.search(r"x += +([NA])", descr)):
 95                            # We need to specially deal with the -N variants
 96                            value = match.group(1)
 97                        else:
 98                            value = ""
 99                            if name == "variant":
100                                continue
101                    for oid in sdids:
102                        new_ids.append((oid[0] + value, oid[1] | {f"{name}": descr}))
103                sdids = new_ids
104            elif name != "variant":
105                raise KeyError(f"Cannot find '{pattern}' in {did.keys()}!")
106
107        # Some datasheets have the specific table at the very end
108        if tables := chapter.tables("order codes"):
109            for row in tables[0].cell_rows("code"):
110                for cells in row.values():
111                    for cell in cells:
112                        devices.extend(html.listify(cell.text()))
113
114        # convert to proper device identifiers
115        dids = set()
116        for string, descr in sdids:
117            did = did_from_string(string)
118            # print(did)
119            # filter through the device strings:
120            for device in devices:
121                if re.match(device.replace("x", ".").lower(), did.string):
122                    dids.add(did)
123                    break
124
125        # print(devices)
126        if not dids:
127            raise ValueError(f"Could not find devices for {self.fullname}!")
128        return list(dids)
packages
149    @cached_property
150    def packages(self):
151        domains = self._table_pinout.domains_x(r"num(<br>)?ber|pins?:|pin/ball +name|:WLCSP.*?", br="<br>")
152        if not domains:
153            raise ValueError(f"Unable to find package domains in table! {self._chapter_pins._relpath} {tables[0].caption()}")
154        packages = []
155        for domain in domains:
156            if domain == "Pin (UFQFPN48):Number": continue
157            ndpackage = domain.split(":")[-1].replace("UFBGA/TFBGA64", "UFBGA64/TFBGA64")
158            ndpackage = ndpackage.replace("LPQF", "LQFP").replace("TSSPOP", "TSSOP")
159            ndpackage = ndpackage.replace("UFBG100", "UFBGA100").replace("UBGA", "UFBGA")
160            ndpackage = ndpackage.replace("UQFN", "UFQFPN").replace("WLCSP20L", "WLCSP20")
161            ndpackage = ndpackage.replace("UFQFPN48E", "UFQFPN48+E").replace("UFQFN", "UFQFPN")
162            ndpackage = ndpackage.replace("LQFP48 SMPS<br>UFQFPN48 SMPS", "LQFP48/UFQFPN48+SMPS")
163            ndpackage = ndpackage.replace("LQFP<br>64", "LQFP64").replace("LQFP<br>48", "LQFP48")
164            ndpackage = ndpackage.replace("UFQFPN<br>32", "UFQFPN32").replace("UFQFP<br>N48", "UFQFPN48")
165            ndpackage = ndpackage.replace("WLCSP<br>25", "WLCSP25")
166            ndpackage = re.sub(r"^BGA", "UFBGA", ndpackage)
167            ndpackage = re.sub(r"\(\d\)| ", "", ndpackage)
168            ndpackage = re.sub(r"[Ee]xt-?/?", "", ndpackage)
169            ndpackage = re.sub(r"<br>SMPS", "SMPS", ndpackage)
170            ndpackage = re.sub(r"SMSP", "SMPS", ndpackage)
171            ndpackage = re.sub(r"with|-|_", "+", ndpackage)
172            ndpackage = re.sub(r"or", "/", ndpackage)
173            ndpackage = re.sub(r"\(STM32L0.*?UxSonly\)", "+S", ndpackage)
174            ndpackage = re.sub(r"(\d+)SMPS", r"\1+SMPS", ndpackage)
175            ndpackage = re.sub(r"\+GP", "", ndpackage)
176            if "DS13311" in self.name or "DS13312" in self.name:
177                ndpackage = ndpackage.replace("+SMPS", "")
178            if (match := re.search(r":(STM32.*?):", domain)) is not None:
179                devs = html.listify(match.group(1))
180                ndpackage += "+" + "|".join(d.replace("x", ".") for d in devs)
181            ndpackage, *filters = ndpackage.split("+")
182            filters = ("+" + "+".join(filters)) if filters else ""
183            spackage = [p for p in re.split(r"[/,]| or |<br>", ndpackage) if p]
184            # print(domain, spackage)
185            for package in spackage:
186                if (pack := split_package(package)) is not None:
187                    packages.append((domain, package + filters, *pack))
188                else:
189                    print(f"Unknown package {package}!")
190        if not packages:
191            if "DS13259" in self.name:
192                packages = [("Pin:Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
193            elif "DS13047" in self.name:
194                packages = [("Pin (UFQFPN48):Number", "UFQFPN48", "UFQFPN48", "UFQFPN", 48)]
195            else:
196                raise ValueError(f"Unable to find packages! {self._chapter_pins._relpath} {domains}")
197        return packages
packages_pins
199    @property
200    def packages_pins(self):
201        # Add pinouts and signals
202        pin_replace = {r"sup| |D?NC|/$|\(.*?\)|.*?connected.*": "", "–": "-", r"VREF_\+": "VREF+"}
203        add_replace = {r"[- ]|\(.*?\)|.*?selection.*|.*?reset.*": "",
204                       r"OPAMP": ",OPAMP", r"COMP": ",COMP", r"OPAMP,1": "OPAMP1"}
205        afs_replace = {r"[- ]|\(.*?\)": "", "LCD_SEG": ",LCD_SEG"}
206        pos_replace = {r'[“”\-"]|NC|\(.*?\)': ""}
207        sig_replace = {r"[- ]|\(.*?\)": "", r"(MOSI|SCK|NSS)I2S": r"\1,I2S", "_µM": "_M",
208                       r"(CH\d)TIM": r"\1,TIM", r"_([RT]XD\d?|DV|EN|CLK)ETH_": r"_\1,ETH_"}
209
210        data_packages = defaultdict(list)
211        data_pins = defaultdict(dict)
212
213        packages = set((d[0], d[1]) for d in self.packages)
214        # Import the pin definitions incl. additional function
215        for row in self._table_pinout.cell_rows(br="<br>"):
216            pin_name = row.match_value("pin +name|:name")[0].text(**pin_replace).strip()
217            if not pin_name: continue
218            ios = row.match_value("I ?/ ?O")[0].text(**{"-":""})
219            ptype = row.match_value("type")[0].text()
220            # Hack to make fix the PD0/PD1 pins
221            if pin_name.startswith("OSC") and (remap := row.match_value("remap")):
222                if (pin := remap[0].text()).startswith("P"):
223                    pin_name = f"{pin}-{pin_name}"
224
225            data_pin = data_pins[pin_name]
226            if ios: data_pin["structure"] = ios
227            if ptype: data_pin["type"] = ptype
228            if ptype == "I/O" and "STM32F1" not in self.device_family:
229                signals = html.listify(row.match_value("additional")[0].text(**add_replace))
230                data_pin["additional"] = set(signals)
231
232            for domain, package_name in packages:
233                if ppos := html.listify(row[domain][0].text(**pos_replace)):
234                    data_packages[package_name].append( (pin_name, ppos) )
235
236        # Import the alternate functions
237        for table in self._tables_alternate_functions:
238            cells = table.domains("port|pin +name").cells(r"AF(IO)?\d+")
239            for pin, afs in cells.items():
240                name = html.replace(pin.split(":")[-1], **pin_replace)
241                data_pin = data_pins[name]
242                if "alternate" not in data_pin:
243                    data_pin["alternate"] = defaultdict(list)
244                for af, csignals in afs.items():
245                    af = int(re.search(r"AF(IO)?(\d{1,2})", af).group(2))
246                    for csignal in csignals:
247                        signals = html.listify(csignal.text(**sig_replace))
248                        data_pin["alternate"][af].extend(signals)
249
250        return data_packages, data_pins
class DatasheetSensor(modm_data.html.document.Document):
253class DatasheetSensor(html.Document):
254    def __init__(self, path: str):
255        super().__init__(path)
256
257    def __repr__(self) -> str:
258        return f"DSsens({self.fullname})"
DatasheetSensor(path: str)
254    def __init__(self, path: str):
255        super().__init__(path)