chronaxis.planning.text_inputs
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""静态解释新的ASC和原始CAN表配置,不打开输入或DBC。23来源使用既有name/time/dbc_by_channel及path或有序paths;parser明确ASC版本4或CSV布局/时间模式。offsets可逐文件声明相对于共同time.offset的额外平移,5不自动猜跨日、设备同步或日期。返回CANTextSpec及规范来源身份供公共AnalysisRunner。6"""78from hashlib import sha256910from ..core.models import TimebaseSpec11from ..core.types import TimeType12from ..sources.asc import ASCSpec13from ..sources.blf import BLFSpec14from ..sources.raw_csv import CANCSVSpec,CSVTimeSpec15from ..sources.raw_csv_layout import CANCSVLayout16from ..sources.text_can_spec import CANTextSpec17from ..timebase.conversion import checked_add_ns18from .checks import (PlanValidationError,absolute_path,array_at,canonical,choice_at,coordinate_at,19 duration_at,metadata_at,model_at,object_at,text_at,timezone_at)202122def parse_text_source(raw,base,path):23 """建立一个明确的新文本CAN来源。参数: raw原对象、base配置目录、path错误位置。24 返回: SourcePlan。异常: PlanValidationError定位配置问题;不发生原始输入I/O。25 """26 from .models import SourcePlan27 raw=object_at(raw,path,('name','source_type','time','dbc_by_channel'),('path','paths','parser','offsets','metadata','input_check','file_merge') + (('reader_options',)))28 source_type=choice_at(raw['source_type'],('can_asc','asc_or_blf','raw_can_csv'),path+'.source_type')29 name=text_at(raw['name'],path+'.name',identifier=True)30 clock=object_at(raw['time'],path+'.time',('time_type','unit'),('origin','offset','timezone'))31 time_type=choice_at(clock['time_type'],('relative','absolute'),path+'.time.time_type')32 unit=choice_at(clock['unit'],('s','ms','us','ns','ISO8601'),path+'.time.unit')33 if time_type=='relative':34 if 'origin' not in clock or 'timezone' in clock or unit=='ISO8601':35 raise PlanValidationError(path+'.time','relative必须声明共同origin,且不补日历时区')36 origin=text_at(clock['origin'],path+'.time.origin')37 clock_id='relative_'+sha256(origin.encode('utf-8')).hexdigest()[:16]38 else:39 if 'origin' in clock:40 raise PlanValidationError(path+'.time.origin','absolute不接受relative origin')41 origin=None; clock_id='utc'42 timebase=model_at(TimebaseSpec,path+'.time',clock_id,TimeType(time_type),origin)43 offset=coordinate_at(clock.get('offset','0s'),path+'.time.offset')44 zone=None45 if 'timezone' in clock:46 tz=timezone_at(clock['timezone'],path+'.time.timezone')47 delta=None if tz is None else tz.utcoffset(None)48 if delta is None or delta.total_seconds()%60:49 raise PlanValidationError(path+'.time.timezone','此原始报文读取器需要固定时区:UTC或±HH:MM')50 zone=int(delta.total_seconds()//60)51 if ('path' in raw)==('paths' in raw):52 raise PlanValidationError(path,'path和paths必须且只能提供一种')53 names=[raw['path']] if 'path' in raw else array_at(raw['paths'],path+'.paths')54 if not 1<=len(names)<=4096:55 raise PlanValidationError(path+'.paths','需要1至4096个有序文件')56 paths=tuple(absolute_path(item,base,f'{path}.paths[{i}]') for i,item in enumerate(names))57 if source_type=='asc_or_blf' and any(p.suffix.lower() not in {'.asc','.blf'} for p in paths):58 raise PlanValidationError(path+'.paths','自动读取只支持ASC/BLF后缀')59 offsets=array_at(raw.get('offsets',['0s']*len(paths)),path+'.offsets')60 if len(offsets)!=len(paths):61 raise PlanValidationError(path+'.offsets','逐文件偏移数量必须与paths相同')62 shifted=tuple(model_at(checked_add_ns,f'{path}.offsets[{i}]',offset,coordinate_at(value,f'{path}.offsets[{i}]'))63 for i,value in enumerate(offsets))64 dbc=raw['dbc_by_channel']65 if type(dbc) is not dict or len(dbc)!=1:66 raise PlanValidationError(path+'.dbc_by_channel','当前文本CAN需要一个明确通道到DBC路径')67 channel=next(iter(dbc))68 from .channels import channel_index69 channel_number=channel_index(channel,path+'.dbc_by_channel',key=True)70 dbc_path=absolute_path(dbc[channel],base,path+'.dbc_by_channel.'+channel)71 parser=raw.get('parser',{})72 if source_type in ('can_asc','asc_or_blf'):73 parser=object_at(parser,path+'.parser',(),('version','encoding'))74 if unit!='s':75 raise PlanValidationError(path+'.time.unit','ASC原文字以秒表示')76 inputs=tuple(model_at(BLFSpec,path+'.parser',p,shift) if source_type=='asc_or_blf' and p.suffix.lower()=='.blf' else77 model_at(ASCSpec,path+'.parser',p,parser.get('version','vector-asc-v1'),78 parser.get('encoding','utf-8-sig'),'epoch' if time_type=='absolute' else 'relative',zone,shift)79 for p,shift in zip(paths,shifted))80 else:81 parser=object_at(parser,path+'.parser',('layout','time_format'),82 ('id_base','length_base','channel_origin','payload_style','rollover_min_drop'))83 layout_raw=parser['layout']84 if type(layout_raw) is dict and type(layout_raw.get('fields')) is dict:85 fields=dict(layout_raw['fields'])86 # 通道列缺省时继承当前来源唯一通道;先还原输入起点,reader只减一次。87 origin=parser.get('channel_origin',0)88 if type(origin) is not int or origin not in (0,1):89 raise PlanValidationError(path+'.parser.channel_origin','通道起点须为0或1')90 fields.setdefault('channel',{'fixed':channel_number+origin})91 layout_raw=dict(layout_raw,fields=fields)92 layout=model_at(CANCSVLayout.from_dict,path+'.parser.layout',layout_raw)93 time_format=choice_at(parser['time_format'],('numeric','clock','clock-ms-us-v1','iso8601'),path+'.parser.time_format')94 if ((time_format in ('clock','clock-ms-us-v1') and (time_type!='relative' or unit!='s'))95 or time_format=='iso8601' and (time_type!='absolute' or unit!='ISO8601')96 or time_format=='numeric' and (unit=='ISO8601' or zone is not None)):97 raise PlanValidationError(path+'.time','时间种类/单位/时区与原表解析模式不符')98 rollover=duration_at(parser['rollover_min_drop'],path+'.parser.rollover_min_drop',positive=True) if 'rollover_min_drop' in parser else None99 inputs=tuple(model_at(CANCSVSpec,path+'.parser',p,layout,100 model_at(CSVTimeSpec,path+'.parser',time_format,unit if time_format=='numeric' else 's',shift,zone,rollover),101 parser.get('id_base','auto'),parser.get('length_base',10),parser.get('channel_origin',0),parser.get('payload_style','spaced'))102 for p,shift in zip(paths,shifted))103 options=object_at(raw.get('reader_options',{}),path+'.reader_options',(),('decode_policy',))104 policy=choice_at(options.get('decode_policy','strict'),105 ('strict','partial','legacy'),path+'.reader_options.decode_policy')106 input_check=choice_at(raw.get('input_check','auto'),('auto','full'),path+'.input_check')107 merge=choice_at(raw.get('file_merge','concatenate'),('concatenate','time_order'),path+'.file_merge')108 if source_type=='raw_can_csv' and 'file_merge' in raw:109 raise PlanValidationError(path+'.file_merge','file_merge仅适用于ASC/BLF来源')110 spec=model_at(CANTextSpec,path,name,inputs,dbc_path,channel_number,timebase,policy,input_check,merge)111 identity=dict(raw,paths=tuple(str(p) for p in paths),dbc_by_channel={channel:str(dbc_path)})112 identity.pop('path',None)113 return SourcePlan(name,source_type,spec,unit,clock.get('timezone'),canonical(identity),metadata_at(raw.get('metadata',{}),path+'.metadata'))114