Files
controller-wisetopdataserver/python/biopro/devlib/header.py
T
2021-12-20 14:52:55 +08:00

563 lines
20 KiB
Python

from pathlib import Path
from . import *
from .device import *
from .instruction import *
from .parameter import *
class MatchRule(JsonSerialize, metaclass=abc.ABCMeta):
"""Device matching rule. Program use this table to find correct library according to the response information from
remote device.
**json format**
::
MatchRule
= DeviceMatchRule
| ListMatchRule
"""
__slots__ = ()
@abc.abstractmethod
def baseline_serial(self) -> DeviceSerialNumber:
pass
@abc.abstractmethod
def is_device_match(self, device: DeviceResponseInfo) -> Optional['DeviceMatchRule']:
pass
@abc.abstractmethod
def __iter__(self) -> Iterable['DeviceMatchRule']:
pass
class NumberMatchExpression(JsonSerialize):
"""the number expression format used in MatchRule.
**json format**
::
NumberMatchExpression
= int # fix value
| "*" # ignore
| [int, int] # range value
| [int, "*"] # at least
| ["*", int] # at most
"""
__slots__ = ('_from_number', '_to_number')
def __init__(self, from_number: Optional[int], to_number: Optional[int]):
"""
:param from_number: from number. None for ignore this.
:param to_number: to number (included). None for ignore this.
"""
self._from_number = from_number
self._to_number = to_number
@property
def from_number(self) -> int:
return self._from_number if self._from_number is not None else 0
@property
def to_number(self) -> Optional[int]:
return self._to_number
def test(self, value: int) -> bool:
"""exam **value** satisfy this requirement.
"""
if self._from_number is None and self._to_number is None:
return True
else:
return (self._from_number is None or self._from_number <= value) and \
(self._to_number is None or value <= self._to_number)
def as_json(self):
if self._from_number is None and self._to_number is None:
return '*'
elif self._from_number == self._to_number:
return self._from_number
else:
return [
'*' if self._from_number is None else self._from_number,
'*' if self._to_number is None else self._to_number
]
class DeviceMatchRule(MatchRule):
"""device match rule.
**json format**
::
DeviceMatchRule = {
"local_name_pattern"? :regex
"major_product_number" :NumberMatchExpression
"minor_product_number" :NumberMatchExpression
"major_version_number" :NumberMatchExpression
"minor_version_number" :NumberMatchExpression
"serial_year_number"? :NumberMatchExpression
"serial_month_number"? :NumberMatchExpression
}
"""
__slots__ = ('_p1', '_p2', '_v1', '_v2', '_s1', '_s2', '_pattern')
def __init__(self,
major_product_number: NumberMatchExpression,
minor_product_number: NumberMatchExpression,
major_version_number: NumberMatchExpression,
minor_version_number: NumberMatchExpression,
serial_year_number: Optional[NumberMatchExpression] = None,
serial_month_number: Optional[NumberMatchExpression] = None,
local_name_pattern: Optional[str] = None):
"""
:param major_product_number: major product number requirement
:param minor_product_number: minor product number requirement
:param major_version_number: major version number requirement
:param minor_version_number: minor version number requirement
:param serial_year_number: serial year number requirement
:param serial_month_number: serial month requirement
:param local_name_pattern: device local name pattern
"""
self._p1 = major_product_number
self._p2 = minor_product_number
self._v1 = major_version_number
self._v2 = minor_version_number
self._s1 = serial_year_number
self._s2 = serial_month_number
self._pattern = local_name_pattern
if self._pattern is not None:
# test pattern syntax
re.compile(self._pattern)
@property
def major_product_number(self) -> NumberMatchExpression:
"""
:return: major product number requirement
"""
return self._p1
@property
def minor_product_number(self) -> NumberMatchExpression:
"""
:return: minor product number requirement
"""
return self._p2
@property
def major_version_number(self) -> NumberMatchExpression:
"""
:return: major version number requirement
"""
return self._v1
@property
def minor_version_number(self) -> NumberMatchExpression:
"""
:return: minor version number requirement
"""
return self._v2
@property
def serial_year_number(self) -> Optional[NumberMatchExpression]:
"""
:return: serial year number requirement. number start from 2000
"""
return self._s1
@property
def serial_month_number(self) -> Optional[NumberMatchExpression]:
"""
:return: serial month requirement. number from 1.
"""
return self._s2
@property
def local_name_pattern(self) -> Optional[str]:
"""
:return: device local name pattern
"""
return self._pattern
def baseline_serial(self) -> DeviceSerialNumber:
p1 = self._p1.from_number
p2 = self._p2.from_number
v1 = self._v1.from_number
v2 = self._v2.from_number
s1 = self._s1.from_number if self._s1 is not None else 0
s2 = self._s2.from_number if self._s2 is not None else 0
return DeviceSerialNumber(p1, p2, v1, v2, s1, s2)
def is_device_match(self, device: DeviceResponseInfo) -> Optional['DeviceMatchRule']:
serial = device.serial_number
if not self._p1.test(serial.major_product_number):
return None
if not self._p2.test(serial.minor_product_number):
return None
if not self._v1.test(serial.major_version_number):
return None
if not self._v2.test(serial.minor_version_number):
return None
if self._s1 is not None and not self._s1.test(serial.serial_year_number):
return None
if self._s2 is not None and not self._s2.test(serial.serial_month_number):
return None
if device.device_name is not None and \
self._pattern is not None and \
re.match(self._pattern, device.device_name) is None:
return None
return self
def __iter__(self) -> Iterable['DeviceMatchRule']:
yield self
def as_json(self):
ret = {
'major_product_number': self._p1.as_json(),
'minor_product_number': self._p2.as_json(),
'major_version_number': self._v1.as_json(),
'minor_version_number': self._v2.as_json(),
}
if self._s1 is not None:
ret['serial_year_number'] = self._s1.as_json()
if self._s2 is not None:
ret['serial_month_number'] = self._s2.as_json()
if self._pattern is not None:
ret['local_name_pattern'] = self._pattern
return ret
class ListMatchRule(MatchRule, ImmutableListNode[DeviceMatchRule]):
"""list of match rule.
**json format**
::
ListMatchRule
= [DeviceMatchRule]
"""
__slots__ = ()
def __init__(self, rules: Iterable[DeviceMatchRule]):
"""
:param rules: match rules
"""
super().__init__(rules)
def baseline_serial(self) -> DeviceSerialNumber:
return self.__getitem__(0).baseline_serial()
def is_device_match(self, device: DeviceResponseInfo) -> Optional[DeviceMatchRule]:
for rule in self:
if rule.is_device_match(device):
return rule
return None
def __iter__(self) -> Iterable['DeviceMatchRule']:
return iter(self._list)
class AbstractLibraryLoader(metaclass=abc.ABCMeta):
SPECIFICATION = 1
__slots__ = ('file', 'name', 'version', 'match_rule', 'constant', 'parameter', 'instruction')
def __init__(self):
self.file: Optional[str] = None
self.name: str = ''
self.version: DeviceLibraryVersion = DeviceLibraryVersion(0, 0, 0)
self.match_rule: MatchRule = ListMatchRule([])
self.constant: ConstantScope = ConstantScope.EMPTY
self.parameter: ParameterTable = ParameterTable()
self.instruction = InstructionTable()
@abc.abstractmethod
def parse_library(self, path: Path, json: JSON_OBJECT):
pass
@classmethod
def parse(cls, path: Path, json: JSON_OBJECT) -> 'AbstractLibraryLoader':
specification = _get(json, 'specification', int, 'device', nullable=True, default=cls.SPECIFICATION)
if specification == 1:
ret = LibraryLoader1()
else:
raise RuntimeError('unsupported library specification number : ' + str(specification))
ret.parse_library(path, json)
return ret
# noinspection PyMethodMayBeStatic
class DefaultLibraryLoader:
__slots__ = ()
def parse_match_rule(self, json: JSON) -> 'MatchRule':
if isinstance(json, list):
return self.parse_match_rule_list(json)
elif isinstance(json, dict):
return self.parse_match_rule_device(json)
else:
raise RuntimeError('not a device match rule : ' + _truncate(json))
def parse_match_rule_list(self, json: JSON_ARRAY) -> 'ListMatchRule':
return ListMatchRule(_list(json, self.parse_match_rule_device))
def parse_match_rule_device(self, json: JSON) -> DeviceMatchRule:
if not isinstance(json, dict):
raise TypeError('not a device match rule : ' + _truncate(json))
p1 = _get(json, 'major_product_number', None, 'device match rule')
p2 = _get(json, 'minor_product_number', None, 'device match rule')
v1 = _get(json, 'major_version_number', None, 'device match rule')
v2 = _get(json, 'minor_version_number', None, 'device match rule')
s1 = _get(json, 'serial_year_number', None, 'device match rule', nullable=True)
s2 = _get(json, 'serial_month_number', None, 'device match rule', nullable=True)
pattern = _get(json, 'local_name_pattern', str, 'device match rule', nullable=True)
return DeviceMatchRule(self.parse_match_rule_expr(p1),
self.parse_match_rule_expr(p2),
self.parse_match_rule_expr(v1),
self.parse_match_rule_expr(v2),
self.parse_match_rule_expr(s1) if s1 is not None else None,
self.parse_match_rule_expr(s2) if s2 is not None else None,
local_name_pattern=pattern)
def parse_match_rule_expr(self, json: JSON) -> NumberMatchExpression:
if json == '*':
return NumberMatchExpression(None, None)
elif isinstance(json, int):
return NumberMatchExpression(json, json)
elif isinstance(json, list):
if len(json) != 2:
raise RuntimeError('illegal NumberMatchExpression list length')
else:
a = json[0]
b = json[1]
try:
return NumberMatchExpression(None if a == '*' else int(a),
None if b == '*' else int(b))
except ValueError as e:
raise RuntimeError('illegal NumberMatchExpression value : ' + _truncate(json)) from e
else:
raise RuntimeError('illegal NumberMatchExpression type : ' + _truncate(json))
def parse_constant_pool(self, json: JSON_OBJECT) -> ConstantScope:
return ConstantScope(json)
def parse_parameter_table(self, json: JSON_OBJECT, constant: ConstantScope) -> ParameterTable:
parameters: Dict[str, ParameterInfo] = _dict(json, lambda k, v: self.parse_parameter_info(k, v, constant))
# check required
for required in ParameterInfo.REQUIRED:
if required not in parameters:
raise RuntimeError('lost required parameter : ' + required)
return ParameterTable(parameters)
def parse_parameter_info(self, name: str, json: JSON_OBJECT, constant: ConstantScope) -> ParameterInfo:
desp = _get(json, 'description', str, 'parameter', nullable=True)
meta = _get(json, 'record_meta', bool, 'parameter', nullable=True, default=False)
private = _get(json, 'private', bool, 'parameter', nullable=True, default=False)
on_change = _get(json, 'on_change', str, 'parameter', nullable=True)
# initial
initial = _get(json, 'initial', None, 'parameter', nullable=True)
if initial is not None:
initial = Expression.parse(initial).value(constant)
# value and domain
domain = _get(json, 'domain', None, 'parameter', nullable=True)
value = _get(json, 'value', None, 'parameter', nullable=True)
if value is not None:
value = Expression.parse(value)
if domain is None and isinstance(value, ArrayExpression):
domain = [0, len(value)]
if isinstance(value, ArrayExpression):
value = ListExpression(list(value))
# domain
if domain is not None:
domain = ParameterDomain.parse(domain, constant)
elif initial is not None:
domain = ParameterConstantDomain
else:
raise RuntimeError('parameter lost domain : ' + _truncate(json))
# value_set
value_set = _get(json, 'value_set', (str, dict), 'parameter', nullable=True)
if isinstance(value_set, dict):
value_set = PropertySetter.parse(value_set)
# alias
alias = _get(json, 'alias', None, 'parameter', nullable=True)
if isinstance(alias, str):
alias = [alias]
elif isinstance(alias, list):
alias = [str(v) for v in alias]
elif alias is not None:
raise RuntimeError('illegal parameter alias type : ' + _truncate(alias))
# guard
guard = _get(json, 'guard', None, 'parameter', nullable=True)
if isinstance(guard, list):
guard = ListGuardExpression.parse(guard)
elif guard is not None:
guard = ListGuardExpression([GuardExpression.parse(guard)])
return ParameterInfo(name, domain,
initial=initial,
value=value,
value_set=value_set,
on_change=on_change,
guard=guard,
alias=alias,
description=desp,
record_meta=meta,
private=private)
def parse_instruction_table(self, json: JSON_OBJECT) -> 'InstructionTable':
return InstructionTable(_dict(json, self.parse_instruction))
def parse_instruction(self, name: str, json: JSON_OBJECT) -> 'Instruction':
if isinstance(json, list):
return self.parse_instruction_list(name, json)
elif isinstance(json, dict):
if 'instructions' in json:
return self.parse_instruction_list_complex(name, json)
else:
return self.parse_instruction_send(name, json)
else:
raise RuntimeError('unknown instruction type : ' + _truncate(json))
def parse_instruction_list(self, name: str, json: JSON_ARRAY) -> ListInstruction:
ins = []
for j in json:
if isinstance(j, str):
ins.append(j)
elif isinstance(j, dict):
ins.append(Expression.parse(j))
else:
raise RuntimeError('unknown instruction : ' + _truncate(j))
return ListInstruction(name, ins)
def parse_instruction_list_complex(self, name: str, json: JSON_OBJECT) -> ListInstruction:
export = _get(json, 'export', bool, 'list instruction', nullable=True, default=False)
ins0 = _get(json, 'instructions', list, 'list instruction')
ins1 = []
for ins in ins0:
if isinstance(ins, str):
ins1.append(ins)
elif isinstance(ins, dict):
ins1.append(Expression.parse(ins))
else:
raise RuntimeError('unknown instruction : ' + _truncate(ins))
return ListInstruction(name, ins1, export=export)
def parse_instruction_send(self, name: str, json: JSON_OBJECT) -> SendInstruction:
export = _get(json, 'export', bool, 'list instruction', nullable=True, default=False)
oper = _get(json, 'operator', int, 'send instruction', nullable=True, default=0)
itype = _get(json, 'type', str, 'send instruction')
itype = DeviceInstruction.get_instruction_type(itype)
para = _get(json, 'parameter', dict, 'send instruction', nullable=True)
if para is not None:
para = SendInstructionKeywordParameter({k: Expression.parse(v) for k, v in para.items()})
fore = _get(json, 'foreach-parameter', dict, 'send instruction', nullable=True)
if fore is not None:
fore = self.parse_instruction_foreach_parameter(fore)
guard = _get(json, 'guard', None, 'send instruction', nullable=True)
if isinstance(guard, list):
guard = ListGuardExpression.parse(guard)
elif guard is not None:
guard = ListGuardExpression([GuardExpression.parse(guard)])
table = _get(json, 'data', list, 'send instruction')
table = [InstructionContent.parse(v) for v in table]
return SendInstruction(name, itype, oper, table,
parameter=para,
foreach_parameter=fore,
guard=guard,
export=export)
def parse_instruction_foreach_parameter(self, json: JSON_OBJECT) -> SendInstructionForeachParameter:
parameter = _get(json, 'parameter', str, 'foreach parameter', nullable=True)
expression = _get(json, 'expression', str, 'foreach parameter', nullable=True)
if (parameter is None and expression is None) or (parameter is not None and expression is not None):
raise RuntimeError('foreach parameter required either parameter or expression')
if expression is not None:
expression = ComplexExpression(expression)
variable = _get(json, 'variable', list, 'foreach parameter')
default = _get(json, 'default', int, 'foreach parameter', nullable=True)
return SendInstructionForeachParameter(variable, parameter=parameter, expression=expression, default=default)
class LibraryLoader1(AbstractLibraryLoader, DefaultLibraryLoader):
__slots__ = ()
def parse_library(self, path: Path, json: JSON_OBJECT):
self.name = _get(json, 'name', str, 'device')
self.version = DeviceLibraryVersion.value_of(_get(json, 'version', None, 'device'))
self.match_rule = self.parse_match_rule(_get(json, 'match_rule', None, 'device'))
self.constant = self.parse_constant_pool(_get(json, 'constant', dict, 'device', nullable=True, default={}))
self.parameter = self.parse_parameter_table(_get(json, 'parameters', dict, 'device'), self.constant)
self.instruction = self.parse_instruction_table(_get(json, 'instruction', dict, 'device'))