Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WLM control by task handler #27

Merged
merged 12 commits into from
Nov 20, 2024
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ python-decouple==3.8
pylint==3.3.*
pylint-django==2.6.*
djangorestframework==3.15.*
pylablib==1.4.*
55 changes: 50 additions & 5 deletions wlm_server/task/handler.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Module for task handler with WLM."""

import threading
from datetime import timedelta
from datetime import datetime, timedelta

from django.conf import settings
from pylablib.devices.HighFinesse.wlm import WLM

from .message import MessageQueue
from .measure import MeasureQueue
from config.models import Config
from setting.models import Setting
from .message import ActionType, MessageQueue
from .measure import MeasureInfo, MeasureQueue

class TaskHandler(threading.Thread):
"""Task handler for controlling and monitoring WLM.
Expand All @@ -21,12 +24,54 @@ class TaskHandler(threading.Thread):

def __init__(self):
super().__init__()
self._wlm: WLM
self._message_queue: MessageQueue = settings.MESSAGE_QUEUE
self._measure_queue: MeasureQueue = MeasureQueue()
self._channel_to_period: dict[int, timedelta] = {}
self._open_connection()

def _open_connection(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method may cause several errors, e.g., there is no Config, failed to open WLM.
In fact, regarding WLM, it is always possible to fail the API call as the USB connection goes wrong sometimes.
I think we should carefully handle such error cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe modeling this handler as an FSM would be helpful..?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm.. Indeed, I didn't consider any error on purpose.
To send the occurred error info to the request handler, we need to implement more APIs, hence I'd like to handle this after implementing main features.

What do you think about this? @kangz12345

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright then, for now the structure might vary in the future, so let's handle the errors later.

config = Config.objects.first()
self._wlm = WLM(config.wlm_version, config.wlm_dll_path, config.wlm_app_path)
self._wlm.open()
self._wlm.start_measurement()
kangz12345 marked this conversation as resolved.
Show resolved Hide resolved

def _close_connection(self):
self._wlm.stop_measurement()
self._wlm.close()

def _start_channel_measurement(self, channel: int):
kangz12345 marked this conversation as resolved.
Show resolved Hide resolved
setting = Setting.objects.filter(channel__name=channel).order_by('-created_at').first()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this in mind: the filtering might cost quite a lot in the future.
For now I think it would be fine.

period = setting.period
self._channel_to_period[channel] = period
measure = MeasureInfo(channel, datetime.now())
self._measure_queue.push(measure)

def _stop_channel_measurement(self, channel: int):
self._measure_queue.remove(channel)

def _set_channel_exposure(self, channel: int, exposure: timedelta):
self._wlm.set_exposure(exposure=exposure.total_seconds(), channel=channel)

def run(self):
while True:
while (message := self._message_queue.pop()) is not None: # pylint: disable=unused-variable
pass
while (message := self._message_queue.pop()) is not None:
channel = message.channel
data = message.data
match message.action:
kangz12345 marked this conversation as resolved.
Show resolved Hide resolved
case ActionType.CLOSE:
self._close_connection()
return
case ActionType.OPERATE:
on = data['on']
if on:
self._start_channel_measurement(channel)
else:
self._stop_channel_measurement(channel)
case ActionType.EXPOSURE:
exposure = data['exposure']
self._set_channel_exposure(channel, exposure)
case ActionType.PERIOD:
period = data['period']
self._channel_to_period[channel] = period
measure = self._measure_queue.pop() # pylint: disable=unused-variable
9 changes: 9 additions & 0 deletions wlm_server/task/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@ def pop(self) -> MeasureInfo | None:
except IndexError:
return None
return item[1]

def remove(self, channel: int):
"""Removes the measurement of the given channel from the measurement queue.

Args:
channel: Target channel.
"""
self._queue = [item for item in self._queue if item[1].channel != channel]
heapq.heapify(self._queue)