1#!/usr/bin/env python3 2 3"""Report all symbols and docs in decoders of rtl_433 as json.""" 4 5# from ../include/rtl_433_devices.h 6# DECL(silvercrest) \ 7# 8# static char *output_fields_EG53MA4[] = { 9# "model", 10# "type", 11# "id", 12# "flags", 13# "pressure_kPa", 14# "temperature_F", 15# "mic", 16# NULL, 17# }; 18# 19# r_device schraeder = { 20# .name = "Schrader TPMS", 21# .modulation = OOK_PULSE_MANCHESTER_ZEROBIT, 22# .short_width = 120, 23# .long_width = 0, 24# .sync_width = 0, 25# .gap_limit = 0, 26# .reset_limit = 480, 27# .decode_fn = &schraeder_callback, 28# .disabled = 0, 29# .fields = output_fields, 30# }; 31 32import sys 33import os 34from os import listdir 35from os.path import join, isfile, isdir, getsize 36import fnmatch 37import json 38import datetime 39import re 40 41errout = sys.stderr 42haserr = False 43 44 45def log(s): 46 print(s, file=errout) 47 48 49def err(s): 50 global haserr 51 haserr = True 52 print(s, file=errout) 53 54 55def process_protocols(path): 56 """Extract protocol numbers from a decl file.""" 57 protocols = [] 58 59 with open(path, encoding='utf-8', errors='replace') as f: 60 for line in f.readlines(): 61 # DECL(prologue) 62 m = re.match(r'\s*DECL\s*\(\s*([^\)]*)\s*\)', line) 63 if m: 64 pName = m.group(1) 65 protocols.append(pName) 66 67 return protocols 68 69 70def update_links(links, rName, name, i, key, param): 71 if not rName: 72 err(f"::error file={name},line={i}::Key without r_device ({key}: {param})") 73 links[rName].update({key: param}) 74 75 76def process_source(path, name): 77 """Extract symbols and documentation from a decoder file.""" 78 links = {} 79 links[name] = {"src": name, "line": 1, "type": "file"} 80 with open(join(path, name), encoding='utf-8', errors='replace') as f: 81 fName = None 82 fLine = None 83 rName = None 84 captureDoc = False 85 fileDoc = False 86 dLine = None 87 dSee = None 88 doc = None 89 for i, line in enumerate(f): 90 # look for documentation comments: 91 # /** @file ... */ 92 # /** @fn ... */ 93 m = re.match(r'\s*\*/', line) 94 if captureDoc and m: 95 captureDoc = False 96 if fileDoc: 97 links[name].update({"doc_line": dLine, "doc": doc}) 98 fileDoc = False 99 doc = None 100 if fName: 101 if fName not in links: 102 links[fName] = {"src": name, "type": "func"} 103 links[fName].update({"doc_line": dLine, "doc": doc}) 104 doc = None 105 fName = None 106 continue 107 if captureDoc: 108 doc += line 109 m = re.match(r'\s*\@sa\s+(.*?)\(\)\s*', line) 110 if m: 111 dSee = m.group(1) 112 continue 113 # inline link /** @sa func() */ 114 m = re.match(r'\s*/\*\*\s*\@sa\s+(.*?)\(\)\s*\*/', line) 115 if m: 116 dLine = i + 1 117 dSee = m.group(1) 118 continue 119 # inline /** ... */ 120 m = re.match(r'\s*/\*\*\s*(.*?)\s*\*/', line) 121 if m: 122 dLine = i + 1 123 doc = m.group(1) 124 continue 125 # copyright /** @file ... */ 126 m = re.match(r'\s*/\*\*\s*@file', line) 127 if m: 128 captureDoc = True 129 fileDoc = True 130 dLine = i + 1 131 doc = '' 132 continue 133 # /** @fn ... */ 134 m = re.match( 135 r'\s*/\*\*\s*@fn\s+(?:\s*static\s*)?(?:\s*int\s*)?([a-zA-Z0-9_]+)\(\s*r_device\s+\*\s*[a-z]+\s*,\s*bitbuffer_t\s+\*\s*[a-z]+', line) 136 if m: 137 fName = m.group(1) 138 captureDoc = True 139 dLine = i + 1 140 doc = '' 141 continue 142 m = re.match(r'\s*/\*\*', line) 143 if m: 144 captureDoc = True 145 dLine = i + 1 146 doc = '' 147 continue 148 149 # look for r_device with decode_fn 150 m = re.match(r'\s*r_device\s+([^\*]*?)\s*=', line) 151 if m: 152 rName = m.group(1) 153 if rName in links: 154 err(f"::error file={name},line={i}::Duplicate r_device ({rName})") 155 links[rName] = {"src": name, "line": i + 1, "type": "r_device"} 156 if dSee: 157 links[rName].update({"doc_line": dLine, "doc_see": dSee}) 158 dSee = None 159 if doc: 160 links[rName].update({"doc_line": dLine, "doc": doc}) 161 doc = None 162 continue 163 # .name = "The Name", 164 m = re.match(r'\s*\.name\s*=\s*"([^"]*)', line) 165 if m: 166 update_links(links, rName, name, i, 'name', m.group(1)) 167 continue 168 # .modulation = OOK_PULSE_MANCHESTER_ZEROBIT, 169 m = re.match(r'\s*\.modulation\s*=\s*([^,\s]*)', line) 170 if m: 171 update_links(links, rName, name, i, 'modulation', m.group(1)) 172 continue 173 # .short_width = 120, 174 m = re.match(r'\s*\.short_width\s*=\s*([^,\s]*)', line) 175 if m: 176 update_links(links, rName, name, i, 'short_width', m.group(1)) 177 continue 178 # .long_width = 0, 179 m = re.match(r'\s*\.long_width\s*=\s*([^,\s]*)', line) 180 if m: 181 update_links(links, rName, name, i, 'long_width', m.group(1)) 182 continue 183 # .sync_width = 0, 184 m = re.match(r'\s*\.sync_width\s*=\s*([^,\s]*)', line) 185 if m: 186 update_links(links, rName, name, i, 'sync_width', m.group(1)) 187 continue 188 # .gap_limit = 0, 189 m = re.match(r'\s*\.gap_limit\s*=\s*([^,\s]*)', line) 190 if m: 191 update_links(links, rName, name, i, 'gap_limit', m.group(1)) 192 continue 193 # .reset_limit = 480, 194 m = re.match(r'\s*\.reset_limit\s*=\s*([^,\s]*)', line) 195 if m: 196 update_links(links, rName, name, i, 'reset_limit', m.group(1)) 197 continue 198 # .decode_fn = &the_callback, 199 m = re.match(r'\s*\.decode_fn\s*=\s*&([^,\s]*)', line) 200 if m: 201 update_links(links, rName, name, i, 'decode_fn', m.group(1)) 202 continue 203 # .disabled = 0, 204 m = re.match(r'\s*\.disabled\s*=\s*([^,\s]*)', line) 205 if m: 206 update_links(links, rName, name, i, 'disabled', m.group(1)) 207 continue 208 209 # static int foo_callback(r_device *decoder, bitbuffer_t *bitbuffer) 210 # static int foo_callback(r_device *decoder, bitbuffer_t *bitbuffer, ... 211 # static int 212 # foo_callback(r_device *decoder, bitbuffer_t *bitbuffer) 213 m = re.match( 214 r'(?:\s*static\s*int\s*)?([a-zA-Z0-9_]+)\(\s*r_device\s+\*\s*[a-z]+\s*,\s*bitbuffer_t\s+\*\s*[a-z]+', line) 215 if m: 216 # print(m.group(1)) 217 fName = m.group(1) 218 fLine = i + 1 219 if fName not in links: 220 links[fName] = {} 221 links[fName].update({"src": name, "line": fLine, "type": "func"}) 222 if dSee: 223 links[fName].update({"doc_line": dLine, "doc_see": dSee}) 224 dSee = None 225 if doc: 226 links[fName].update({"doc_line": dLine, "doc": doc}) 227 doc = None 228 continue 229 # "model", "", DATA_STRING, "Schrader", 230 m = re.match(r'\s*"model"\s*,.*DATA_STRING', line) 231 if m: 232 prefix = m.group(0) 233 s = line[len(prefix):] 234 models = re.findall(r'"([^"]+)"', s) 235 if len(models) == 0: 236 err(f"::error file={name},line={i + 1}::No models") 237 if not fName: 238 err(f"::error file={name},line={i + 1}::No func") 239 for model in models: 240 if model in links and links[model]["func"] != fName: 241 log(f"::notice file={name},line={i + 1}::Reused model") 242 elif model in links: 243 log(f"::notice file={name},line={i + 1}::Duplicate model") 244 links[model] = {"src": name, "line": i + 1, "type": "model", "func": fName} 245 246 if captureDoc: 247 err(f"::error file={name},line={dLine}::Unclosed doc comment") 248 if dSee: 249 err(f"::error file={name},line={dLine}::Unattached doc sa") 250 if doc: 251 err(f"::error file={name},line={dLine}::Unattached doc comment") 252 253 return links 254 255 256def check_symbols(symbols): 257 """Check link integrity.""" 258 models_by_func = {} 259 for f in symbols: 260 d = symbols[f] 261 262 if f == "protocols": 263 continue 264 265 if d["type"] == "file": 266 if "doc" not in d: 267 log(f"::notice file={f}::file doc missing") 268 pass 269 270 if d["type"] == "r_device": 271 if "decode_fn" not in d: 272 err(f"::error file={f}::device missing ({json.dumps(d)})") 273 elif d["decode_fn"] not in symbols: 274 err(f"::error file={f}::decoder missing ({d['decode_fn']})") 275 276 if d["type"] == "func": 277 if "line" not in d: 278 err(f"::error file={f}::func missing") 279 if "doc" not in d or not d["doc"]: 280 #err(f"::error file={f}::doc missing") 281 pass 282 283 if d["type"] == "model": 284 func = d["func"] 285 if func not in models_by_func: 286 models_by_func[func] = [] 287 models_by_func[func].append(f) 288 289 for f in symbols: 290 d = symbols[f] 291 292 if f == "protocols": 293 continue 294 295 if d["type"] == "r_device": 296 if "decode_fn" not in d: 297 err(f"::error file={f}::no decode_fn found ({d['src']})") 298 continue 299 decode_fn = d["decode_fn"] 300 func = {} 301 if decode_fn in symbols: 302 func = symbols[decode_fn] 303 else: 304 err(f"::error file={f}::decode_fn not found ({decode_fn})") 305 see = None 306 if "doc_see" in func: 307 see = func["doc_see"] 308 if see not in symbols: 309 err(f"::error file={f}::broken link for @sa ({see})") 310 311 if see and see in models_by_func: 312 # err(f"::error file={f}::models on sa link ({see})") 313 pass 314 elif decode_fn not in models_by_func: 315 err(f"::error file={f}::models not found ({d['src']})") 316 if see: 317 err(f"::error file={f}::but @sa ({func['doc_see']})") 318 319 320def main(args): 321 """Scan basedir for all groups, devices, sets, and content.""" 322 323 # ../include/rtl_433_devices.h 324 # DECL(prologue) 325 326 check = "check" in args 327 if check: 328 args.remove("check") 329 errout = sys.stdout 330 root = (['.'] + args)[-1] 331 basedir = root + '/src/devices/' 332 declpath = root + '/include/rtl_433_devices.h' 333 334 symbols = {} 335 336 symbols['protocols'] = process_protocols(declpath) 337 338 for f in listdir(basedir): 339 if f.endswith('.c'): 340 symbols.update(process_source(basedir, f)) 341 342 check_symbols(symbols) 343 if check: 344 return haserr 345 else: 346 # print(symbols) 347 # print(json.dumps(symbols, indent=2)) 348 print(json.dumps(symbols)) 349 350 351if __name__ == '__main__': 352 sys.exit(main(sys.argv[1:])) 353