diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py
index 465ab97f..fcccaae8 100644
--- a/src/openflexure_microscope_server/things/camera/__init__.py
+++ b/src/openflexure_microscope_server/things/camera/__init__.py
@@ -20,7 +20,7 @@ import piexif
import labthings_fastapi as lt
from labthings_fastapi.types.numpy import NDArray
-from openflexure_microscope_server.ui import ActionButton
+from openflexure_microscope_server.ui import ActionButton, PropertyControl
from openflexure_microscope_server.background_detect import (
ColourChannelDetectLUV,
BackgroundDetectAlgorithm,
@@ -487,6 +487,11 @@ class BaseCamera(lt.Thing):
"""The calibration actions that appear only in settings panel."""
return []
+ @lt.thing_property
+ def manual_camera_settings(self) -> list[PropertyControl]:
+ """The camera settings to expose as property controls in the settings panel."""
+ return []
+
# Note that the default detector name is set at init. This is over written if
# setting is loaded from disk.
@lt.thing_setting
diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py
index 349ad4bd..1f67741f 100644
--- a/src/openflexure_microscope_server/things/camera/picamera.py
+++ b/src/openflexure_microscope_server/things/camera/picamera.py
@@ -35,7 +35,12 @@ from picamera2.outputs import Output
import labthings_fastapi as lt
from labthings_fastapi.exceptions import NotConnectedToServerError
-from openflexure_microscope_server.ui import ActionButton, action_button_for
+from openflexure_microscope_server.ui import (
+ ActionButton,
+ PropertyControl,
+ action_button_for,
+ property_control_for,
+)
from . import picamera_recalibrate_utils as recalibrate_utils
from . import BaseCamera, JPEGBlob, ArrayModel
@@ -833,6 +838,33 @@ class StreamingPiCamera2(BaseCamera):
),
]
+ @lt.thing_property
+ def manual_camera_settings(self) -> list[PropertyControl]:
+ """The camera settings to expose as property controls in the settings panel."""
+ return [
+ property_control_for(
+ self,
+ "exposure_time",
+ label="Exposure Time (0-33251)",
+ read_back=True,
+ read_back_delay=1000,
+ ),
+ property_control_for(
+ self,
+ "analogue_gain",
+ label="Analogue Gain",
+ read_back=True,
+ read_back_delay=1000,
+ ),
+ property_control_for(
+ self,
+ "colour_gains",
+ label="Colour Gains",
+ read_back=True,
+ read_back_delay=1000,
+ ),
+ ]
+
@lt.thing_property
def lens_shading_tables(self) -> Optional[LensShading]:
"""The current lens shading (i.e. flat-field correction).
diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py
index 4aa790e4..5903891b 100644
--- a/src/openflexure_microscope_server/things/camera/simulation.py
+++ b/src/openflexure_microscope_server/things/camera/simulation.py
@@ -21,7 +21,12 @@ from scipy.ndimage import gaussian_filter
import labthings_fastapi as lt
-from openflexure_microscope_server.ui import ActionButton, action_button_for
+from openflexure_microscope_server.ui import (
+ ActionButton,
+ PropertyControl,
+ action_button_for,
+ property_control_for,
+)
from . import BaseCamera, JPEGBlob, ArrayModel
from ..stage import BaseStage
@@ -166,7 +171,7 @@ class SimulatedCamera(BaseCamera):
)
# Add noise and convert to uint8
- image += RNG.normal(scale=2, size=self.shape).astype("int16")
+ image += RNG.normal(scale=self.noise_level, size=self.shape).astype("int16")
image[image < 0] = 0
image[image > 255] = 255
return image.astype("uint8")
@@ -221,6 +226,8 @@ class SimulatedCamera(BaseCamera):
return self._capture_thread.is_alive()
return False
+ noise_level = lt.ThingProperty(float, 2.0)
+
def _capture_frames(self):
portal = lt.get_blocking_portal(self)
while self._capture_enabled:
@@ -306,3 +313,8 @@ class SimulatedCamera(BaseCamera):
action_button_for(self.load_sample, submit_label="Load Sample"),
action_button_for(self.remove_sample, submit_label="Remove Sample"),
]
+
+ @lt.thing_property
+ def manual_camera_settings(self) -> list[PropertyControl]:
+ """The camera settings to expose as property controls in the settings panel."""
+ return [property_control_for(self, "noise_level", label="Noise Level")]
diff --git a/src/openflexure_microscope_server/ui.py b/src/openflexure_microscope_server/ui.py
index 7fad40d3..be2a5c12 100644
--- a/src/openflexure_microscope_server/ui.py
+++ b/src/openflexure_microscope_server/ui.py
@@ -3,18 +3,6 @@
from pydantic import BaseModel
-def action_button_for(action, **kwargs):
- """Create an action button for the specified Thing Action.
-
- :param action: The thing action to create a button for.
- :param kwargs: Any attribute of `ActionButton` except for ``thing`` or ``action``.
- """
- thing = action.args[0]
- thing_path = thing.path.strip("/")
- action_name = action.func.__name__
- return ActionButton(thing=thing_path, action=action_name, **kwargs)
-
-
class ActionButton(BaseModel):
"""The data required for creating an actionButton in Vue.
@@ -53,3 +41,54 @@ class ActionButton(BaseModel):
success_message: str = "Success!"
"""The message to show on successful completion."""
+
+
+def action_button_for(action, **kwargs) -> ActionButton:
+ """Create a ActionButton data for the specified Thing Action.
+
+ :param action: The thing action to create a button for.
+ :param kwargs: Any attribute of `ActionButton` except for ``thing`` or ``action``.
+ """
+ thing = action.args[0]
+ thing_path = thing.path.strip("/")
+ action_name = action.func.__name__
+ return ActionButton(thing=thing_path, action=action_name, **kwargs)
+
+
+class PropertyControl(BaseModel):
+ """The data required for creating an actionButton in Vue.
+
+ Currently this cannot be used to submitData, or to submitOnEvent.
+ """
+
+ thing: str
+ """The Thing "path" for the Thing instance."""
+
+ property_name: str
+ """The name of the property (or setting)."""
+
+ label: str
+ """The label to show in the UI"""
+
+ read_back: bool = False
+ """Whether or not to read back the property after setting.
+
+ This is useful for hardware settings that may be coerced to the closest value.
+ """
+
+ read_back_delay: int = 1000
+ """The delay in ms before reading back the property."""
+
+
+def property_control_for(thing, property_name, **kwargs) -> PropertyControl:
+ """Create an PropertyControl data for the specified Thing Property.
+
+ :param thing: The instance of the thing that has the property to be controlled.
+ :param property_name: The name of the property to create a control for.
+ :param kwargs: Any attribute of `PropertyControl` except for ``thing`` or
+ ``property_name``. If label is not set here it will be the property name.
+ """
+ thing_path = thing.path.strip("/")
+ if "label" not in kwargs:
+ kwargs["label"] = property_name
+ return PropertyControl(thing=thing_path, property_name=property_name, **kwargs)
diff --git a/webapp/src/components/labThingsComponents/propertyControl.vue b/webapp/src/components/labThingsComponents/propertyControl.vue
index 27f9ab55..e3ce0cbb 100644
--- a/webapp/src/components/labThingsComponents/propertyControl.vue
+++ b/webapp/src/components/labThingsComponents/propertyControl.vue
@@ -98,9 +98,14 @@ export default {
type: String,
required: true
},
+ readBack: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
readBackDelay: {
type: Number,
- default: undefined,
+ default: 1000,
required: false
}
},
@@ -124,9 +129,6 @@ export default {
return 1;
}
},
- readBack: function() {
- return this.readBackDelay !== undefined;
- },
propertyDescription: function() {
try {
return this.thingDescription(this.thingName).properties[
@@ -202,7 +204,6 @@ export default {
writeProperty: async function() {
try {
let requestedValue = this.value;
- console.log("writing", requestedValue);
await this.writeThingProperty(
this.thingName,
this.propertyName,
diff --git a/webapp/src/components/labThingsComponents/serverSpecifiedPropertyControl.vue b/webapp/src/components/labThingsComponents/serverSpecifiedPropertyControl.vue
new file mode 100644
index 00000000..a0f81ab5
--- /dev/null
+++ b/webapp/src/components/labThingsComponents/serverSpecifiedPropertyControl.vue
@@ -0,0 +1,31 @@
+
+