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.
classTestObject(Thing):"""test object demonstrating properties"""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)self.my_custom_typed_serializable_attribute=[1,2,3,""]
One can also pass the property value to the Thing parent's __init__ to auto-set or auto-invoke the setter at __init__:
classTestObject(Thing):"""test object demonstrating properties"""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 descriptor object (instance of Property) that holds the property metadata and performs the get-set operations can be
accessed by the instance under self.properties.descriptors["<property name>"]:
importnumpyfromhololinked.coreimportThing,PropertyclassTestObject(Thing):"""test object demonstrating properties"""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("Given type is not list or tuple for "+f"my_custom_typed_serializable_attribute but type {type(value)}")
The value of the property must be serializable to be read by the clients. Read the serializer section for further details & customization. To make a property only locally accessible, set remote=False, i.e. such a property will not accessible on the network nevertheless the descriptor behaviour can still be leveraged.
fromhololinked.core.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)@trigger_mode.getterdefget_trigger_mode(self):returnself.device.read_trigger_mode()
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'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.coreimportProperty,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)]@classmethoddeffrom_ueye_rect(cls,rect:ueye.IS_RECT)->"Rect":returncls(x=rect.s32X.value,y=rect.s32Y.value,width=rect.s32Width.value,height=rect.s32Height.value,)defto_ueye_rect(self)->ueye.IS_RECT:rect=ueye.IS_RECT()rect.s32X=ueye.int(self.x)rect.s32Y=ueye.int(self.y)rect.s32Width=ueye.int(self.width)rect.s32Height=ueye.int(self.height)returnrectclassUEyeCamera(Thing):"""A camera from IDS Imaging"""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.from_ueye_rect(rect_aoi)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=value.to_ueye_rect()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):"""A PC based Oscilloscope from Picotech"""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"])