ivoryOS 0.1.0__tar.gz

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.

Potentially problematic release.


This version of ivoryOS might be problematic. Click here for more details.

Files changed (54) hide show
  1. ivoryOS-0.1.0/LICENSE +21 -0
  2. ivoryOS-0.1.0/MANIFEST.in +3 -0
  3. ivoryOS-0.1.0/PKG-INFO +77 -0
  4. ivoryOS-0.1.0/README.md +55 -0
  5. ivoryOS-0.1.0/example/__init__.py +0 -0
  6. ivoryOS-0.1.0/example/abstract_robot_classes/__init__.py +0 -0
  7. ivoryOS-0.1.0/example/abstract_robot_classes/abclocation.py +6 -0
  8. ivoryOS-0.1.0/example/abstract_robot_classes/arm.py +22 -0
  9. ivoryOS-0.1.0/example/abstract_robot_classes/chemical.py +5 -0
  10. ivoryOS-0.1.0/example/abstract_robot_classes/container.py +15 -0
  11. ivoryOS-0.1.0/example/abstract_robot_classes/handler.py +13 -0
  12. ivoryOS-0.1.0/example/abstract_robot_classes/moveable.py +18 -0
  13. ivoryOS-0.1.0/example/abstract_robot_classes/moveableconfig.py +13 -0
  14. ivoryOS-0.1.0/example/dummy_ur/__init__.py +0 -0
  15. ivoryOS-0.1.0/example/dummy_ur/dummy_balance.py +10 -0
  16. ivoryOS-0.1.0/example/dummy_ur/dummy_deck.py +113 -0
  17. ivoryOS-0.1.0/example/dummy_ur/dummy_pump.py +8 -0
  18. ivoryOS-0.1.0/example/kinova_deck/__init__.py +0 -0
  19. ivoryOS-0.1.0/example/kinova_deck/chemicals.py +15 -0
  20. ivoryOS-0.1.0/example/kinova_deck/components/__init__.py +0 -0
  21. ivoryOS-0.1.0/example/kinova_deck/components/balance.py +49 -0
  22. ivoryOS-0.1.0/example/kinova_deck/components/capper.py +68 -0
  23. ivoryOS-0.1.0/example/kinova_deck/components/europa_control.py +634 -0
  24. ivoryOS-0.1.0/example/kinova_deck/components/needlewash.py +17 -0
  25. ivoryOS-0.1.0/example/kinova_deck/components/shaker.py +28 -0
  26. ivoryOS-0.1.0/example/kinova_deck/components/solenoid.py +52 -0
  27. ivoryOS-0.1.0/example/kinova_deck/experiment.py +30 -0
  28. ivoryOS-0.1.0/example/kinova_deck/kinova_arm.py +124 -0
  29. ivoryOS-0.1.0/example/kinova_deck/kinova_deck_setup.py +123 -0
  30. ivoryOS-0.1.0/example/kinova_deck/kinova_location.py +52 -0
  31. ivoryOS-0.1.0/example/kinova_deck/moveable/__init__.py +0 -0
  32. ivoryOS-0.1.0/example/kinova_deck/moveable/europa.py +50 -0
  33. ivoryOS-0.1.0/example/kinova_deck/moveable/vial.py +19 -0
  34. ivoryOS-0.1.0/example/kinova_deck/moving/__init__.py +0 -0
  35. ivoryOS-0.1.0/example/kinova_deck/moving/kinova_movable_config.py +21 -0
  36. ivoryOS-0.1.0/example/kinova_deck/webui_data/__init__.py +0 -0
  37. ivoryOS-0.1.0/example/kinova_deck/webui_data/scripts/__init__.py +0 -0
  38. ivoryOS-0.1.0/example/kinova_deck/webui_data/scripts/untitled.py +13 -0
  39. ivoryOS-0.1.0/example/test.py +90 -0
  40. ivoryOS-0.1.0/ivoryOS.egg-info/PKG-INFO +77 -0
  41. ivoryOS-0.1.0/ivoryOS.egg-info/SOURCES.txt +52 -0
  42. ivoryOS-0.1.0/ivoryOS.egg-info/dependency_links.txt +1 -0
  43. ivoryOS-0.1.0/ivoryOS.egg-info/requires.txt +11 -0
  44. ivoryOS-0.1.0/ivoryOS.egg-info/top_level.txt +2 -0
  45. ivoryOS-0.1.0/ivoryos/__init__.py +0 -0
  46. ivoryOS-0.1.0/ivoryos/app.py +996 -0
  47. ivoryOS-0.1.0/ivoryos/utils/__init__.py +0 -0
  48. ivoryOS-0.1.0/ivoryos/utils/form.py +304 -0
  49. ivoryOS-0.1.0/ivoryos/utils/llm_agent.py +171 -0
  50. ivoryOS-0.1.0/ivoryos/utils/model.py +415 -0
  51. ivoryOS-0.1.0/ivoryos/utils/openai_agent.py +144 -0
  52. ivoryOS-0.1.0/ivoryos/utils/utils.py +477 -0
  53. ivoryOS-0.1.0/setup.cfg +4 -0
  54. ivoryOS-0.1.0/setup.py +28 -0
ivoryOS-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2024 Ivory Zhang | Hein Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ graft sdl_webui/static
2
+ graft sdl_webui/templates
3
+ global-exclude *.pyc
ivoryOS-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.1
2
+ Name: ivoryOS
3
+ Version: 0.1.0
4
+ Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
+ Home-page: https://gitlab.com/heingroup/ivoryos
6
+ Author: Ivory Zhang
7
+ Author-email: ivoryzhang@chem.ubc.ca
8
+ License: MIT
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: ax-platform
12
+ Requires-Dist: bcrypt
13
+ Requires-Dist: Flask-Login
14
+ Requires-Dist: Flask-Session
15
+ Requires-Dist: Flask-SocketIO
16
+ Requires-Dist: Flask-SQLAlchemy
17
+ Requires-Dist: Flask-WTF
18
+ Requires-Dist: mysqlclient==2.1.1
19
+ Requires-Dist: SQLAlchemy-Utils
20
+ Requires-Dist: openai
21
+ Requires-Dist: python-dotenv
22
+
23
+ ![ivoryos.png](ivoryos.png)
24
+ # ivoryOS: interoperable Web UI for SDLs
25
+ ivoryOS is an open-source Python package enabling SDL interoperability. Add a web UI to SDLs anytime.
26
+ ## Description
27
+ Granting SDLs flexibility and modularity makes it almost impossible to design a UI, yet it's a necessity for allowing more people to interact with it (democratisation).
28
+ This web UI aims to ease up the control of any Python-based SDLs by displaying functions and parameters for initialized modules dynamically.
29
+ The modules can be hardware API, high-level functions, or experiment workflow.
30
+ With the least modification of the current workflow, user can design, save and run their experiment and monitor the process.
31
+ ## Design AI-assistant
32
+ To streamline the experimental design on SDLs, we also integrate Large Language Models (LLMs) to interpret the inspected functions and generate code according to task descriptions.
33
+
34
+ ## Installation
35
+ ```
36
+ pip install ivoryos
37
+ ```
38
+
39
+ ## Usage
40
+ In your SDL script, use `ivoryos(__name__)`. Example in [sample_code.py](example/dummy_ur/dummy_deck.py)
41
+
42
+ ```python
43
+ from ivoryos.app import ivoryos
44
+ ivoryos(__name__)
45
+ ```
46
+
47
+
48
+ ## Enable LLMs with [OpenAI API](https://github.com/openai/openai-python)
49
+ 1. Create a `.env` file in project repo for `OPENAI_API_KEY`
50
+ ```
51
+ OPENAI_API_KEY="Your API Key"
52
+ ```
53
+ 2. In your SDL script, define model, you can use any GPT models.
54
+ ```python
55
+ from ivoryos.app import ivoryos
56
+ ivoryos(__name__, model="gpt-3.5-turbo")
57
+ ```
58
+
59
+ ## Enable local LLMs with [Ollama](https://ollama.com/)
60
+ 1. Download Ollama.
61
+ 2. pull models from Ollama
62
+ 3. In your SDL script, define LLM server and model, you can use any models available on Ollama.
63
+ ```python
64
+ from ivoryos.app import ivoryos
65
+ ivoryos(__name__, llm_server="localhost", model="llama3.1")
66
+ ```
67
+
68
+ ## Developing
69
+ coming soon
70
+
71
+ ## Authors and Acknowledgement
72
+ Ivory Zhang, Lucy Hao
73
+
74
+ Authors acknowledge all former and current Hein Lab members for their valuable suggestions.
75
+
76
+ ## License
77
+ [LICENSE](LICENSE)
@@ -0,0 +1,55 @@
1
+ ![ivoryos.png](ivoryos.png)
2
+ # ivoryOS: interoperable Web UI for SDLs
3
+ ivoryOS is an open-source Python package enabling SDL interoperability. Add a web UI to SDLs anytime.
4
+ ## Description
5
+ Granting SDLs flexibility and modularity makes it almost impossible to design a UI, yet it's a necessity for allowing more people to interact with it (democratisation).
6
+ This web UI aims to ease up the control of any Python-based SDLs by displaying functions and parameters for initialized modules dynamically.
7
+ The modules can be hardware API, high-level functions, or experiment workflow.
8
+ With the least modification of the current workflow, user can design, save and run their experiment and monitor the process.
9
+ ## Design AI-assistant
10
+ To streamline the experimental design on SDLs, we also integrate Large Language Models (LLMs) to interpret the inspected functions and generate code according to task descriptions.
11
+
12
+ ## Installation
13
+ ```
14
+ pip install ivoryos
15
+ ```
16
+
17
+ ## Usage
18
+ In your SDL script, use `ivoryos(__name__)`. Example in [sample_code.py](example/dummy_ur/dummy_deck.py)
19
+
20
+ ```python
21
+ from ivoryos.app import ivoryos
22
+ ivoryos(__name__)
23
+ ```
24
+
25
+
26
+ ## Enable LLMs with [OpenAI API](https://github.com/openai/openai-python)
27
+ 1. Create a `.env` file in project repo for `OPENAI_API_KEY`
28
+ ```
29
+ OPENAI_API_KEY="Your API Key"
30
+ ```
31
+ 2. In your SDL script, define model, you can use any GPT models.
32
+ ```python
33
+ from ivoryos.app import ivoryos
34
+ ivoryos(__name__, model="gpt-3.5-turbo")
35
+ ```
36
+
37
+ ## Enable local LLMs with [Ollama](https://ollama.com/)
38
+ 1. Download Ollama.
39
+ 2. pull models from Ollama
40
+ 3. In your SDL script, define LLM server and model, you can use any models available on Ollama.
41
+ ```python
42
+ from ivoryos.app import ivoryos
43
+ ivoryos(__name__, llm_server="localhost", model="llama3.1")
44
+ ```
45
+
46
+ ## Developing
47
+ coming soon
48
+
49
+ ## Authors and Acknowledgement
50
+ Ivory Zhang, Lucy Hao
51
+
52
+ Authors acknowledge all former and current Hein Lab members for their valuable suggestions.
53
+
54
+ ## License
55
+ [LICENSE](LICENSE)
File without changes
@@ -0,0 +1,6 @@
1
+ from abc import ABC
2
+
3
+
4
+ class ABCLocation(ABC):
5
+ def __init__(self, robot_movement_information):
6
+ self.robot_movement_information = robot_movement_information
@@ -0,0 +1,22 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ from example.abstract_robot_classes.abclocation import ABCLocation
5
+ from example.abstract_robot_classes.moveable import Moveable
6
+
7
+
8
+ class Arm(ABC):
9
+ def __init__(self, name: str):
10
+ self.name = name
11
+
12
+ @abstractmethod
13
+ def move(self, from_loc: ABCLocation, to_loc: ABCLocation, moveable: Optional[Moveable] = None):
14
+ pass
15
+
16
+ @abstractmethod
17
+ def pick_up(self, moveable: Moveable, location: ABCLocation):
18
+ pass
19
+
20
+ @abstractmethod
21
+ def put_down(self, moveable: Moveable, location: ABCLocation):
22
+ pass
@@ -0,0 +1,5 @@
1
+ from abc import ABC
2
+
3
+
4
+ class Chemical(ABC):
5
+ pass
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ from example.abstract_robot_classes.chemical import Chemical
5
+ from example.abstract_robot_classes.moveable import Moveable
6
+ from example.kinova_deck.moving.kinova_movable_config import KinovaMoveableConfig
7
+
8
+
9
+ class Container(Moveable, ABC):
10
+ def __init__(self, name: str, config: Optional[KinovaMoveableConfig] = None):
11
+ super().__init__(name, config)
12
+
13
+ @abstractmethod
14
+ def update_contents(self, chemical: Chemical, amount_ul: int):
15
+ pass
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from .arm import Arm
4
+ from .chemical import Chemical
5
+ from .container import Container
6
+ from .abclocation import ABCLocation
7
+ from .moveable import Moveable
8
+
9
+
10
+ class Handler(Moveable, ABC):
11
+ @abstractmethod
12
+ def dispense(self, chemical: Chemical, container: Container, location: ABCLocation, arm: Arm):
13
+ pass
@@ -0,0 +1,18 @@
1
+ from abc import ABC
2
+ from typing import Optional
3
+
4
+ from example.abstract_robot_classes.moveableconfig import MoveableConfig
5
+
6
+
7
+ class Moveable(ABC):
8
+ def __init__(self, name: str,
9
+ config: Optional[MoveableConfig] = None,
10
+ offset_x: float = 0,
11
+ offset_y: float = 0,
12
+ offset_z: float = 0):
13
+ self.name = name
14
+ self.gripped = False
15
+ self.config: Optional[MoveableConfig] = config
16
+ self.offset_x = offset_x
17
+ self.offset_y = offset_y
18
+ self.offset_z = offset_z
@@ -0,0 +1,13 @@
1
+ from abc import ABC
2
+ from dataclasses import dataclass
3
+
4
+ # TODO: replace any
5
+ @dataclass
6
+ class MoveableSequence(ABC):
7
+ location_A: any
8
+ location_B: any
9
+
10
+
11
+ @dataclass
12
+ class MoveableConfig(ABC):
13
+ safe_moves: list[MoveableSequence]
File without changes
@@ -0,0 +1,10 @@
1
+ class DummyBalance:
2
+
3
+ def __init__(self, com_port: str):
4
+ self.com_port = com_port
5
+
6
+ def weigh_sample(self):
7
+ print(f"Weighing sample using {self.com_port}")
8
+
9
+ def dose_solid(self, amount_in_mg: float):
10
+ print(f"Dosing {amount_in_mg} mg using {self.com_port}")
@@ -0,0 +1,113 @@
1
+ import time
2
+
3
+ from example.dummy_ur.dummy_balance import DummyBalance
4
+ from example.dummy_ur.dummy_pump import DummyPump
5
+
6
+ # some constant values
7
+ a = {"a": 1, "b": 3}
8
+
9
+
10
+ class DummySDLDeck:
11
+ def __init__(self, pump: DummyPump, balance: DummyBalance):
12
+ self.pump = pump
13
+ self.balance = balance
14
+
15
+ def dose_solvent(self, name: str, amount_in_ml: float = 5, rate_ml_per_minute: float = 1):
16
+ self.pump.dose_liquid(amount_in_ml=amount_in_ml, rate_ml_per_minute=rate_ml_per_minute)
17
+ self.balance.weigh_sample()
18
+ print("dosing solvent")
19
+
20
+ def dose_solid(self, amount_in_mg: float = 5, bring_in: bool = False):
21
+ """dose current chemical"""
22
+ self.balance.dose_solid(amount_in_mg=amount_in_mg)
23
+ self.balance.weigh_sample()
24
+ print("dosing solid")
25
+
26
+ def equilibrate(self, temp: float, duration: float):
27
+ print("equilibrate")
28
+
29
+ def sample(self, sample_volume: float = 10):
30
+ print("sample")
31
+
32
+ def dilute(self, solvent: str, factor: float):
33
+ print("dilute")
34
+
35
+ def analyze(self):
36
+ print("analyze")
37
+
38
+ def filtration(self):
39
+ print("filtration")
40
+
41
+
42
+ balance = DummyBalance("Fake com port 1")
43
+ pump = DummyPump("Fake com port 2")
44
+ deck = DummySDLDeck(pump, balance)
45
+
46
+
47
+ class DummyExperiment:
48
+ def __init__(self, deck):
49
+ self.deck = deck
50
+
51
+ def cooling_crystal(self,
52
+ low_temp: float,
53
+ temp_diff: float,
54
+ cooling_rate: float,
55
+ mass_mg: float,
56
+ solvent_A: str,
57
+ solvent_B: str,
58
+ solvent_ratio: float
59
+ ):
60
+ print(type(mass_mg))
61
+
62
+ def reslurry(self):
63
+ pass
64
+
65
+ def solubility(self,
66
+ temp: float,
67
+ temp_time_min: float,
68
+ duplicates:int,
69
+ ):
70
+ pass
71
+
72
+ def calibration_curve(self,
73
+ mass_in_mg: float,
74
+ temp: float,
75
+ temp_time_min: float,
76
+ initial_volume_ml: float,
77
+ sample_volume_ml: float,
78
+ dilution_factor: int,
79
+ number_of_dilutions: int,
80
+ ):
81
+ pass
82
+
83
+
84
+ cryst_co = DummyExperiment(deck)
85
+
86
+
87
+ class ShakerDemo:
88
+ def __init__(self, deck):
89
+ self.deck = deck
90
+
91
+ def move_sample_to_shaker(self, location:str="A1"):
92
+ pass
93
+
94
+ def shake_for_duration(self, duration: float):
95
+ time.sleep(duration)
96
+
97
+ def move_vial_to_tray(self, location:str="A1"):
98
+ pass
99
+
100
+ vial_shaker = ShakerDemo(deck)
101
+
102
+ if __name__ == "__main__":
103
+ from ivoryos.app import ivoryos
104
+ ivoryos(__name__, model="llama3.1", llm_server='localhost',)
105
+
106
+
107
+ # ivoryos(__name__, model="gpt-3.5-turbo")
108
+
109
+ """add 10 mg of acetaminophen, dose 1 ml of methanol, equilibrate for 10 minute at 50 degrees, filter the sample and analyze with HPLC"""
110
+ """i want to run a cooling crystal experiment, with low temperature being 20, and high temperature being 40, cooling rate being 0.1, I want to dose 11 mg of my sample, and use
111
+ water as solvent a and isopropanol for solvent b, the solvent ratio being 0.1"""
112
+
113
+
@@ -0,0 +1,8 @@
1
+ class DummyPump:
2
+
3
+ def __init__(self, com_port: str):
4
+ self.com_port = com_port
5
+
6
+ def dose_liquid(self, amount_in_ml: float, rate_ml_per_minute:float):
7
+ print(f"pretending dosing {amount_in_ml} at {rate_ml_per_minute} ml/min")
8
+
File without changes
@@ -0,0 +1,15 @@
1
+ from example.abstract_robot_classes.chemical import Chemical
2
+
3
+
4
+ class Liquid(Chemical):
5
+ def __init__(self, name: str, formula: str, density: float = -1):
6
+ self.name = name
7
+ self.formula = formula
8
+ self.density: float = density
9
+
10
+
11
+ class Solid(Chemical):
12
+ def __init__(self, name: str, formula: str, density: float = -1):
13
+ self.name = name
14
+ self.formula = formula
15
+ self.density: float = density
@@ -0,0 +1,49 @@
1
+ from serial import Serial
2
+
3
+
4
+ class Balance:
5
+ def __init__(self, port: str):
6
+ self.port = port
7
+ self.ser = Serial(port)
8
+
9
+ def stable_weight(self):
10
+ response = self.send_command("S")
11
+ return self.get_reading(response)
12
+
13
+ def weight_immediately(self):
14
+ response = self.send_command("SI")
15
+ return self.get_reading(response)
16
+
17
+ def tare(self):
18
+ response = self.send_command("T")
19
+ return self.get_reading(response)
20
+
21
+ def zero(self):
22
+ self.send_command("Z")
23
+ return
24
+
25
+ def tare_immediately(self):
26
+ response = self.ser.write("TI")
27
+ return self.get_reading(response)
28
+
29
+ def zero_immediately(self):
30
+ self.send_command("ZI")
31
+
32
+ def send_command(self, command):
33
+ if not command.endswith("\n\r"):
34
+ command += "\n\r"
35
+ self.ser.write(command.encode())
36
+ response = self.ser.readline()
37
+ return response.decode().strip()
38
+
39
+ @staticmethod
40
+ def get_reading(response):
41
+ _, _, number, unit = response.split()
42
+ return (float(number))
43
+
44
+
45
+ if __name__ == "__main__":
46
+ b = Balance("COM7")
47
+ w = b.stable_weight()
48
+ print(w)
49
+ print()
@@ -0,0 +1,68 @@
1
+ from ftdi_serial import Serial
2
+
3
+ """
4
+ decapper
5
+ """
6
+
7
+ class Capper:
8
+ def __init__(self, port, baud=115200, bytesize=8, timeout=20):
9
+ # self.somatic = serial_.Serial(port=port, baudrate=baud, bytesize=bytesize,
10
+ # timeout=timeout, stopbits=serial_.STOPBITS_ONE)
11
+ self.capper = Serial(device_port=port, baudrate=baud, data_bits=bytesize, read_timeout=timeout)
12
+
13
+ print(f"Established connection with {self.capper} at {port}")
14
+
15
+ def connect(self):
16
+ self.capper.__init__()
17
+
18
+ def disconnect(self):
19
+ # put capper in its default state of "closed" before closing connection
20
+ # self.home()
21
+ self.capper.close()
22
+ print("Disconnected capper")
23
+
24
+ def open(self):
25
+ cmd = f"open"
26
+ self._execute(cmd)
27
+
28
+ def close(self):
29
+ cmd = "close"
30
+ self._execute(cmd)
31
+
32
+ def tighten(self):
33
+ cmd = "tighten"
34
+ self._execute(cmd)
35
+
36
+ def loosen(self):
37
+ cmd = "loosen"
38
+ self._execute(cmd)
39
+
40
+ def home(self):
41
+ cmd = "home"
42
+ self._execute(cmd)
43
+
44
+
45
+ def _execute(self, cmd, wait=True):
46
+ ans = None
47
+ self.capper.write(cmd.encode())
48
+
49
+ # Serial device returns "-4" when action complete
50
+ while True:
51
+ # ans = self.somatic.readline().decode("ascii").strip()
52
+ ans = self.capper.read_line().decode("ascii").strip()
53
+ if ans == "1":
54
+ break
55
+ return ans
56
+
57
+
58
+ if __name__ == "__main__":
59
+ capper = Capper("COM9")
60
+ capper.home()
61
+ capper.open()
62
+ capper.close()
63
+ # x.loosen()
64
+ # x.tighten()
65
+ # time.sleep(3)
66
+ # x.home()
67
+
68
+