citrascope 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- citrascope/api/abstract_api_client.py +14 -0
- citrascope/api/citra_api_client.py +41 -0
- citrascope/citra_scope_daemon.py +75 -0
- citrascope/hardware/abstract_astro_hardware_adapter.py +97 -2
- citrascope/hardware/adapter_registry.py +15 -3
- citrascope/hardware/devices/__init__.py +17 -0
- citrascope/hardware/devices/abstract_hardware_device.py +79 -0
- citrascope/hardware/devices/camera/__init__.py +13 -0
- citrascope/hardware/devices/camera/abstract_camera.py +114 -0
- citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
- citrascope/hardware/devices/camera/usb_camera.py +407 -0
- citrascope/hardware/devices/camera/ximea_camera.py +756 -0
- citrascope/hardware/devices/device_registry.py +273 -0
- citrascope/hardware/devices/filter_wheel/__init__.py +7 -0
- citrascope/hardware/devices/filter_wheel/abstract_filter_wheel.py +73 -0
- citrascope/hardware/devices/focuser/__init__.py +7 -0
- citrascope/hardware/devices/focuser/abstract_focuser.py +78 -0
- citrascope/hardware/devices/mount/__init__.py +7 -0
- citrascope/hardware/devices/mount/abstract_mount.py +115 -0
- citrascope/hardware/direct_hardware_adapter.py +805 -0
- citrascope/hardware/dummy_adapter.py +202 -0
- citrascope/hardware/filter_sync.py +94 -0
- citrascope/hardware/indi_adapter.py +6 -2
- citrascope/hardware/kstars_dbus_adapter.py +46 -37
- citrascope/hardware/nina_adv_http_adapter.py +13 -11
- citrascope/settings/citrascope_settings.py +6 -0
- citrascope/tasks/runner.py +2 -0
- citrascope/tasks/scope/static_telescope_task.py +17 -12
- citrascope/tasks/task.py +3 -0
- citrascope/time/__init__.py +14 -0
- citrascope/time/time_health.py +103 -0
- citrascope/time/time_monitor.py +186 -0
- citrascope/time/time_sources.py +261 -0
- citrascope/web/app.py +260 -60
- citrascope/web/static/app.js +121 -731
- citrascope/web/static/components.js +136 -0
- citrascope/web/static/config.js +259 -420
- citrascope/web/static/filters.js +55 -0
- citrascope/web/static/formatters.js +129 -0
- citrascope/web/static/store-init.js +204 -0
- citrascope/web/static/style.css +44 -0
- citrascope/web/templates/_config.html +175 -0
- citrascope/web/templates/_config_hardware.html +208 -0
- citrascope/web/templates/_monitoring.html +242 -0
- citrascope/web/templates/dashboard.html +109 -377
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/METADATA +18 -1
- citrascope-0.9.0.dist-info/RECORD +69 -0
- citrascope-0.7.0.dist-info/RECORD +0 -41
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/WHEEL +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""USB camera adapter using OpenCV."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, cast
|
|
7
|
+
|
|
8
|
+
from citrascope.hardware.abstract_astro_hardware_adapter import SettingSchemaEntry
|
|
9
|
+
from citrascope.hardware.devices.camera import AbstractCamera
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UsbCamera(AbstractCamera):
|
|
13
|
+
"""Adapter for USB cameras accessible via OpenCV.
|
|
14
|
+
|
|
15
|
+
Supports USB cameras including guide cameras, planetary cameras, and standard webcams.
|
|
16
|
+
Note: Most USB cameras have limited exposure control compared to dedicated
|
|
17
|
+
astronomy cameras, but many are suitable for planetary imaging, guiding,
|
|
18
|
+
and testing.
|
|
19
|
+
|
|
20
|
+
Configuration:
|
|
21
|
+
camera_index (int): Camera device index (0 for first camera)
|
|
22
|
+
output_format (str): Output format - 'fits', 'png', 'jpg'
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Class-level cache for camera detection (shared across all instances)
|
|
26
|
+
_camera_cache: list[dict[str, str | int]] | None = None
|
|
27
|
+
_cache_timestamp: float = 0
|
|
28
|
+
_cache_ttl: float = float("inf") # Cache forever until daemon restart
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_friendly_name(cls) -> str:
|
|
32
|
+
"""Return human-readable name for this camera device.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Friendly display name
|
|
36
|
+
"""
|
|
37
|
+
return "USB Camera (OpenCV)"
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get_dependencies(cls) -> dict[str, str | list[str]]:
|
|
41
|
+
"""Return required Python packages.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dict with packages and install extra
|
|
45
|
+
"""
|
|
46
|
+
return {
|
|
47
|
+
"packages": ["cv2", "cv2_enumerate_cameras"],
|
|
48
|
+
"install_extra": "usb-camera",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def clear_camera_cache(cls):
|
|
53
|
+
"""Clear cached camera list to force re-detection.
|
|
54
|
+
|
|
55
|
+
Call this from a "Scan Hardware" button or when hardware changes are expected.
|
|
56
|
+
"""
|
|
57
|
+
cls._camera_cache = None
|
|
58
|
+
cls._cache_timestamp = 0
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def get_settings_schema(cls) -> list[SettingSchemaEntry]:
|
|
62
|
+
"""Return schema for USB camera settings.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of setting schema entries
|
|
66
|
+
"""
|
|
67
|
+
# Detect available cameras
|
|
68
|
+
available_cameras = cls._detect_available_cameras()
|
|
69
|
+
|
|
70
|
+
schema = [
|
|
71
|
+
{
|
|
72
|
+
"name": "camera_index",
|
|
73
|
+
"friendly_name": "Camera",
|
|
74
|
+
"type": "int",
|
|
75
|
+
"default": 0,
|
|
76
|
+
"description": "Select which camera to use",
|
|
77
|
+
"required": False,
|
|
78
|
+
"options": available_cameras,
|
|
79
|
+
"group": "Camera",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "output_format",
|
|
83
|
+
"friendly_name": "Output Format",
|
|
84
|
+
"type": "str",
|
|
85
|
+
"default": "fits",
|
|
86
|
+
"description": "Image output format",
|
|
87
|
+
"required": False,
|
|
88
|
+
"options": ["fits", "png", "jpg"],
|
|
89
|
+
"group": "Camera",
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
return cast(list[SettingSchemaEntry], schema)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def _detect_available_cameras(cls) -> list[dict[str, str | int]]:
|
|
96
|
+
"""Detect available USB cameras on the system.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of camera options as dicts with 'value' (index) and 'label' (name)
|
|
100
|
+
"""
|
|
101
|
+
import time
|
|
102
|
+
|
|
103
|
+
# Check cache first
|
|
104
|
+
cache_age = time.time() - cls._cache_timestamp
|
|
105
|
+
if cls._camera_cache is not None and cache_age < cls._cache_ttl:
|
|
106
|
+
from citrascope.logging import CITRASCOPE_LOGGER
|
|
107
|
+
|
|
108
|
+
CITRASCOPE_LOGGER.debug(f"Using cached camera list (age: {cache_age:.1f}s)")
|
|
109
|
+
return cls._camera_cache
|
|
110
|
+
|
|
111
|
+
start_time = time.time()
|
|
112
|
+
|
|
113
|
+
cameras = []
|
|
114
|
+
try:
|
|
115
|
+
import cv2
|
|
116
|
+
|
|
117
|
+
# Try to use cv2-enumerate-cameras for rich camera names
|
|
118
|
+
try:
|
|
119
|
+
from cv2_enumerate_cameras import enumerate_cameras
|
|
120
|
+
|
|
121
|
+
# Get fancy camera names from enumerate_cameras
|
|
122
|
+
# Assumption: enumerate_cameras() returns cameras in the same order as OpenCV detection
|
|
123
|
+
camera_infos = list(enumerate_cameras())
|
|
124
|
+
|
|
125
|
+
logging.debug(f"cv2_enumerate_cameras found {len(camera_infos)} cameras")
|
|
126
|
+
|
|
127
|
+
# Use the actual device index from camera_info for OpenCV compatibility
|
|
128
|
+
# Don't actually open cameras here - too wasteful
|
|
129
|
+
for camera_info in enumerate_cameras():
|
|
130
|
+
name = camera_info.name or f"Camera {camera_info.index}"
|
|
131
|
+
|
|
132
|
+
# Note: We're NOT opening cameras to verify or get resolution
|
|
133
|
+
# Use camera_info.index (the actual OpenCV device index) not enumerate position
|
|
134
|
+
cameras.append({"value": camera_info.index, "label": name})
|
|
135
|
+
logging.debug(f"Found camera with device_id {camera_info.index}: {name}")
|
|
136
|
+
|
|
137
|
+
except ImportError:
|
|
138
|
+
# cv2-enumerate-cameras not installed, use basic detection
|
|
139
|
+
for index in range(10):
|
|
140
|
+
cap = cv2.VideoCapture(index)
|
|
141
|
+
if cap.isOpened():
|
|
142
|
+
# Get camera resolution as identifier
|
|
143
|
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
144
|
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
145
|
+
|
|
146
|
+
# Try to get backend name
|
|
147
|
+
backend = cap.getBackendName() if hasattr(cap, "getBackendName") else ""
|
|
148
|
+
backend_str = f" ({backend})" if backend else ""
|
|
149
|
+
|
|
150
|
+
cameras.append({"value": index, "label": f"Camera {index} - {width}x{height}{backend_str}"})
|
|
151
|
+
cap.release()
|
|
152
|
+
else:
|
|
153
|
+
# Stop searching after first unavailable index
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
# If no cameras found, provide default option
|
|
157
|
+
if not cameras:
|
|
158
|
+
cameras.append({"value": 0, "label": "Camera 0 (default)"})
|
|
159
|
+
|
|
160
|
+
except ImportError:
|
|
161
|
+
# opencv not installed, provide default
|
|
162
|
+
cameras.append({"value": 0, "label": "Camera 0 (opencv-python not installed)"})
|
|
163
|
+
except Exception:
|
|
164
|
+
# Any other error, provide default
|
|
165
|
+
cameras.append({"value": 0, "label": "Camera 0 (default)"})
|
|
166
|
+
|
|
167
|
+
elapsed = time.time() - start_time
|
|
168
|
+
if elapsed > 0.1: # Log if takes more than 100ms
|
|
169
|
+
from citrascope.logging import CITRASCOPE_LOGGER
|
|
170
|
+
|
|
171
|
+
CITRASCOPE_LOGGER.info(f"Camera detection took {elapsed:.3f}s, found {len(cameras)} camera(s)")
|
|
172
|
+
|
|
173
|
+
# Cache the results
|
|
174
|
+
cls._camera_cache = cameras
|
|
175
|
+
cls._cache_timestamp = time.time()
|
|
176
|
+
|
|
177
|
+
return cameras
|
|
178
|
+
|
|
179
|
+
def __init__(self, logger: logging.Logger, **kwargs):
|
|
180
|
+
"""Initialize the USB camera.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
logger: Logger instance
|
|
184
|
+
**kwargs: Configuration parameters matching the schema
|
|
185
|
+
"""
|
|
186
|
+
super().__init__(logger, **kwargs)
|
|
187
|
+
|
|
188
|
+
# Camera settings
|
|
189
|
+
self.camera_index = kwargs.get("camera_index", 0)
|
|
190
|
+
self.output_format = kwargs.get("output_format", "fits")
|
|
191
|
+
|
|
192
|
+
# Camera instance (lazy loaded)
|
|
193
|
+
self._camera = None
|
|
194
|
+
self._connected = False
|
|
195
|
+
|
|
196
|
+
# Lazy import opencv to avoid hard dependency
|
|
197
|
+
self._cv2_module = None
|
|
198
|
+
|
|
199
|
+
def connect(self) -> bool:
|
|
200
|
+
"""Connect to the USB camera.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if connection successful, False otherwise
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
# Lazy import
|
|
207
|
+
if self._cv2_module is None:
|
|
208
|
+
import cv2
|
|
209
|
+
|
|
210
|
+
self._cv2_module = cv2
|
|
211
|
+
|
|
212
|
+
self.logger.info(f"Connecting to USB camera at index {self.camera_index}...")
|
|
213
|
+
|
|
214
|
+
# Open camera
|
|
215
|
+
self._camera = self._cv2_module.VideoCapture(self.camera_index)
|
|
216
|
+
|
|
217
|
+
if not self._camera.isOpened():
|
|
218
|
+
self.logger.error(f"Failed to open camera at index {self.camera_index}")
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
# Get camera properties
|
|
222
|
+
width = int(self._camera.get(self._cv2_module.CAP_PROP_FRAME_WIDTH))
|
|
223
|
+
height = int(self._camera.get(self._cv2_module.CAP_PROP_FRAME_HEIGHT))
|
|
224
|
+
|
|
225
|
+
self._connected = True
|
|
226
|
+
self.logger.info(f"USB camera connected: {width}x{height}")
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
except ImportError:
|
|
230
|
+
self.logger.error("opencv-python library not found. Install with: pip install opencv-python")
|
|
231
|
+
return False
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.logger.error(f"Failed to connect to USB camera: {e}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def disconnect(self):
|
|
237
|
+
"""Disconnect from the USB camera."""
|
|
238
|
+
if self._camera:
|
|
239
|
+
try:
|
|
240
|
+
self._camera.release()
|
|
241
|
+
self.logger.info("USB camera disconnected")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self.logger.error(f"Error disconnecting camera: {e}")
|
|
244
|
+
|
|
245
|
+
self._camera = None
|
|
246
|
+
self._connected = False
|
|
247
|
+
|
|
248
|
+
def is_connected(self) -> bool:
|
|
249
|
+
"""Check if camera is connected and responsive.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if connected, False otherwise
|
|
253
|
+
"""
|
|
254
|
+
return self._connected and self._camera is not None and self._camera.isOpened()
|
|
255
|
+
|
|
256
|
+
def take_exposure(
|
|
257
|
+
self,
|
|
258
|
+
duration: float,
|
|
259
|
+
gain: Optional[int] = None,
|
|
260
|
+
offset: Optional[int] = None,
|
|
261
|
+
binning: int = 1,
|
|
262
|
+
save_path: Optional[Path] = None,
|
|
263
|
+
) -> Path:
|
|
264
|
+
"""Capture an exposure (frame).
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
duration: Ignored for USB cameras (captures single frame)
|
|
268
|
+
gain: Not supported by most USB cameras via OpenCV
|
|
269
|
+
offset: Not supported by USB cameras via OpenCV
|
|
270
|
+
binning: Binning factor - applied via software resize
|
|
271
|
+
save_path: Path to save the image
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Path to the saved image
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
RuntimeError: If camera not connected or capture fails
|
|
278
|
+
"""
|
|
279
|
+
if not self.is_connected():
|
|
280
|
+
raise RuntimeError("Camera not connected")
|
|
281
|
+
|
|
282
|
+
if save_path is None:
|
|
283
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
284
|
+
save_path = Path(f"/tmp/usb_camera_{timestamp}.{self.output_format}")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
self.logger.info("Capturing USB camera frame...")
|
|
288
|
+
|
|
289
|
+
# Capture frame
|
|
290
|
+
ret, frame = self._camera.read()
|
|
291
|
+
|
|
292
|
+
if not ret or frame is None:
|
|
293
|
+
raise RuntimeError("Failed to capture frame from USB camera")
|
|
294
|
+
|
|
295
|
+
# Apply binning if requested
|
|
296
|
+
if binning > 1:
|
|
297
|
+
height, width = frame.shape[:2]
|
|
298
|
+
new_size = (width // binning, height // binning)
|
|
299
|
+
frame = self._cv2_module.resize(frame, new_size, interpolation=self._cv2_module.INTER_AREA)
|
|
300
|
+
|
|
301
|
+
# Determine format from file extension (if save_path provided) or configured output_format
|
|
302
|
+
file_extension = save_path.suffix.lower().lstrip(".")
|
|
303
|
+
format_to_use = file_extension if file_extension in ["fits", "png", "jpg", "jpeg"] else self.output_format
|
|
304
|
+
|
|
305
|
+
# Save based on format
|
|
306
|
+
if format_to_use == "fits":
|
|
307
|
+
self._save_as_fits(frame, save_path)
|
|
308
|
+
elif format_to_use == "png":
|
|
309
|
+
self._cv2_module.imwrite(str(save_path), frame)
|
|
310
|
+
elif format_to_use in ["jpg", "jpeg"]:
|
|
311
|
+
self._cv2_module.imwrite(str(save_path), frame, [self._cv2_module.IMWRITE_JPEG_QUALITY, 95])
|
|
312
|
+
|
|
313
|
+
self.logger.info(f"Image saved to {save_path}")
|
|
314
|
+
return save_path
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
self.logger.error(f"Failed to capture image: {e}")
|
|
318
|
+
raise RuntimeError(f"Image capture failed: {e}")
|
|
319
|
+
|
|
320
|
+
def abort_exposure(self):
|
|
321
|
+
"""Abort current exposure.
|
|
322
|
+
|
|
323
|
+
Note: USB camera captures via OpenCV are instantaneous, nothing to abort.
|
|
324
|
+
"""
|
|
325
|
+
self.logger.debug("USB camera captures are instantaneous - nothing to abort")
|
|
326
|
+
|
|
327
|
+
def get_temperature(self) -> Optional[float]:
|
|
328
|
+
"""Get camera sensor temperature.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
None - USB cameras accessed via OpenCV don't expose temperature readings
|
|
332
|
+
"""
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
def set_temperature(self, temperature: float) -> bool:
|
|
336
|
+
"""Set target camera sensor temperature.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
temperature: Target temperature in degrees Celsius
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
False - USB cameras accessed via OpenCV don't support temperature control
|
|
343
|
+
"""
|
|
344
|
+
self.logger.debug("USB cameras do not support temperature control via OpenCV")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
def start_cooling(self) -> bool:
|
|
348
|
+
"""Enable camera cooling system.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
False - USB cameras accessed via OpenCV don't have cooling systems
|
|
352
|
+
"""
|
|
353
|
+
self.logger.debug("USB cameras do not have cooling systems via OpenCV")
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def stop_cooling(self) -> bool:
|
|
357
|
+
"""Disable camera cooling system.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
False - USB cameras accessed via OpenCV don't have cooling systems
|
|
361
|
+
"""
|
|
362
|
+
self.logger.debug("USB cameras do not have cooling systems via OpenCV")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
def get_camera_info(self) -> dict:
|
|
366
|
+
"""Get camera capabilities and information.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Dictionary containing camera specs
|
|
370
|
+
"""
|
|
371
|
+
info = {
|
|
372
|
+
"name": f"USB Camera {self.camera_index}",
|
|
373
|
+
"type": "USB Camera",
|
|
374
|
+
"has_cooling": False,
|
|
375
|
+
"has_temperature_sensor": False,
|
|
376
|
+
"output_format": self.output_format,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if self.is_connected() and self._camera:
|
|
380
|
+
info["width"] = int(self._camera.get(self._cv2_module.CAP_PROP_FRAME_WIDTH))
|
|
381
|
+
info["height"] = int(self._camera.get(self._cv2_module.CAP_PROP_FRAME_HEIGHT))
|
|
382
|
+
info["fps"] = int(self._camera.get(self._cv2_module.CAP_PROP_FPS))
|
|
383
|
+
|
|
384
|
+
return info
|
|
385
|
+
|
|
386
|
+
def _save_as_fits(self, frame, save_path: Path):
|
|
387
|
+
"""Save frame as FITS format.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
frame: OpenCV BGR frame
|
|
391
|
+
save_path: Path to save FITS file
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
import numpy as np
|
|
395
|
+
from astropy.io import fits
|
|
396
|
+
|
|
397
|
+
# Convert BGR to grayscale for astronomy (luminance)
|
|
398
|
+
gray = self._cv2_module.cvtColor(frame, self._cv2_module.COLOR_BGR2GRAY)
|
|
399
|
+
|
|
400
|
+
hdu = fits.PrimaryHDU(gray.astype(np.uint16))
|
|
401
|
+
hdu.header["INSTRUME"] = "USB Camera"
|
|
402
|
+
hdu.header["CAMERA"] = f"Index {self.camera_index}"
|
|
403
|
+
hdu.writeto(save_path, overwrite=True)
|
|
404
|
+
|
|
405
|
+
except ImportError:
|
|
406
|
+
self.logger.error("astropy not installed. Install with: pip install astropy")
|
|
407
|
+
raise
|