Files
Codesys-Library-Atom-Modbus/tools/prepare_component.py
T
2026-09-30 21:17:41 -06:00

77 lines
3.8 KiB
Python

#!/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<len(c) and depth:
if c[end]=='{': depth+=1
elif c[end]=='}': depth-=1
end+=1
if depth: raise ValueError('Unbalanced generated stub')
old=c[start+1:end-1]
old=re.sub(r'/\*.*?\*/|//[^\n]*','',old,flags=re.S).strip()
if old: raise ValueError('Stub contains code; inspect manually instead of replacing it')
body='{\n '+param+'->'+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.')