I ended up purchasing the Tykma Electrox LaserGear BOQX for the Joulescope JS220 backside marking. The total price was $18,700 USD including the fume extractor. It’s definitely pricier than other options for the capabilities, but it has a class 1 enclosure my CM trusts with a company we trust. My CM is happy, it is working well, and I’m happy.
I managed to integrate the MiniLase Pro SE software (rebranded EZCAD2) into my python-based final test station. The station performs some testing on the unit and reads off the device info before firing up the laser. The test station runs a python socket server process to communicate the data between the test station python (based upon pytation) and the laser software.
The test station publishes the serial number and manufacturing date to the socket server. The EZCAD2 software has the option to query values from a socket server, and it pulls down these values for each run. I also could not figure out how to fully automate the end time. Pywinauto seemed to work initially but then became unreliable. Not sure why. So, it’s on a fixed delay. A bit hacked together, but it’s working well enough so far.
See it in action:
And here is the python code implemented as a pytation device:
# Copyright 2022 Jetperch LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pytation import Context
import asyncio
import logging
import os
import pywinauto
import socket
import sys
import threading
import time
HOST = '127.0.0.1'
PORT = 1000
EXE = r'C:\Program Files (x86)\Minilase Pro SE\Minilase Pro SE.exe'
PSE = r'C:\repos\template.pse'
class Boqx:
"""The TYKMA Electrox LaserGear BOQX laser.
This laser used the MiniLase Pro SE software (rebranded ezcad2).
"""
NAME = f'BOQX laser'
def __init__(self):
self.parameter = {
'serial_number': 'xxxxxx',
'production_date': 'xxxx-xx-xx'
}
self._port = PORT
self._thread = None
self._log = logging.getLogger(__name__)
self._asyncio_loop = None
self._app = None
self._win = None
def setup(self, context: Context):
if context is None:
exe = EXE
pse = PSE
else:
exe = context.config.get('exe', EXE)
pse = context.config.get('pse', PSE)
appname = 'Minilase Pro SE - ' + os.path.splitext(os.path.basename(pse))[0]
self._app = pywinauto.Application().start(f'{exe} {pse}')
self._win = self._app[appname]
# self._win.Properties.print_control_identifiers()
# self._win.menu_items()
self._win.minimize()
self.server_start()
def server_start(self):
self._thread = threading.Thread(target=self._run_thread)
self._thread.start()
def server_stop(self):
client('!stop:')
self._thread.join()
def restore(self):
self.parameter['serial_number'] = 'xxxxxx'
self.parameter['production_date'] = 'xxxx-xx-xx'
def teardown(self):
self._win.menu_select("File->Exit")
time.sleep(0.1)
dlg = self._app.top_window()
dlg.No.click()
self.server_stop()
def mark(self, timeout_s=None):
timeout_s = 60.0 if timeout_s is None else float(timeout_s)
self._win.send_keystrokes('{F2}')
t_start = time.time()
print('mark: wait start')
while (time.time() - t_start) < timeout_s:
time.sleep(0.5)
print('mark: timeout')
async def _handle_client(self, reader, writer):
print('client: connected')
try:
while True:
cmd = await reader.readuntil(b':')
cmd = cmd.decode('utf-8')[:-1]
print(f'command: {cmd}')
if cmd in self.parameter:
rsp = self.parameter[cmd] + '\x00'
elif cmd == '!update':
line = await reader.readuntil(b':')
line = line.decode('utf-8')[:-1]
print(f' {line}')
for key, value in [x.split('=') for x in line.split(',')]:
if key in self.parameter:
self.parameter[key] = value
else:
self._log.warning(f'unsupported key: {key}')
rsp = 'OK\n'
elif cmd == '!stop':
self._asyncio_loop.stop()
rsp = 'OK\n'
else:
self._log.warning(f'Unsupported command: {cmd}')
rsp = 'ERROR\n'
rsp = rsp.encode('utf-8')
writer.write(rsp)
await writer.drain()
except asyncio.IncompleteReadError:
print('IncompleteReadError')
print('client: disconnect')
writer.close()
def _run_thread(self):
loop = asyncio.new_event_loop()
self._asyncio_loop = loop
asyncio.set_event_loop(loop)
coro = asyncio.start_server(self._handle_client, HOST, self._port)
server = loop.run_until_complete(coro)
# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
loop.run_forever()
except KeyboardInterrupt:
pass
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
def client(cmd):
if isinstance(cmd, str):
cmd = cmd.encode('utf-8')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(cmd)
rsp = s.recv(4096)
rsp = rsp.decode('utf-8')
print(rsp)
s.close()