Skip to content

Commit

Permalink
Merge pull request #34 from sunjerry019/cli
Browse files Browse the repository at this point in the history
Add a CLI Application
  • Loading branch information
sunjerry019 authored Mar 15, 2022
2 parents 8409bab + b604d02 commit bf7afbb
Show file tree
Hide file tree
Showing 15 changed files with 366 additions and 7 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ All logging is provided by the `LoggerMixIn` class under [`src/nanosquared/commo
If you are adding modules to the codebase, it is recommended to inherit the `LoggerMixIn` class.

## Usage
**To start the quick-and-dirty CLI Application, simply double click on `launch_m2.bat`.**

**The environment `nanosquared` needs to be set up with Anaconda.**

If you are using a `pip`-installed version, simply do:
```python
import nanosquared
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions launch_m2.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off
call %ProgramData%\Anaconda3\Scripts\activate.bat nanosquared
python ./src/cli-app/m2-app.py
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="nanosquared",
version="0.1.1-alpha",
version="0.2",
author="Yudong Sun",
author_email="[email protected]",
description="Automated M-Squared Measurement",
Expand Down
138 changes: 138 additions & 0 deletions src/cli-app/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import os, sys
import distutils.util

# https://stackoverflow.com/a/287944/3211506
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'

class CLI():
COLORS = bcolors
GAP = f"\033[95m==>>>\033[0m "

def __init__(self) -> None:
pass

@staticmethod
def clear_screen():
os.system('cls' if os.name == 'nt' else 'clear')

@staticmethod
def print_sep():
print("======================")

@staticmethod
def presskeycont():
input("Press Enter to continue...")
return

@staticmethod
def getPositiveNonZeroFloat(question, default = None) -> float:

prompt = f"[Default = {default}]" if default is not None else ""

while True:
try:
resp = input(CLI.GAP + question + " " + prompt + " > ").strip()
if default is not None and resp == '':
return default
else:
resp = float(resp)
if resp <= 0:
raise ValueError
return resp
except ValueError:
print("ERROR: Please respond with a positive number/float.")
except EOFError:
print("Encountered EOF, exiting...")
sys.exit()

@staticmethod
def getIntWithLimit(question, default = None, lowerlimit: int = 1) -> int:
"""Gets an integer that is no lower than the `lowerlimit`
Parameters
----------
question : str
Question to ask
default : int, optional
Default value, by default None
lowerlimit : int, optional
Lowest acceptable integer, by default 1
Returns
-------
int
Received input
"""

prompt = f"[Default = {default}]" if default is not None else ""

while True:
try:
resp = input(CLI.GAP + question + " " + prompt + " > ").strip()
if default is not None and resp == '':
return default
else:
resp = int(resp)
if resp <= lowerlimit:
raise ValueError
return resp
except ValueError:
print(f"ERROR: Please respond with an integer that is at least {lowerlimit}")
except EOFError:
print("Encountered EOF, exiting...")
sys.exit()

@staticmethod
def options(question, options, default):
assert (default in options) or (default is None), "ERROR: default not in options"

prompt = f"[Default = {default}]" if default is not None else ""

while True:
try:
resp = input(CLI.GAP + question + " " + prompt + " > ").strip().lower()
if default is not None and resp == '':
return default
else:
if resp not in options:
raise ValueError
return resp
except ValueError:
print("ERROR: Please respond with one of the options.")
except EOFError:
print("Encountered EOF, exiting...")
sys.exit()

# https://gist.github.com/garrettdreyfus/8153571?permalink_comment_id=3263216#gistcomment-3263216
@staticmethod
def whats_it_gonna_be_boy(question, default='no') -> bool:
if default is None:
prompt = " [y/n]"
elif default == 'yes':
prompt = " [Y/n]"
elif default == 'no':
prompt = " [y/N]"
else:
raise ValueError(f"Unknown setting '{default}' for default.")

while True:
try:
resp = input(CLI.GAP + question + prompt + " > ").strip().lower()
if default is not None and resp == '':
return (default == 'yes')
else:
return bool(distutils.util.strtobool(resp))
except ValueError:
print("ERROR: Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
except EOFError:
print("Encountered EOF, exiting...")
sys.exit()
211 changes: 211 additions & 0 deletions src/cli-app/m2-app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
#!/usr/bin/env python3

# Made 2022, Sun Yudong
# yudong.sun [at] mpq.mpg.de / yudong [at] outlook.de

# Possible Improvements
# - Different fitting methods
# - Provide option to choose which way the beam is coming in
# - Some proper way of breaking operations

from email.policy import default
import os, sys
from matplotlib.style import available
import serial.tools.list_ports

from cli import CLI

try:
import nanosquared
print("Using pip-installed version (may not be up-to-date).")
print("If you have recently updated the repository, do `pip install .` to update the installed version with the one in the repository.")
except ModuleNotFoundError as e:
base_dir = os.path.dirname(os.path.realpath(__file__))
root_dir = os.path.abspath(os.path.join(base_dir, ".."))
sys.path.insert(0, root_dir)

import nanosquared

# https://asciiflow.com/

def setup():
while True:
print(f"{CLI.COLORS.HEADER}==== SETUP ===={CLI.COLORS.ENDC}")
print("\nIn the following questions, pressing Enter will enter the default option, which is indicated in capital letters.")
print("\nIn development mode, no actual devices are required.\nAll function calls will therefore be simulated.")
print("Use this mode if you only want to fit.")
devMode = CLI.whats_it_gonna_be_boy("Run in development mode?")

print("\nNanoScan and WinCamD Beam Profilers are supported. \nyes = NanoScan, no = WinCamD")
useNanoScan = CLI.whats_it_gonna_be_boy("Use NanoScan?", default = 'yes')

ports = serial.tools.list_ports.comports()

print("\nAvailable COM Ports")
available_ports = []
default_port = None
for port, desc, hwid in sorted(ports):
port_num = port[3:]
available_ports.append(port_num)

if "communication" in desc.lower() or "comm" in desc.lower():
default_port = port_num

print("| {}: {} [{}]".format(port, desc, hwid))

if len(available_ports) < 1:
print("Error: No COM ports available. Exiting...")
sys.exit()

if default_port is None:
default_port = available_ports[0]

comPort = int(CLI.options("Which COM Port for stage?", options = available_ports, default = default_port))

print(f"{CLI.COLORS.HEADER}Obtained:\n--- devMode : {devMode}\n--- Profiler : {'NanoScan' if useNanoScan else 'WinCamD'}\n--- COM Port : COM{comPort}{CLI.COLORS.ENDC}")
confirm = CLI.whats_it_gonna_be_boy(f"Proceed?", default = "yes")

if confirm:
break
CLI.clear_screen()

return devMode, useNanoScan, comPort

CLI.clear_screen()
print(f"""
┌──────────────────────────────────────┐
│ │
{CLI.COLORS.OKCYAN}Welcome to M² Measurement Wizard{CLI.COLORS.ENDC}
│ │
│ Made 2021-2022, Yudong Sun │
│ github.com/sunjerry019/nanosquared │
│ │
└──────────────────────────────────────┘
""")

devMode, useNanoScan, comPort = setup()

cfg = { "port" : f"COM{comPort}" }

cam = nanosquared.cameras.nanoscan.NanoScan if useNanoScan else nanosquared.cameras.wincamd.WinCamD

print(f"{CLI.COLORS.OKGREEN}Got it! Initialising...{CLI.COLORS.ENDC}")
with cam(devMode = devMode) as n:
with nanosquared.stage.controller.GSC01(devMode = devMode, devConfig = cfg) as s:
with nanosquared.measurement.measure.Measurement(devMode = devMode, camera = n, controller = s) as M:
print(f"{CLI.COLORS.OKGREEN}Initialisation done!{CLI.COLORS.ENDC}")

print("")
CLI.print_sep()
print(f"{CLI.COLORS.FAIL}IMPT{CLI.COLORS.ENDC}\n{CLI.COLORS.FAIL}IMPT{CLI.COLORS.ENDC}: If you happen to quit halfway through, use the Task Manager > Processes to ensure that no NanoScanII.exe instances are running before restarting this wizard.\n{CLI.COLORS.FAIL}IMPT{CLI.COLORS.ENDC}")
CLI.print_sep()
print("")

ic = CLI.whats_it_gonna_be_boy("Launch Interactive Console?")

def launchInteractive():
CLI.print_sep()
print(f"\n{CLI.COLORS.OKCYAN}with nanosquared.measurement.measure.Measurement(devMode = {devMode}) as M{CLI.COLORS.ENDC}")
import code; code.interact(local=locals())

if ic:
launchInteractive()
else:
if not devMode:
print("Assuming you want to take a measurement...")
while True:
while True:
wavelength = CLI.getPositiveNonZeroFloat("Laser Wavelength (nm) ?")
precision = CLI.getIntWithLimit("Precision of search? (pulses) ?", default = 10, lowerlimit = 2)
other = input(CLI.GAP + "Other metadata > ")

print(f"{CLI.COLORS.HEADER}Obtained:\n--- Wavelength : {wavelength} nm\n--- Precision : {precision} pps\n--- Other Metadata : {other}{CLI.COLORS.ENDC}")
confirm = CLI.whats_it_gonna_be_boy(f"Proceed?", default = "yes")

if confirm:
break

meta = {
"Wavelength" : f"{wavelength} nm",
"Precision (pps)" : precision,
"Metadata" : other
}
M.take_measurements(precision = precision, metadata = meta)

print(f"{CLI.COLORS.OKGREEN}Done!{CLI.COLORS.ENDC}")

print(f"{CLI.COLORS.OKGREEN}Fitting data (X-Axis)...{CLI.COLORS.ENDC}")
res = M.fit_data(axis = M.camera.AXES.X, wavelength = wavelength)
print(f"{CLI.COLORS.OKGREEN}=== X-Axis ==={CLI.COLORS.ENDC}")
print(f"{CLI.COLORS.OKGREEN}=== Fit Result{CLI.COLORS.ENDC}: {res}")
print(f"{CLI.COLORS.OKGREEN}=== M-squared{CLI.COLORS.ENDC} : {M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()

CLI.presskeycont()

print(f"{CLI.COLORS.OKGREEN}Fitting data (Y-Axis)...{CLI.COLORS.ENDC}")
res = M.fit_data(axis = M.camera.AXES.Y, wavelength = wavelength) # Use defaults (same as above)
print(f"{CLI.COLORS.OKGREEN}=== Y-Axis ==={CLI.COLORS.ENDC}")
print(f"{CLI.COLORS.OKGREEN}=== Fit Result{CLI.COLORS.ENDC}: {res}")
print(f"{CLI.COLORS.OKGREEN}=== M-squared{CLI.COLORS.ENDC} : {M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()

print(f"{CLI.COLORS.OKGREEN}All Done!{CLI.COLORS.ENDC}")

ic2 = CLI.whats_it_gonna_be_boy("Launch Interactive Console?")
if ic2:
launchInteractive()

anothermeasurement = CLI.whats_it_gonna_be_boy("Take another measurement?")
if not anothermeasurement:
break
else:
print("Assuming you want to fit...")
while True:
while True:
wavelength = CLI.getPositiveNonZeroFloat("Laser Wavelength (nm) ?")

print(f"{CLI.COLORS.HEADER}Obtained:\n--- Wavelength : {wavelength} nm{CLI.COLORS.ENDC}")
confirm = CLI.whats_it_gonna_be_boy(f"Proceed?", default = "yes")

if confirm:
break

while True:
try:
filename = input(CLI.GAP + "Filename > ")
M.read_from_file(filename = filename)
break
except OSError as e:
print(f"OSError: {e}. Try again.")

print(f"{CLI.COLORS.OKGREEN}Fitting data (X-Axis)...{CLI.COLORS.ENDC}")
res = M.fit_data(axis = M.camera.AXES.X, wavelength = wavelength)
print(f"{CLI.COLORS.OKGREEN}=== X-Axis ==={CLI.COLORS.ENDC}")
print(f"{CLI.COLORS.OKGREEN}=== Fit Result{CLI.COLORS.ENDC}: {res}")
print(f"{CLI.COLORS.OKGREEN}=== M-squared{CLI.COLORS.ENDC} : {M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()

CLI.presskeycont()

print(f"{CLI.COLORS.OKGREEN}Fitting data (Y-Axis)...{CLI.COLORS.ENDC}")
res = M.fit_data(axis = M.camera.AXES.Y, wavelength = wavelength) # Use defaults (same as above)
print(f"{CLI.COLORS.OKGREEN}=== Y-Axis ==={CLI.COLORS.ENDC}")
print(f"{CLI.COLORS.OKGREEN}=== Fit Result{CLI.COLORS.ENDC}: {res}")
print(f"{CLI.COLORS.OKGREEN}=== M-squared{CLI.COLORS.ENDC} : {M.fitter.m_squared}")
fig, ax = M.fitter.getPlotOfFit()
fig.show()


print(f"{CLI.COLORS.OKGREEN}All Done!{CLI.COLORS.ENDC}")

ic2 = CLI.whats_it_gonna_be_boy("Launch Interactive Console?")
if ic2:
launchInteractive()

anotherfit = CLI.whats_it_gonna_be_boy("Fit another?")
if not anotherfit:
break
File renamed without changes.
3 changes: 2 additions & 1 deletion src/nanosquared/cameras/nanoscan.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ def waitForData(self) -> bool:
# return x

def __exit__(self, e_type, e_val, traceback):
self.NS.__exit__(e_type, e_val, traceback)
if not self.devMode:
self.NS.__exit__(e_type, e_val, traceback)
return super(NanoScan, self).__exit__(e_type, e_val, traceback)

class NanoScanDLL(Client64):
Expand Down
Loading

0 comments on commit bf7afbb

Please sign in to comment.