563 lines
20 KiB
Python
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'))
|