Properties expose python attributes to clients & support custom get-set(-delete) operations. Further, change events can be subscribed/observed to be automatically informed of a change in the value of a property.
hololinked uses param under the hood to implement properties, which in turn uses the
descriptor protocol.
Note
Python's own property is not supported for remote access due to limitations in using foreign attributes
within the property object. Said limitation causes redundancy with the existing implementation of
Property class, nevertheless, the term Property
(with capital 'P') is used to comply with the terminology of Web of Things.
fromhololinked.serverimportThing,PropertyclassTestObject(Thing):my_untyped_serializable_attribute=Property(default=frozenset([2,3,4]),allow_None=True,doc="this property can hold any python value")def__init__(self,*,id:str,**kwargs)->None:super().__init__(id=id,**kwargs)self.my_untyped_serializable_attribute=kwargs.get('some_prop',None)
One can also pass the property value to the parent's __init__ to auto-set or auto-invoke the setter at __init__:
fromhololinked.serverimportThing,PropertyclassTestObject(Thing):my_untyped_serializable_attribute=Property(default=frozenset([2,3,4]),allow_None=True,doc="this property can hold any python value")def__init__(self,*,id:str,my_untyped_serializable_attribute:Any,**kwargs)->None:super().__init__(id=id,my_untyped_serializable_attribute=my_untyped_serializable_attribute,**kwargs)
As previously stated, by default, a data container is auto allocated at the instance level. One can supply a custom getter-setter if necessary,
especially when applying the property directly onto the hardware.
The object (descriptor instance of Property) that performs the get-set operations or auto-allocation
of an internal instance variable for the property can be
accessed by the instance under self.properties.descriptors["<property name>"]:
importnumpyfromhololinked.serverimportThing,PropertyclassTestObject(Thing):my_custom_typed_serializable_attribute=Property(default=[2,"foo"],allow_None=False,doc="""this property can hold some values based on get-set overload""")@my_custom_typed_serializable_attribute.getterdefget_prop(self):try:returnself._fooexceptAttributeError:returnself.properties.descriptors["my_custom_typed_serializable_attribute"].default@my_custom_typed_serializable_attribute.setterdefset_prop(self,value):ifisinstance(value,(list,tuple))andlen(value)<100:forindex,valinenumerate(value):ifnotisinstance(val,(str,int,type(None))):raiseValueError(f"Value at position {index} not "+"acceptable member type of "+"my_custom_typed_serializable_attribute "+f"but type {type(val)}")self._foo=valueelifisinstance(value,numpy.ndarray):self._foo=valueelse:raiseTypeError(f"Given type is not list or tuple for "+f"my_custom_typed_serializable_attribute but type {type(value)}")def__init__(self,*,id:str,**kwargs)->None:super().__init__(id=id,**kwargs)self.my_untyped_serializable_attribute=kwargs.get('some_prop',None)self.my_custom_typed_serializable_attribute=[1,2,3,""]
The value of the property must be serializable to be read by the clients. Read the serializer
section for further details & customization.
Warning
Please dont use the same name as the property for the property's getter and setter methods, otherwise the property will be replaced with these methods and you are left only with the method, not the property
fromhololinked.serverimportThing,PropertyclassTestObject(Thing):my_property=Property(default=[2,"foo"],allow_None=False,doc="this property can hold some values based on get-set overload")@my_property.getterdefmy_property(self):# wrong - please dont use the property's name for the getter methodreturnself._foo
To make a property only locally accessible, set remote=False, i.e. such a property will not accessible
on the network.
fromhololinked.server.propertiesimportString,Number,Selector,Boolean,ListclassOceanOpticsSpectrometer(Thing):nonlinearity_correction=Boolean(default=False,doc="""set True for auto CCD nonlinearity correction. Not supported by all models, like STS.""")# type: booltrigger_mode=Selector(objects=[0,1,2,3,4],default=0,doc="""0 = normal/free running, 1 = Software trigger, 2 = Ext. Trigger Level, 3 = Ext. Trigger Synchro/ Shutter mode, 4 = Ext. Trigger Edge""")# type: int@trigger_mode.setterdefapply_trigger_mode(self,value:int):self.device.trigger_mode(value)self._trigger_mode=value@trigger_mode.getterdefget_trigger_mode(self):try:returnself._trigger_modeexcept:returnself.parameters["trigger_mode"].default
For typed properties, before the setter is invoked, the value is internally validated.
The return value of getter method is never validated and is left to the developer's or the client object's caution.
Schema Constrained Property
For complicated data structures, one can use pydantic or JSON schema based type definition and validation. Set the model argument
to define the type:
fromhololinked.serverimportProperty,ThingfromtypingimportAnnotated,TuplefrompydanticimportBaseModel,FieldfrompyueyeimportueyeclassRect(BaseModel):x:Annotated[int,Field(default=0,ge=0)]y:Annotated[int,Field(default=0,ge=0)]width:Annotated[int,Field(default=0,gt=0)]height:Annotated[int,Field(default=0,gt=0)]classUEyeCamera(Thing):defget_aoi(self)->Rect:"""Get current AOI from camera as Rect object (with x, y, width, height)"""rect_aoi=ueye.IS_RECT()ret=ueye.is_AOI(self.handle,ueye.IS_AOI_IMAGE_GET_AOI,rect_aoi,ueye.sizeof(rect_aoi))assertreturn_code_OK(self.handle,ret)returnRect(x=rect_aoi.s32X.value,y=rect_aoi.s32Y.value,width=rect_aoi.s32Width.value,height=rect_aoi.s32Height.value)defset_aoi(self,value:Rect)->None:"""Set camera AOI. Specify as x,y,width,height or a tuple (x, y, width, height) or as Rect object."""rect_aoi=ueye.IS_RECT()rect_aoi.s32X=ueye.int(value.x)rect_aoi.s32Y=ueye.int(value.y)rect_aoi.s32Width=ueye.int(value.width)rect_aoi.s32Height=ueye.int(value.height)ret=ueye.is_AOI(self.handle,ueye.IS_AOI_IMAGE_SET_AOI,rect_aoi,ueye.sizeof(rect_aoi))assertreturn_code_OK(self.handle,ret)AOI=Property(fget=get_aoi,fset=set_aoi,model=Rect,doc="Area of interest within the image",)# type: Rect
importctypesfrompicosdk.ps6000importps6000aspsfrompicosdk.functionsimportassert_pico_oktrigger_schema={'type':'object','properties':{'enabled':{'type':'boolean'},'channel':{'type':'string','enum':['A','B','C','D','EXTERNAL','AUX']# include both external and aux for 5000 & 6000 series# let the device driver will check if the channel is valid for the series},'threshold':{'type':'number'},'adc':{'type':'boolean'},'direction':{'type':'string','enum':['above','below','rising','falling','rising_or_falling']},'delay':{'type':'integer'},'auto_trigger':{'type':'integer','minimum':0}},"description":"Trigger settings for a single channel of the picoscope",}classPicoscope(Thing):trigger=Property(doc="Trigger settings",model=trigger_schema)# type: dict@trigger.setterdefset_trigger(self,value:dict)->None:channel=value["channel"].upper()direction=value["direction"].upper()enabled=ctypes.c_int16(int(value["enabled"]))delay=ctypes.c_int32(value["delay"])direction=ps.PS6000_THRESHOLD_DIRECTION[f'PS6000_{direction}']ifchannelin['A','B','C','D']:channel=ps.PS6000_CHANNEL['PS6000_CHANNEL_{}'.format(channel)]else:channel=ps.PS6000_CHANNEL['PS6000_TRIGGER_AUX']ifnotvalue["adc"]:ifchannelin['A','B','C','D']:threshold=int(threshold*self.max_adc*1e3/self.ranges[self.channel_settings[channel]['v_range']])else:threshold=int(self.max_adc/5)threshold=ctypes.c_int16(threshold)auto_trigger=ctypes.c_int16(int(auto_trigger))self._status['trigger']=ps.ps6000SetSimpleTrigger(self._ct_handle,enabled,channel,threshold,direction,delay,auto_trigger)assert_pico_ok(self._status['trigger'])