demo syntax from Markus Zolliker, initial commit + test file
squashed together Change-Id: I11b2f4ae99abe72c3edbc83f18826df48b2ecd02
This commit is contained in:
parent
92cf6f34a1
commit
8ad9e9396c
315
doc/demo_syntax.md
Normal file
315
doc/demo_syntax.md
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
## A SECoP Syntax for Demonstration
|
||||||
|
|
||||||
|
I feel it is very hard to explain in a talk what we are talking
|
||||||
|
about when discussing the concepts of SECoP. Would it not be easier
|
||||||
|
with an example syntax and an example SEC node? That is why I
|
||||||
|
created a simple syntax, which is on one hand easily understandable
|
||||||
|
by humans and on the other hand fulfills the requirements for a real syntax.
|
||||||
|
|
||||||
|
### An Example of an SEC Node with the Demo Syntax
|
||||||
|
|
||||||
|
We have 2 main devices: sample temperature and magnetic field.
|
||||||
|
|
||||||
|
> ts
|
||||||
|
ts=9.887
|
||||||
|
|
||||||
|
> mf
|
||||||
|
mf=5.1
|
||||||
|
|
||||||
|
|
||||||
|
We have some additional devices, coil temperature of upper and lower coil.
|
||||||
|
|
||||||
|
> tc1
|
||||||
|
tc1=2.23
|
||||||
|
|
||||||
|
> tc2
|
||||||
|
tc2=2.311
|
||||||
|
|
||||||
|
When we use the term "device" here, we do not mean a device in the sense of a
|
||||||
|
cryomagnet or a furnace. For SECoP, roughly any physical quantity which may
|
||||||
|
be of interest to the experimentalist, is its own device.
|
||||||
|
|
||||||
|
|
||||||
|
We can use wildcards to see the values of all devices at once.
|
||||||
|
|
||||||
|
> *
|
||||||
|
ts=9.887
|
||||||
|
mf=5.1
|
||||||
|
tc1=2.23
|
||||||
|
tc2=2.311
|
||||||
|
label='Cryomagnet MX15; at 9.887 K; Persistent at 5.1 Tesla'
|
||||||
|
|
||||||
|
|
||||||
|
The "> " is not part of the syntax, it is just indicating the request.
|
||||||
|
A request is always one line.
|
||||||
|
A reply might contain several lines and always ends with an empty line.
|
||||||
|
|
||||||
|
The last device here is a text displayed on the SE computer. It may not be
|
||||||
|
very useful, but it demonstrates that a device value may also be a text.
|
||||||
|
|
||||||
|
A device has parameters. The main parameter is its value. The value parameter
|
||||||
|
is just omitted in the path. We can list the the parameters and its values
|
||||||
|
with the following command:
|
||||||
|
|
||||||
|
> mf:*
|
||||||
|
mf=5.13
|
||||||
|
mf:status=0
|
||||||
|
mf:target=14.9
|
||||||
|
mf:ramp=0.4
|
||||||
|
mf:set_point=5.13
|
||||||
|
mf:persistent_mode=0
|
||||||
|
mf:switch_heater=1
|
||||||
|
|
||||||
|
If we want to change the field we have to set its target value:
|
||||||
|
|
||||||
|
> mf:target=12
|
||||||
|
mf:target=12.0
|
||||||
|
|
||||||
|
> mf:*
|
||||||
|
mf=5.13
|
||||||
|
mf:status=1
|
||||||
|
mf:target=12.0
|
||||||
|
mf:ramp=0.4
|
||||||
|
mf:set_point=5.13
|
||||||
|
mf:persistent_mode=0
|
||||||
|
mf:switch_heater=1
|
||||||
|
|
||||||
|
The status is indicating the state of the device. 0 means idle, 1 means busy
|
||||||
|
(running to the target).
|
||||||
|
|
||||||
|
A parameter has properties. The 'value' property is implicit, its just
|
||||||
|
omitted in the path:
|
||||||
|
|
||||||
|
> mf:status:*
|
||||||
|
mf:status=0
|
||||||
|
mf:status:value_names=0:idle,1:busy,2:error
|
||||||
|
mf:status:type=int
|
||||||
|
mf:status:t=2016-08-23 14:55:45.254348
|
||||||
|
|
||||||
|
Please notice the property "value_names" indicating the meaning of the status
|
||||||
|
values.
|
||||||
|
|
||||||
|
> mf:target:*
|
||||||
|
mf:target=12.0
|
||||||
|
mf:target:writable=1
|
||||||
|
mf:target:unit=T
|
||||||
|
mf:target:type=float
|
||||||
|
mf:target:t=2016-08-23 14:55:44.658749
|
||||||
|
|
||||||
|
The last property 't' is the timestamp. If the client want to record timestamps,
|
||||||
|
it can enable it for all device or parameter readings:
|
||||||
|
|
||||||
|
> :reply_items=t
|
||||||
|
:reply_items=t
|
||||||
|
|
||||||
|
> *
|
||||||
|
ts=9.867;t=2016-08-23 14:55:44.655862
|
||||||
|
mf=5.13;t=2016-08-23 14:55:44.656032
|
||||||
|
tc1=2.23;t=2016-08-23 14:55:44.656112
|
||||||
|
tc2=2.311;t=2016-08-23 14:55:44.656147
|
||||||
|
label='Cryomagnet MX15; at 9.867 K; Ramping at 5.13 Tesla';t=2016-08-23 14:55:44.656183
|
||||||
|
|
||||||
|
There is also a list command showing the devices (and parameters) without
|
||||||
|
values. I am not sure if we really need that, as we can just use a wildcard
|
||||||
|
read command and throw away the values.
|
||||||
|
|
||||||
|
> !*
|
||||||
|
ts
|
||||||
|
mf
|
||||||
|
tc1
|
||||||
|
tc2
|
||||||
|
label
|
||||||
|
|
||||||
|
> !ts:*
|
||||||
|
ts
|
||||||
|
ts:status
|
||||||
|
ts:target
|
||||||
|
ts:ramp
|
||||||
|
ts:use_ramp
|
||||||
|
ts:set_point
|
||||||
|
ts:heater_power
|
||||||
|
ts:raw_sensor
|
||||||
|
|
||||||
|
The property "meaning" indicates the meaning of the most important devices.
|
||||||
|
We can list all the devices which have a "meaning" property
|
||||||
|
|
||||||
|
> *::meaning
|
||||||
|
ts::meaning=temperature
|
||||||
|
mf::meaning=magnetic_field
|
||||||
|
|
||||||
|
> Markus: We have more things to tell here.
|
||||||
|
|
||||||
|
|
||||||
|
As a last example: the ultimate command to get everything:
|
||||||
|
|
||||||
|
> *:*:*
|
||||||
|
ts=9.887
|
||||||
|
ts::meaning=temperature
|
||||||
|
ts::unit=K
|
||||||
|
ts::description='VTI sensor (15 Tesla magnet)\ncalibration: X28611'
|
||||||
|
ts::type=float
|
||||||
|
ts::t=2016-08-23 14:55:44.655862
|
||||||
|
ts:status=0
|
||||||
|
ts:status:type=int
|
||||||
|
ts:status:t=2016-08-23 14:55:44.655946
|
||||||
|
ts:target=10.0
|
||||||
|
ts:target:writable=1
|
||||||
|
ts:target:unit=K
|
||||||
|
ts:target:type=float
|
||||||
|
ts:target:t=2016-08-23 14:55:44.655959
|
||||||
|
ts:ramp=0.0
|
||||||
|
ts:ramp:writable=1
|
||||||
|
ts:ramp:unit=K/min
|
||||||
|
ts:ramp:type=float
|
||||||
|
ts:ramp:t=2016-08-23 14:55:44.655972
|
||||||
|
ts:use_ramp=0
|
||||||
|
ts:use_ramp:type=int
|
||||||
|
ts:use_ramp:t=2016-08-23 14:55:44.655984
|
||||||
|
ts:set_point=10.0
|
||||||
|
ts:set_point:unit=K
|
||||||
|
ts:set_point:type=float
|
||||||
|
ts:set_point:t=2016-08-23 14:55:44.655995
|
||||||
|
ts:heater_power=0.154
|
||||||
|
ts:heater_power:unit=W
|
||||||
|
ts:heater_power:type=float
|
||||||
|
ts:heater_power:t=2016-08-23 14:55:44.656006
|
||||||
|
ts:raw_sensor=1876.3
|
||||||
|
ts:raw_sensor:unit=Ohm
|
||||||
|
ts:raw_sensor:type=float
|
||||||
|
ts:raw_sensor:t=2016-08-23 14:55:44.656018
|
||||||
|
mf=5.13
|
||||||
|
mf::meaning=magnetic_field
|
||||||
|
mf::unit=T
|
||||||
|
mf::description=magnetic field (15 Tesla magnet)
|
||||||
|
mf::type=float
|
||||||
|
mf::t=2016-08-23 14:55:44.656032
|
||||||
|
mf:status=0
|
||||||
|
mf:status:type=int
|
||||||
|
mf:status:t=2016-08-23 14:55:44.656044
|
||||||
|
mf:target=12.0
|
||||||
|
mf:target:writable=1
|
||||||
|
mf:target:unit=T
|
||||||
|
mf:target:type=float
|
||||||
|
mf:target:t=2016-08-23 14:55:44.658749
|
||||||
|
mf:ramp=0.4
|
||||||
|
mf:ramp:writable=1
|
||||||
|
mf:ramp:unit=T/min
|
||||||
|
mf:ramp:type=float
|
||||||
|
mf:ramp:t=2016-08-23 14:55:44.656066
|
||||||
|
mf:set_point=5.13
|
||||||
|
mf:set_point:unit=T
|
||||||
|
mf:set_point:type=float
|
||||||
|
mf:set_point:t=2016-08-23 14:55:44.656077
|
||||||
|
mf:persistent_mode=0
|
||||||
|
mf:persistent_mode:type=int
|
||||||
|
mf:persistent_mode:t=2016-08-23 14:55:44.656088
|
||||||
|
mf:switch_heater=1
|
||||||
|
mf:switch_heater:type=int
|
||||||
|
mf:switch_heater:t=2016-08-23 14:55:44.656099
|
||||||
|
tc1=2.23
|
||||||
|
tc1::unit=K
|
||||||
|
tc1::description='top coil (15 Tesla magnet)\ncalibration: X30906'
|
||||||
|
tc1::type=float
|
||||||
|
tc1::t=2016-08-23 14:55:44.656112
|
||||||
|
tc1:status=0
|
||||||
|
tc1:status:type=int
|
||||||
|
tc1:status:t=2016-08-23 14:55:44.656123
|
||||||
|
tc1:raw_sensor=5434.0
|
||||||
|
tc1:raw_sensor:unit=Ohm
|
||||||
|
tc1:raw_sensor:type=float
|
||||||
|
tc1:raw_sensor:t=2016-08-23 14:55:44.656134
|
||||||
|
tc2=2.311
|
||||||
|
tc2::unit=K
|
||||||
|
tc2::description='bottom coil (15 Tesla magnet)\ncalibration: C103'
|
||||||
|
tc2::type=float
|
||||||
|
tc2::t=2016-08-23 14:55:44.656147
|
||||||
|
tc2:status=0
|
||||||
|
tc2:status:type=int
|
||||||
|
tc2:status:t=2016-08-23 14:55:44.656159
|
||||||
|
tc2:raw_sensor=4834.5
|
||||||
|
tc2:raw_sensor:unit=Ohm
|
||||||
|
tc2:raw_sensor:type=float
|
||||||
|
tc2:raw_sensor:t=2016-08-23 14:55:44.656169
|
||||||
|
label='Cryomagnet MX15; Ramping'
|
||||||
|
label::writable=1
|
||||||
|
label::type=string
|
||||||
|
label::t=2016-08-23 14:55:44.656183
|
||||||
|
.:reply_items=t
|
||||||
|
.:reply_items:writable=1
|
||||||
|
.:reply_items:type=string
|
||||||
|
.:reply_items:t=2016-08-23 14:55:44.659617
|
||||||
|
.:compact_output=0
|
||||||
|
.:compact_output:writable=1
|
||||||
|
.:compact_output:type=int
|
||||||
|
.:compact_output:t=2016-08-23 14:55:44.656219
|
||||||
|
|
||||||
|
|
||||||
|
The last device '.' is a dummy device to hold the parameters of a client
|
||||||
|
connection. Changing these parameters must not affect other client connections.
|
||||||
|
The experimental parameter compact_output is for compressing the result of
|
||||||
|
wildcard requests: unchanged device and parameter names are omitted.
|
||||||
|
|
||||||
|
|
||||||
|
> :compact_output=1
|
||||||
|
.:compact_output=1
|
||||||
|
|
||||||
|
> *:*:*
|
||||||
|
ts=9.887
|
||||||
|
::meaning=temperature
|
||||||
|
::unit=K
|
||||||
|
::description='VTI sensor (15 Tesla magnet)\ncalibration: X28611'
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180514
|
||||||
|
:status=0
|
||||||
|
::type=int
|
||||||
|
::t=2016-08-23 15:04:55.180587
|
||||||
|
:target=10.0
|
||||||
|
::writable=1
|
||||||
|
::unit=K
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180594
|
||||||
|
:ramp=0.0
|
||||||
|
::writable=1
|
||||||
|
::unit=K/min
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180599
|
||||||
|
:use_ramp=0
|
||||||
|
::type=int
|
||||||
|
::t=2016-08-23 15:04:55.180604
|
||||||
|
:set_point=10.0
|
||||||
|
::unit=K
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180609
|
||||||
|
:heater_power=0.154
|
||||||
|
::unit=W
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180615
|
||||||
|
:raw_sensor=1876.3
|
||||||
|
::unit=Ohm
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180620
|
||||||
|
mf=5.13
|
||||||
|
::meaning=magnetic_field
|
||||||
|
::unit=T
|
||||||
|
::description=magnetic field (15 Tesla magnet)
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180626
|
||||||
|
:status=0
|
||||||
|
::type=int
|
||||||
|
::t=2016-08-23 15:04:55.180632
|
||||||
|
:target=14.9
|
||||||
|
::writable=1
|
||||||
|
::unit=T
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180637
|
||||||
|
:ramp=0.4
|
||||||
|
::writable=1
|
||||||
|
::unit=T/min
|
||||||
|
::type=float
|
||||||
|
::t=2016-08-23 15:04:55.180642
|
||||||
|
:set_point=5.13
|
||||||
|
::unit=T
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
285
doc/demo_syntax_mapping.md
Normal file
285
doc/demo_syntax_mapping.md
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
## A SECoP Syntax for Demonstration
|
||||||
|
|
||||||
|
### Mapping of Messages
|
||||||
|
|
||||||
|
Please remind: replies always end with en empty line, which means that there are
|
||||||
|
2 newline characters at the end of each reply. To emphasize that, in this document
|
||||||
|
a bare '_' is shown as a replacement for the closing empty line.
|
||||||
|
|
||||||
|
#### ListDevices
|
||||||
|
|
||||||
|
> !*
|
||||||
|
<device-name1>
|
||||||
|
<device-name2>
|
||||||
|
...
|
||||||
|
<device-nameN>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### ListDeviceParams
|
||||||
|
|
||||||
|
> !<device>:*
|
||||||
|
<device>:<param1>
|
||||||
|
<device>:<param2>
|
||||||
|
...
|
||||||
|
<device>:<paramN>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### ReadRequest
|
||||||
|
|
||||||
|
> <device>:<param>
|
||||||
|
<device>:<param1>=<value>
|
||||||
|
_
|
||||||
|
|
||||||
|
Or, in case it is combined with timestamp (may be also sigma, unit ...). See also
|
||||||
|
below under "client parameters" / "reply_items".
|
||||||
|
|
||||||
|
> <device>:<param>
|
||||||
|
<device>:<param1>=<value>;t=<timestamp>
|
||||||
|
_
|
||||||
|
> <device>:<param>
|
||||||
|
<device>:<param1>=<value>;t=<timestamp>;s=<sigma>
|
||||||
|
_
|
||||||
|
> <device>:<param>
|
||||||
|
<device>:<param1>=<value>;t=<timestamp>;s=<sigma>;unit=<unit>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### ReadPropertiesOfAllDevices
|
||||||
|
remark: for shortness the name of the value property and the preceding ':' is omitted
|
||||||
|
|
||||||
|
> *:*:*
|
||||||
|
<device1>:<param1>=<value1>
|
||||||
|
<device1>:<param1>:<prop2>=<value2>
|
||||||
|
...
|
||||||
|
<deviceN>:<paramM>:<propL>=<valueK>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### ReadPropertiesOfADevice
|
||||||
|
means read properties of all parameters of a device)
|
||||||
|
|
||||||
|
> <device>:*:*
|
||||||
|
<device>:<param1>=<value1>
|
||||||
|
<device>:<param1>:<prop2>=<value2>
|
||||||
|
...
|
||||||
|
<device>:<paramN>:<propM>=<valueL>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### ReadPropertiesOfAParameter
|
||||||
|
|
||||||
|
> <device>:<param>:*
|
||||||
|
<device>:<param>=<value1>
|
||||||
|
<device>:<param>:<prop2>=<value2>
|
||||||
|
...
|
||||||
|
<device>:<param>:<propN>=<valueN>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### ReadSpecificProperty
|
||||||
|
|
||||||
|
> <device>:<param>:<prop>
|
||||||
|
<device>:<param>:<prop>=<value>
|
||||||
|
_
|
||||||
|
|
||||||
|
In case you want the value only, and not the timestamp etc, '.' is a placeholder for the
|
||||||
|
value property
|
||||||
|
|
||||||
|
> <device>:<param>:.
|
||||||
|
<device>:<param>:.=<value>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### ReadValueNotOlderThan
|
||||||
|
|
||||||
|
Instead of this special Request, I propose to make a client parameter "max_age",
|
||||||
|
which specifies in general how old values may be to be returned from cache.
|
||||||
|
|
||||||
|
#### Write
|
||||||
|
|
||||||
|
remark: writes to other than the 'value' property are not allowed. If anyone sees a
|
||||||
|
reasonable need to make writeable properties, a WriteProperty Request/Reply should
|
||||||
|
be discussed
|
||||||
|
|
||||||
|
> <device>:<param>=<value>
|
||||||
|
<device>:<param>=<readback-value>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### Command
|
||||||
|
|
||||||
|
If we want to distinguish between a write request and a command, we need also a
|
||||||
|
different syntax. It is to decide, how arguments may be structured / typed.
|
||||||
|
Should a command also send back a "return value"?
|
||||||
|
|
||||||
|
> <device>:<param> <arguments>
|
||||||
|
<device>:<param> <arguments>
|
||||||
|
_
|
||||||
|
|
||||||
|
#### Error replies
|
||||||
|
|
||||||
|
Error reply format:
|
||||||
|
|
||||||
|
~<error-specifier>~ <path or other info>
|
||||||
|
_
|
||||||
|
|
||||||
|
The error-specifier should be predefined identifer.
|
||||||
|
|
||||||
|
> tempature:target
|
||||||
|
~NoSuchCommand~ tempature
|
||||||
|
_
|
||||||
|
|
||||||
|
> temperature:taget
|
||||||
|
~NoSuchParameter~ temperature:taget
|
||||||
|
_
|
||||||
|
|
||||||
|
#### FeatureListRequest
|
||||||
|
|
||||||
|
Instead of an extra FeatureListRequest message, I propose to do have a device,
|
||||||
|
which contains some SEC node properties, with information about the SEC node,
|
||||||
|
and client parameters, which can be set by the ECS for optional
|
||||||
|
features. Remind that internally the SEC Node has to store the client parameters
|
||||||
|
separately for every client.
|
||||||
|
|
||||||
|
#### SEC Node properties
|
||||||
|
|
||||||
|
You are welcome to propose better names:
|
||||||
|
|
||||||
|
> ::implements_timestamps
|
||||||
|
::implements_timestamps=1
|
||||||
|
_
|
||||||
|
> ::implements_async_communication
|
||||||
|
::implements_async_communication=1
|
||||||
|
_
|
||||||
|
|
||||||
|
The maximum time delay for a response from the SEC Node:
|
||||||
|
|
||||||
|
> ::reply_timeout=10
|
||||||
|
::reply_timeout=10
|
||||||
|
_
|
||||||
|
|
||||||
|
SEC Node properties might be omitted for the default behaviour.
|
||||||
|
|
||||||
|
#### Client parameters
|
||||||
|
|
||||||
|
Enable transmission of timestamp and sigma with every value
|
||||||
|
|
||||||
|
> :reply_items=t,s
|
||||||
|
:reply_items=t,s
|
||||||
|
_
|
||||||
|
|
||||||
|
If a requested property is not present on a parameter, it is just omitted in the reply.
|
||||||
|
|
||||||
|
The reply_items parameter might be not be present, when the SEC node does not implement
|
||||||
|
timestamps and similar.
|
||||||
|
|
||||||
|
Update timeout (see Update message)
|
||||||
|
|
||||||
|
> :update_timeout=10
|
||||||
|
:update_timeout=10
|
||||||
|
|
||||||
|
#### SubscribeToAsyncData
|
||||||
|
|
||||||
|
In different flavors: all parameters and all devices:
|
||||||
|
|
||||||
|
> +*:*
|
||||||
|
<device1>.<param1>
|
||||||
|
...
|
||||||
|
<deviceN>.<paramM>
|
||||||
|
_
|
||||||
|
|
||||||
|
Only the values of all devices:
|
||||||
|
|
||||||
|
> +*
|
||||||
|
+<device1>
|
||||||
|
...
|
||||||
|
+<deviceN>
|
||||||
|
_
|
||||||
|
|
||||||
|
All parameters of one device:
|
||||||
|
|
||||||
|
> +<device>:*
|
||||||
|
+<device>:<param1>
|
||||||
|
...
|
||||||
|
+<device>:<paramN>
|
||||||
|
_
|
||||||
|
|
||||||
|
I think we need no special subscriptions to properties, as :reply_items should be
|
||||||
|
recognized by the updates. All other properties are not supposed to be changed during
|
||||||
|
an experiment (we might discuss this).
|
||||||
|
|
||||||
|
If an Unsubscribe Message would be implemented, it could be start with a '-'
|
||||||
|
|
||||||
|
#### Update
|
||||||
|
|
||||||
|
In order to stick to a strict request / reply mechanism I propose instead of a real
|
||||||
|
asynchronous communication to have an UpdateRequest. The reply of an UpdateRequest
|
||||||
|
might happen delayed.
|
||||||
|
|
||||||
|
- it replies immediately with a list of all subscripted updates, if some happend since
|
||||||
|
the last UpdateRequest
|
||||||
|
- if nothing has changed, the UpdateReply is sent as soon as any update happens
|
||||||
|
- if nothing happens within the time specified by :update_timeout
|
||||||
|
an empty reply is returned.
|
||||||
|
|
||||||
|
If a client detects no reply within :update_timeout plus ::reply_timeout,
|
||||||
|
it can assume the the SEC Node is dead.
|
||||||
|
|
||||||
|
The UpdateRequest may be just an empty line (my favorite) or, if you prefer an
|
||||||
|
question mark:
|
||||||
|
|
||||||
|
> ?
|
||||||
|
<device1>=<value1>
|
||||||
|
<device2>.<paramX>=<value2>
|
||||||
|
_
|
||||||
|
|
||||||
|
With no update during :update_timeout seconds, the reply would be
|
||||||
|
|
||||||
|
> ?
|
||||||
|
_
|
||||||
|
|
||||||
|
|
||||||
|
#### Additional Messages
|
||||||
|
|
||||||
|
I list here some additional messages, which could be useful, but wich were not yet
|
||||||
|
identified as special messages. They all follow the same syntax, which means that
|
||||||
|
it is probably no extra effort for the implementation.
|
||||||
|
|
||||||
|
Interestingly, we have defined ReadPropertiesOfAllDevices, but not Read a specific
|
||||||
|
property of all parameters. At least reading the value properties is useful:
|
||||||
|
|
||||||
|
> *:*
|
||||||
|
|
||||||
|
List all device classes (assuming a device property "class")
|
||||||
|
|
||||||
|
> *::class
|
||||||
|
<device1>::class=<class1>
|
||||||
|
<device2>::class=<class2>
|
||||||
|
...
|
||||||
|
<deviceN>::class=<classN>
|
||||||
|
_
|
||||||
|
|
||||||
|
The property meaning is for saying: this device is important for the experimentalist,
|
||||||
|
and has the indicated meaning. It might be used by the ECS to skip devices, which
|
||||||
|
are of no interest for non experts. Example:
|
||||||
|
|
||||||
|
> *::meaning
|
||||||
|
ts::meaning=sample temperature
|
||||||
|
mf::meaning=magnetic field
|
||||||
|
_
|
||||||
|
|
||||||
|
We might find other useful messages, which can be implemented without any additional
|
||||||
|
syntax.
|
||||||
|
|
||||||
|
|
||||||
|
#### A possible syntax extension:
|
||||||
|
|
||||||
|
Allow a comma separated list for path items:
|
||||||
|
|
||||||
|
> ts,mf
|
||||||
|
ts=1.65
|
||||||
|
mf=5.13
|
||||||
|
_
|
||||||
|
|
||||||
|
> ts::.,t,s
|
||||||
|
ts=<value>
|
||||||
|
ts::t=<timestamp>
|
||||||
|
ts::s=<sigma>
|
||||||
|
_
|
||||||
|
|
||||||
|
If somebody does not like the :reply_items mechanism, we could us the latter example
|
||||||
|
as alternative for reading values together with timestamp and sigma.
|
38
src/demo_syntax/cmd_lineserver.py
Normal file
38
src/demo_syntax/cmd_lineserver.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import fileinput
|
||||||
|
|
||||||
|
class LineHandler():
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_line(self, line):
|
||||||
|
print " ", line
|
||||||
|
|
||||||
|
def handle_line(self, line):
|
||||||
|
'''
|
||||||
|
test: simple echo handler
|
||||||
|
'''
|
||||||
|
self.send_line("> " + line)
|
||||||
|
|
||||||
|
class LineServer():
|
||||||
|
|
||||||
|
def __init__(self, isfile, lineHandlerClass):
|
||||||
|
self.lineHandlerClass = lineHandlerClass
|
||||||
|
self.isfile = isfile
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
handler = self.lineHandlerClass()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if self.isfile:
|
||||||
|
line = raw_input("")
|
||||||
|
print "> "+line
|
||||||
|
else:
|
||||||
|
line = raw_input("> ")
|
||||||
|
except EOFError:
|
||||||
|
return
|
||||||
|
handler.handle_line(line)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = LineServer("localhost", 9999, LineHandler)
|
||||||
|
server.loop()
|
312
src/demo_syntax/demo_server.py
Normal file
312
src/demo_syntax/demo_server.py
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# SECoP demo syntax simulation
|
||||||
|
#
|
||||||
|
# Author: Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
#
|
||||||
|
# Input from a file for test purposes:
|
||||||
|
#
|
||||||
|
# python -m demo_server f < test.txt
|
||||||
|
#
|
||||||
|
# Input from commandline:
|
||||||
|
#
|
||||||
|
# python -m demo_server
|
||||||
|
#
|
||||||
|
# Input from TCP/IP connections:
|
||||||
|
#
|
||||||
|
# python -m demo_server <port>
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
from collections import OrderedDict
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) <= 1:
|
||||||
|
port = 0
|
||||||
|
import cmd_lineserver as lineserver
|
||||||
|
elif sys.argv[1] == "f":
|
||||||
|
port = 1
|
||||||
|
import cmd_lineserver as lineserver
|
||||||
|
else:
|
||||||
|
port = int(sys.argv[1])
|
||||||
|
import tcp_lineserver as lineserver
|
||||||
|
|
||||||
|
|
||||||
|
class SecopError(Exception):
|
||||||
|
def __init__(self, message, text):
|
||||||
|
Exception.__init__(self, message, text)
|
||||||
|
|
||||||
|
class UnknownDeviceError(SecopError):
|
||||||
|
def __init__(self, message, args):
|
||||||
|
SecopError.__init__(self, "NoSuchDevice", args[0])
|
||||||
|
|
||||||
|
class UnknownParamError(SecopError):
|
||||||
|
def __init__(self, message, args):
|
||||||
|
SecopError.__init__(self, "NoSuchParam", "%s:%s" % args)
|
||||||
|
|
||||||
|
class UnknownPropError(SecopError):
|
||||||
|
def __init__(self, message, args):
|
||||||
|
SecopError.__init__(self, "NoSuchProperty", "%s:%s:%s" % args)
|
||||||
|
|
||||||
|
class SyntaxError(SecopError):
|
||||||
|
def __init__(self, message):
|
||||||
|
SecopError.__init__(self, "SyntaxError", message)
|
||||||
|
|
||||||
|
class OtherError(SecopError):
|
||||||
|
def __init__(self, message):
|
||||||
|
SecopError.__init__(self, "OtherError", message)
|
||||||
|
|
||||||
|
|
||||||
|
def encode(input):
|
||||||
|
inp = str(input)
|
||||||
|
enc = repr(inp)
|
||||||
|
if inp.find(';') >= 0 or inp != enc[1:-1]:
|
||||||
|
return enc
|
||||||
|
return inp
|
||||||
|
|
||||||
|
def gettype(obj):
|
||||||
|
typ = str(type(obj).__name__)
|
||||||
|
if typ == "unicode":
|
||||||
|
typ = "string"
|
||||||
|
return typ
|
||||||
|
|
||||||
|
def wildcard(dictionary, pattern):
|
||||||
|
list = []
|
||||||
|
if pattern == "":
|
||||||
|
pattern = "."
|
||||||
|
for p in pattern.split(","):
|
||||||
|
if p[-1] == "*":
|
||||||
|
if p[:-1].find("*") >= 0:
|
||||||
|
raise SyntaxError("illegal wildcard pattern %s" % p)
|
||||||
|
for key in dictionary:
|
||||||
|
if key.startswith(p[:-1]):
|
||||||
|
list.append(key)
|
||||||
|
elif p in dictionary:
|
||||||
|
list.append(p)
|
||||||
|
else:
|
||||||
|
raise KeyError(pattern)
|
||||||
|
return list
|
||||||
|
|
||||||
|
|
||||||
|
class SecopClientProps(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.reply_items = {".":"t", "writable":1}
|
||||||
|
self.compact_output = {".":0, "writable":1}
|
||||||
|
|
||||||
|
class SecopLineHandler(lineserver.LineHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
lineserver.LineHandler.__init__(self, *args, **kwargs)
|
||||||
|
self.props = SecopClientProps()
|
||||||
|
|
||||||
|
def handle_line(self, msg):
|
||||||
|
try:
|
||||||
|
if msg[-1:] == "\r": # strip CR at end (for CRLF)
|
||||||
|
msg=msg[:-1]
|
||||||
|
if msg[:1] == "+":
|
||||||
|
self.subscribe(msg[1:].split(":"))
|
||||||
|
elif msg[:1] == "-":
|
||||||
|
self.unsubscribe(msg[1:].split(":"))
|
||||||
|
elif msg[:1] == "!":
|
||||||
|
self.list(msg[1:])
|
||||||
|
else:
|
||||||
|
j = msg.find("=")
|
||||||
|
if j >= 0:
|
||||||
|
self.write(msg[0:j], msg[j+1:])
|
||||||
|
else:
|
||||||
|
self.read(msg)
|
||||||
|
except SecopError as e:
|
||||||
|
self.send_line("~%s~ %s" % (e.args[0], e.args[1]))
|
||||||
|
self.send_line("")
|
||||||
|
|
||||||
|
|
||||||
|
def get_device(self, d):
|
||||||
|
if d == "":
|
||||||
|
d = "."
|
||||||
|
if d == ".":
|
||||||
|
return self.props.__dict__
|
||||||
|
try:
|
||||||
|
return secNodeDict[d]
|
||||||
|
except KeyError:
|
||||||
|
raise UnknownDeviceError("",(d))
|
||||||
|
|
||||||
|
def get_param(self, d, p):
|
||||||
|
if p == "":
|
||||||
|
p = "."
|
||||||
|
try:
|
||||||
|
return self.get_device(d)[p]
|
||||||
|
except KeyError:
|
||||||
|
raise UnknownParamError("",(d,p))
|
||||||
|
|
||||||
|
def get_prop(self, d, p, y):
|
||||||
|
if y == "":
|
||||||
|
y = "."
|
||||||
|
try:
|
||||||
|
paramDict = self.get_param(d, p)
|
||||||
|
return (paramDict, paramDict[y])
|
||||||
|
except KeyError:
|
||||||
|
raise UnknownPropertyError("",(d,p,y))
|
||||||
|
|
||||||
|
def clear_output_path(self):
|
||||||
|
# used for compressing only
|
||||||
|
self.outpath = [".", ".", "."]
|
||||||
|
try:
|
||||||
|
self.compact = self.props.compact_output["."] != 0
|
||||||
|
except KeyError:
|
||||||
|
self.compact = False
|
||||||
|
|
||||||
|
def output_path(self, d, p=".", y="."):
|
||||||
|
# compose path from arguments. compress if compact is True
|
||||||
|
if d == self.outpath[0]:
|
||||||
|
msg = ":"
|
||||||
|
else:
|
||||||
|
msg = d + ":"
|
||||||
|
if self.compact:
|
||||||
|
self.outpath[0] = d
|
||||||
|
self.outpath[1] = "."
|
||||||
|
self.outpath[2] = "."
|
||||||
|
if p == self.outpath[1]:
|
||||||
|
msg += ":"
|
||||||
|
else:
|
||||||
|
msg += p + ":"
|
||||||
|
if self.compact:
|
||||||
|
self.outpath[1] = p
|
||||||
|
self.outpath[2] = "."
|
||||||
|
if y == "" or y == self.outpath[2]:
|
||||||
|
while msg[-1:] == ":":
|
||||||
|
msg = msg[:-1]
|
||||||
|
else:
|
||||||
|
msg += y
|
||||||
|
if self.compact:
|
||||||
|
self.outpath[2] = y
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def write(self, pathArg, value):
|
||||||
|
self.clear_output_path()
|
||||||
|
path = pathArg.split(":")
|
||||||
|
while len(path) < 3:
|
||||||
|
path.append("")
|
||||||
|
d,p,y = path
|
||||||
|
parDict = self.get_param(d, p)
|
||||||
|
if (y != "." and y != "") or not parDict.get("writable", 0):
|
||||||
|
self.send_line("? %s is not writable" % self.output_path(d,p,y))
|
||||||
|
typ = type(parDict["."])
|
||||||
|
try:
|
||||||
|
val = (typ)(value)
|
||||||
|
except ValueError:
|
||||||
|
raise SyntaxError("can not convert '%s' to %s" % (value, gettype(value)))
|
||||||
|
parDict["."] = val
|
||||||
|
parDict["t"] = datetime.utcnow()
|
||||||
|
self.send_line(self.output_path(d, p, ".") + "=" + encode(parDict["."]))
|
||||||
|
|
||||||
|
|
||||||
|
def read(self, pathArg):
|
||||||
|
self.clear_output_path()
|
||||||
|
path = pathArg.split(":")
|
||||||
|
if len(path) > 3:
|
||||||
|
raise SyntaxError("path may only contain 3 elements")
|
||||||
|
while len(path) < 3:
|
||||||
|
path.append("")
|
||||||
|
|
||||||
|
# first collect a list of matched properties
|
||||||
|
list = []
|
||||||
|
try:
|
||||||
|
devList = wildcard(secNodeDict, path[0])
|
||||||
|
except KeyError as e:
|
||||||
|
raise UnknownDeviceError("", (e.message))
|
||||||
|
for d in devList:
|
||||||
|
devDict = secNodeDict[d]
|
||||||
|
try:
|
||||||
|
parList = wildcard(devDict, path[1])
|
||||||
|
except KeyError as e:
|
||||||
|
raise UnknownParamError("", (d, e.message))
|
||||||
|
for p in parList:
|
||||||
|
parDict = devDict[p]
|
||||||
|
try:
|
||||||
|
propList = wildcard(parDict, path[2])
|
||||||
|
except KeyError as e:
|
||||||
|
raise UnknownPropError("", (d, p, e.message))
|
||||||
|
for y in propList:
|
||||||
|
list.append((d, p, y))
|
||||||
|
|
||||||
|
# then, if no error happened, write out the messages
|
||||||
|
try:
|
||||||
|
replyitems = self.props.reply_items["."]
|
||||||
|
replyitems = replyitems.split(",")
|
||||||
|
except KeyError:
|
||||||
|
replyitems = []
|
||||||
|
for item in list:
|
||||||
|
d, p, y = item
|
||||||
|
paramDict = secNodeDict[d][p]
|
||||||
|
if path[2] == "":
|
||||||
|
msg = self.output_path(d, p, "") + "=" + encode(paramDict["."])
|
||||||
|
for y in replyitems:
|
||||||
|
if y == ".": continue # do not show the value twice
|
||||||
|
try:
|
||||||
|
msg += ";" + y + "=" + encode(paramDict[y])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
msg = self.output_path(d, p, y) + "=" + encode(paramDict[y])
|
||||||
|
self.send_line(msg)
|
||||||
|
|
||||||
|
def list(self, pathArg):
|
||||||
|
self.clear_output_path()
|
||||||
|
path = pathArg.split(":")
|
||||||
|
# first collect a list of matched items
|
||||||
|
list = []
|
||||||
|
try:
|
||||||
|
devList = wildcard(secNodeDict, path[0])
|
||||||
|
except KeyError as e:
|
||||||
|
raise UnknownDeviceError("", (e.message))
|
||||||
|
for d in devList:
|
||||||
|
devDict = secNodeDict[d]
|
||||||
|
if len(path) == 1:
|
||||||
|
list.append((d, ".", "."))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
parList = wildcard(devDict, path[1])
|
||||||
|
except KeyError as e:
|
||||||
|
raise UnknownParamError("", (d, e.message))
|
||||||
|
for p in parList:
|
||||||
|
parDict = devDict[p]
|
||||||
|
if len(path) == 2:
|
||||||
|
list.append((d, p, "."))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
propList = wildcard(parDict, path[2])
|
||||||
|
except KeyError as e:
|
||||||
|
raise UnknownPropError("", (d, p, e.message))
|
||||||
|
for y in propList:
|
||||||
|
list.append((d, p, y))
|
||||||
|
|
||||||
|
# then, if no error happened, write out the items
|
||||||
|
for item in list:
|
||||||
|
d, p, y = item
|
||||||
|
self.send_line(self.output_path(d, p, y))
|
||||||
|
|
||||||
|
def subscribe(self, pathArg):
|
||||||
|
raise OtherError("subscribe unimplemented")
|
||||||
|
|
||||||
|
def unsubscribe(self, pathArg):
|
||||||
|
raise OtherError("unsubscribe unimplemented")
|
||||||
|
|
||||||
|
if port <= 1:
|
||||||
|
server = lineserver.LineServer(port, SecopLineHandler)
|
||||||
|
else:
|
||||||
|
server = lineserver.LineServer("localhost", port, SecopLineHandler)
|
||||||
|
|
||||||
|
|
||||||
|
secNodeDict=json.load(open("secnode.json", "r"), object_pairs_hook=OrderedDict)
|
||||||
|
#json.dump(secNodeDict, open("secnode_out.json", "w"), indent=2, separators=(",",":"))
|
||||||
|
for d in secNodeDict:
|
||||||
|
devDict = secNodeDict[d]
|
||||||
|
for p in devDict:
|
||||||
|
parDict = devDict[p]
|
||||||
|
try:
|
||||||
|
parDict["type"] = gettype(parDict["."])
|
||||||
|
except KeyError:
|
||||||
|
print d, p, " no '.' (value) property"
|
||||||
|
continue
|
||||||
|
parDict["t"] = datetime.utcnow()
|
||||||
|
|
||||||
|
server.loop()
|
104
src/demo_syntax/secnode.json
Normal file
104
src/demo_syntax/secnode.json
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"ts":{
|
||||||
|
".":{
|
||||||
|
".":9.887,
|
||||||
|
"meaning":"temperature",
|
||||||
|
"unit":"K",
|
||||||
|
"description":"VTI sensor (15 Tesla magnet)\ncalibration: X28611"
|
||||||
|
},
|
||||||
|
"status":{
|
||||||
|
".":0
|
||||||
|
},
|
||||||
|
"target":{
|
||||||
|
".":10.0,
|
||||||
|
"writable":1,
|
||||||
|
"unit":"K"
|
||||||
|
},
|
||||||
|
"ramp":{
|
||||||
|
".":0.0,
|
||||||
|
"writable":1,
|
||||||
|
"unit":"K/min"
|
||||||
|
},
|
||||||
|
"use_ramp":{
|
||||||
|
".":0
|
||||||
|
},
|
||||||
|
"set_point":{
|
||||||
|
".":10.0,
|
||||||
|
"unit":"K"
|
||||||
|
},
|
||||||
|
"heater_power":{
|
||||||
|
".":0.154,
|
||||||
|
"unit":"W"
|
||||||
|
},
|
||||||
|
"raw_sensor":{
|
||||||
|
".":1876.3,
|
||||||
|
"unit":"Ohm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mf":{
|
||||||
|
".":{
|
||||||
|
".":5.13,
|
||||||
|
"meaning":"magnetic_field",
|
||||||
|
"unit":"T",
|
||||||
|
"description":"magnetic field (15 Tesla magnet)"
|
||||||
|
},
|
||||||
|
"status":{
|
||||||
|
".":0,
|
||||||
|
"value_names":"0:idle,1:busy,2:error"
|
||||||
|
},
|
||||||
|
"target":{
|
||||||
|
".":14.9,
|
||||||
|
"writable":1,
|
||||||
|
"unit":"T"
|
||||||
|
},
|
||||||
|
"ramp":{
|
||||||
|
".":0.4,
|
||||||
|
"writable":1,
|
||||||
|
"unit":"T/min"
|
||||||
|
},
|
||||||
|
"set_point":{
|
||||||
|
".":5.13,
|
||||||
|
"unit":"T"
|
||||||
|
},
|
||||||
|
"persistent_mode":{
|
||||||
|
".":0
|
||||||
|
},
|
||||||
|
"switch_heater":{
|
||||||
|
".":1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tc1":{
|
||||||
|
".":{
|
||||||
|
".":2.23,
|
||||||
|
"unit":"K",
|
||||||
|
"description":"top coil (15 Tesla magnet)\ncalibration: X30906"
|
||||||
|
},
|
||||||
|
"status":{
|
||||||
|
".":0
|
||||||
|
},
|
||||||
|
"raw_sensor":{
|
||||||
|
".":5434.0,
|
||||||
|
"unit":"Ohm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tc2":{
|
||||||
|
".":{
|
||||||
|
".":2.311,
|
||||||
|
"unit":"K",
|
||||||
|
"description":"bottom coil (15 Tesla magnet)\ncalibration: C103"
|
||||||
|
},
|
||||||
|
"status":{
|
||||||
|
".":0
|
||||||
|
},
|
||||||
|
"raw_sensor":{
|
||||||
|
".":4834.5,
|
||||||
|
"unit":"Ohm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"label":{
|
||||||
|
".":{
|
||||||
|
".": "test;test",
|
||||||
|
"writable": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/demo_syntax/tcp_lineserver.py
Normal file
59
src/demo_syntax/tcp_lineserver.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import asyncore
|
||||||
|
import socket
|
||||||
|
|
||||||
|
class LineHandler(asyncore.dispatcher_with_send):
|
||||||
|
|
||||||
|
def __init__(self, sock):
|
||||||
|
self.buffer = ""
|
||||||
|
asyncore.dispatcher_with_send.__init__(self, sock)
|
||||||
|
self.crlf = 0
|
||||||
|
|
||||||
|
def handle_read(self):
|
||||||
|
data = self.recv(8192)
|
||||||
|
if data:
|
||||||
|
parts = data.split("\n")
|
||||||
|
if len(parts) == 1:
|
||||||
|
self.buffer += data
|
||||||
|
else:
|
||||||
|
self.handle_line(self.buffer + parts[0])
|
||||||
|
for part in parts[1:-1]:
|
||||||
|
if part[-1] == "\r":
|
||||||
|
self.crlf = True
|
||||||
|
part = part[:-1]
|
||||||
|
else:
|
||||||
|
self.crlf = False
|
||||||
|
self.handle_line(part)
|
||||||
|
self.buffer = parts[-1]
|
||||||
|
|
||||||
|
def send_line(self, line):
|
||||||
|
self.send(line + ("\r\n" if self.crlf else "\n"))
|
||||||
|
|
||||||
|
def handle_line(self, line):
|
||||||
|
'''
|
||||||
|
test: simple echo handler
|
||||||
|
'''
|
||||||
|
self.send_line("> " + line)
|
||||||
|
|
||||||
|
class LineServer(asyncore.dispatcher):
|
||||||
|
|
||||||
|
def __init__(self, host, port, lineHandlerClass):
|
||||||
|
asyncore.dispatcher.__init__(self)
|
||||||
|
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.set_reuse_addr()
|
||||||
|
self.bind((host, port))
|
||||||
|
self.listen(5)
|
||||||
|
self.lineHandlerClass = lineHandlerClass
|
||||||
|
|
||||||
|
def handle_accept(self):
|
||||||
|
pair = self.accept()
|
||||||
|
if pair is not None:
|
||||||
|
sock, addr = pair
|
||||||
|
print "Incoming connection from %s" % repr(addr)
|
||||||
|
handler = self.lineHandlerClass(sock)
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
asyncore.loop()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = LineServer("localhost", 9999, LineHandler)
|
||||||
|
server.loop()
|
13
src/demo_syntax/test_requests.txt
Normal file
13
src/demo_syntax/test_requests.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
:reply_items=
|
||||||
|
ts
|
||||||
|
mf
|
||||||
|
tc1
|
||||||
|
*
|
||||||
|
mf:*
|
||||||
|
mf:target=12
|
||||||
|
mf:*
|
||||||
|
mf:target:*
|
||||||
|
:reply_items=t
|
||||||
|
*
|
||||||
|
!*
|
||||||
|
*:*:*
|
Loading…
x
Reference in New Issue
Block a user