Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
jmsojo committed Aug 22, 2024
2 parents e93d750 + eae6e82 commit 69201e5
Show file tree
Hide file tree
Showing 20 changed files with 739 additions and 379 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

name: Upload Python Package

on:
push:
tags: [ 'v*.*.*' ]

permissions:
contents: read

jobs:
deploy:

runs-on: ubuntu-latest

# Specifying a GitHub environment is optional, but strongly encouraged
environment: release
permissions:
# This permission is mandatory for trusted publishing
id-token: write

steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
8 changes: 8 additions & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: Ruff
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,18 @@ The changes made to the addresses should last until the bus loses power, at whic

## List of supported devices
Currently (somewhat) supported devices:
* Dual-Position Slider (ELL6) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=9464) - useful as a shutter
* Four-Position Slider (ELL9) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=9464) - useful as a filter wheel
* Rotation Mount (ELL14) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=12829) - useful for polarization state generators
* Motorized Iris (ELL15) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=15837) - useful for aperture control
* Linear Stage (ELL17) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=10461) - useful for motorized focusing
* Rotation Stage (ELL18) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=10459) - useful for rotating mirrors
* Linear Stage (ELL20) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=11736) - useful for motorized focusing
* Dual-Position Slider (ELL6) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=9464) - useful as a shutter
* Four-Position Slider (ELL9) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=9464) - useful as a filter wheel

As of right now, I do not have access to any other devices from the Elliptec™ family. If you are interested in controlling a device that is not on this list, feel free to reach out to me. Thank you to Thorlabs Inc. for providing me with some of the devices above for testing.

## Untested (but possibly working) devices
These devices have never been tested with this library, but could potentially work with some minor code changes, since they share a design with one of the *somewhat supported* ones:
* Linear Stage (ELL17) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=10461) - useful for motorized focusing
* Six-Position Slider (ELL12) - [Thorlabs product page](https://www.thorlabs.com/newgrouppage9.cfm?objectgroup_id=9464) - useful as a filter wheel

The same could possibly extend to the discontinued/obsolete devices such as the ELL7, ELL8, and ELL10.
Expand All @@ -141,6 +142,7 @@ What works:

What needs improvements:
* documentation
* tests (more devices, more details)
* automated discovery of devices

What is missing:
Expand Down
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "elliptec"
version = "0.0.3"
version = "0.0.5"
authors = [
{ name="David Roesel", email="[email protected]" },
]
Expand All @@ -25,4 +25,14 @@ dependencies = [
[project.urls]
"Homepage" = "https://github.com/roesel/elliptec"
"Documentation" = "https://elliptec.readthedocs.io"
"Bug Tracker" = "https://github.com/roesel/elliptec/issues"
"Bug Tracker" = "https://github.com/roesel/elliptec/issues"

[tool.ruff]
# Allow lines to be as long as 119 characters.
line-length = 119
# Ignore errors of unused module imports in __init__.py
ignore-init-module-imports = true

[tool.black]
# Allow lines to be as long as 119 characters.
line-length = 119
27 changes: 25 additions & 2 deletions src/elliptec/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""The Elliptec Python Library"""
from .cmd import commands
from .devices import devices
from .errors import ExternalDeviceNotFound
from .scan import *
from .tools import *
from .scan import find_ports, scan_for_devices
from .tools import is_null_or_empty, parse, s32, error_check, move_check

# Classes for controllers
from .controller import Controller

Expand All @@ -14,3 +16,24 @@
from .slider import Slider
from .rotator import Rotator
from .linear import Linear
from .iris import Iris

__all__ = [
"commands",
"devices",
"ExternalDeviceNotFound",
"Controller",
"Motor",
"Shutter",
"Slider",
"Rotator",
"Linear",
"Iris",
"find_ports",
"scan_for_devices",
"is_null_or_empty",
"parse",
"s32",
"error_check",
"move_check",
]
56 changes: 29 additions & 27 deletions src/elliptec/cmd.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
''' This file contains a dictionary of commands that the devices can accept in
""" This file contains a dictionary of commands that the devices can accept in
three different categories: get, set, move. For each command, it returns the
instruction code to send to the device. '''
instruction code to send to the device. """

# TODO: Each type of device should get it's own list of supported commands.

get_ = {
'info' : b'in',
'status' : b'gs',
'position': b'gp',
'stepsize' : b'gj',
'home_offset' : b'go',
'motor_1_info' : b'i1',
'motor_2_info' : b'i2',
}

set_ = {
'stepsize' : b'sj',
'isolate' : b'is',
'address' : b'ca',
'home_offset': b'so'
}
"info": b"in",
"status": b"gs",
"position": b"gp",
"stepsize": b"gj",
"home_offset": b"go",
"motor_1_info": b"i1",
"motor_2_info": b"i2",
}

set_ = {"stepsize": b"sj", "isolate": b"is", "address": b"ca", "home_offset": b"so"}

mov_ = {
'home_clockwise' : b'ho0',
'home_anticlockwise' : b'ho1',
'forward' : b'fw',
'backward' : b'bw',
'absolute' : b'ma',
'relative' : b'mr'
}
"home_clockwise": b"ho0",
"home_anticlockwise": b"ho1",
"forward": b"fw",
"backward": b"bw",
"absolute": b"ma",
"relative": b"mr",
}

do_ = {
"save_user_data": b"us",
}


def commands():
return {"get":get_, "set":set_, "move":mov_}
"""Returns a dictionary of commands that the devices can accept."""
return {"get": get_, "set": set_, "move": mov_, "do": do_}


if __name__ == '__main__':
if __name__ == "__main__":
cmds = commands()
print(cmds)
print(cmds)
109 changes: 90 additions & 19 deletions src/elliptec/controller.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,91 @@
"""This module contains the Controller class, which is the base class for all devices."""

import sys
import serial
from .tools import parse
import sys

class Controller():

def __init__(self, port, baudrate=9600, bytesize=8, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=2, write_timeout=0.5, debug=True):
class Controller:
"""Class for controlling the Elliptec devices via serial port. This is a general class,
subclasses are implemented for each device type."""

last_position = None
last_response = None
last_status = None

def __init__(self,
port = None,
baudrate=9600,
bytesize=8,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=2,
write_timeout=0.5,
debug=True):
if port == None:
self.__search_and_connect(baudrate,
bytesize,
parity,
stopbits,
timeout,
write_timeout)
else:
self.__connect_to_port(port,
baudrate,
bytesize,
parity,
stopbits,
timeout,
write_timeout)

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.close()

def __connect_to_port(self,
port,
baudrate,
bytesize,
parity,
stopbits,
timeout,
write_timeout):
try:
self.s = serial.Serial(port, baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout, write_timeout=write_timeout)
self.s = serial.Serial(port,
baudrate=baudrate,
bytesize=bytesize,
parity=parity,
stopbits=stopbits,
timeout=timeout,
write_timeout=write_timeout)

except serial.SerialException:
print('Could not open port %s' % port)
# TODO: nicer/more logical shutdown (this kills the entire app?)
sys.exit()

self.debug = debug
self.port = port

self.s.close()

if self.s.is_open:
if self.debug:
print('Controller on port {}: Connection established!'.format(port))

def __search_and_connect(self,
baudrate,
bytesize,
parity,
stopbits,
timeout,
write_timeout):
port_list = serial.tools.list_ports.comports()
for port in port_list:
self.__connect_to_port(port,
baudrate,
bytesize,
parity,
stopbits,
timeout,
write_timeout)
break

def __enter__(self):
return self
Expand All @@ -26,7 +94,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self.close()

def read_response(self):
response = self.s.read_until(b'\r\n') # Waiting until response read
"""Reads the response from the controller."""
response = self.s.read_until(b"\r\n") # Waiting until response read

if self.debug:
print("RX:", response)
Expand All @@ -36,18 +105,19 @@ def read_response(self):
# Setting properties of last response/status/position
self.last_response = response
self.last_status = status
#print('STATUS:', status)
# print('STATUS:', status)
if status is not None:
if not isinstance(status, dict):
if status[1] == 'PO':
if status[1] == "PO":
self.last_position = status[1]

return status

def send_instruction(self, instruction, address='0', message=None):
def send_instruction(self, instruction, address="0", message=None):
"""Sends an instruction to the controller. Expects a response which is returned."""
# Encode inputs
addr = address.encode('utf-8')
inst = instruction #.encode('utf-8') # Already encoded
addr = address.encode("utf-8")
inst = instruction # .encode('utf-8') # Already encoded
# Compose command
command = addr + inst
# Append command if necessary
Expand All @@ -58,17 +128,18 @@ def send_instruction(self, instruction, address='0', message=None):
else:
mesg = message

command += mesg.encode('utf-8')
command += mesg.encode("utf-8")

if self.debug:
print("TX:", command)
# Execute the command and wait for a response
self.s.write(command) # This actually executes the command
self.s.write(command) # This actually executes the command
response = self.read_response()

return response

def close(self):
def close_connection(self):
"""Closes the serial connection."""
if self.s.is_open:
self.s.close()
print("Connection is closed!")
Loading

0 comments on commit 69201e5

Please sign in to comment.