#!/usr/bin/env python3 """Insert implementation into an official CODESYS-generated external stub. Fails closed on unexpected syntax; never invents ABI, signature or checksum. """ import argparse from pathlib import Path import re def prepare(source, interface, sdk, destination): if source.stem != 'CmpAtomModbus' or interface.name != 'CmpAtomModbusItf.m4': raise ValueError('Generate component CmpAtomModbus, with its matching Itf.m4') c=source.read_text(); m4=interface.read_text() match=re.search(r'\batmbcall_cext\s*\(\s*(\w+)\s*\*\s*(\w+)\s*\)\s*\{',c,re.I) if not match: raise ValueError('Cannot find one-struct-pointer atMbCall_cext stub') typ,param=match.groups() structs=re.findall(r'typedef\s+struct(?:\s+\w+)?\s*\{([^{}]*)\}\s*(\w+)\s*;',m4,re.S) fields=next((body for body,name in structs if name==typ),None) if fields is None: raise ValueError('Cannot find the generated parameter struct in M4') names=re.findall(r'\b(\w+)\s*(?:\[[^\]]*\])?\s*;',fields) lookup={n.lower():n for n in names} args=['command','handle','ipaddress','port','unitid','functioncode','startaddress', 'quantity','pdata','datacapacity','pstatus','statuscapacity','nowms', 'connecttimeoutms','responsetimeoutms','retrydelayms'] for n in args+['atmbcall_cext']: if n not in lookup: raise ValueError('Missing generated field '+n) # Match the stub body, preserving all surrounding generated source. start=match.end()-1; depth=1; end=start+1 # Stub bodies must be empty or comments, not application code. while end'+lookup['atmbcall_cext']+' = ab_call(\n ' body+=',\n '.join(param+'->'+lookup[n] for n in args)+');\n}' c=c[:start]+body+c[end:] # Use one component C translation unit because the supplied SDK derives # component identity from the top-level C filename list. c='#include "../src/atom_bridge.h"\n'+c c+='\n#include "../src/atom_mb.c"\n#include "../src/atom_bridge.c"\n' frame=(sdk/'src/CmpFrame.c').read_text() hook=re.search(r'(static\s+RTS_RESULT\s+CDECL\s+HookFunction\([^)]*\)\s*\{)',frame) if not hook: raise ValueError('Unexpected SDK HookFunction; inspect manually') frame=frame[:hook.end()]+'\n if (ulHook == CH_EXIT) ab_shutdown();\n'+frame[hook.end():] frame='#include "../src/atom_bridge.h"\n'+frame destination.mkdir(parents=True,exist_ok=True) if any(destination.iterdir()): raise ValueError('Destination must be empty; preserve existing build work') (destination/'CmpAtomModbus.c').write_text(c) (destination/interface.name).write_text(m4) (destination/'CmpFrame.c').write_text(frame) (destination/'makefile').write_text('''# Generated from supplied ExtensionSDK; native build on ARM64 Pi. SDKDIR ?= ../../ExtensionSDK MAJORVER=0 MINORVER=2 CMPVERSION=0x00020000 # Local development ID; check no other component uses this ID. CMPID=0x2001 DOS2UNIX=dos2unix M4=m4 CC ?= gcc # The SDK otherwise includes CmpFrame in its component identity wildcard. override CSRC := CmpAtomModbus.c include $(SDKDIR)/makefile ''') if __name__=='__main__': p=argparse.ArgumentParser(description=__doc__) p.add_argument('source',type=Path); p.add_argument('interface',type=Path) p.add_argument('--sdk',required=True,type=Path) p.add_argument('--out',type=Path,default=Path('component')) a=p.parse_args() prepare(a.source,a.interface,a.sdk,a.out) print('Prepared component; compile and validate the generated interface on target.')