Compare commits

...

14 Commits

Author SHA1 Message Date
linuswck 1896e2534b main: Add PrepareForDfu state 2024-09-04 16:12:06 +08:00
linuswck 6d4b1b0574 driver: end_session if ConnClose & Dfu is recv-ed 2024-09-04 16:10:42 +08:00
linuswck 0b5fda2cd9 net: close oldest sock if available socks run out
- By default, smoltcp by default reset any new connections
    if there is no available socket
2024-09-04 15:46:43 +08:00
linuswck b763350a8b net: disable IRQ when data is being enqueued
- This prevents broken json to be sent out due to IRQ
2024-09-04 15:46:43 +08:00
linuswck 838592c812 gui: Display hw_rev when connected 2024-09-04 15:46:43 +08:00
linuswck e632cbbfdd driver: use readuntil to get response msg
- The driver may recv partial ctrl msg, which causes a json decode error
2024-09-04 15:46:43 +08:00
linuswck 27bf573010 driver: retrieve hw_rev after conn is established 2024-09-04 15:45:27 +08:00
linuswck c267c30b89 driver: add get_hw_rev() fn 2024-09-04 15:39:27 +08:00
linuswck c5826876a6 firmware: Add cmd to get hw_rev 2024-09-04 15:39:27 +08:00
linuswck 6782cda790 Update pid autotuner code 2024-09-04 15:39:27 +08:00
linuswck 2fe2ef531b Update example code 2024-09-04 15:39:27 +08:00
linuswck f2ad06ecae gui: patch the GUI code 2024-09-04 15:39:27 +08:00
linuswck 5166bb7ba8 driver: rm thread use & make it asyncio callable
- make all cmds asyncio callable
- control specific cmds can be enqueued to the handler synchronously
2024-09-04 15:39:21 +08:00
linuswck 9c611fc861 main: poll iface for dfu & hard rst ctrl msgs 2024-09-03 12:15:03 +08:00
10 changed files with 626 additions and 475 deletions

View File

@ -73,7 +73,7 @@
src = "${self}/pykirdy"; src = "${self}/pykirdy";
nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ]; nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ];
propagatedBuildInputs = [ pkgs.qt6.qtbase ] ++ (with pkgs.python3Packages; [ pyqtgraph pyqt6 qasync pglive aenum]); propagatedBuildInputs = [ pkgs.qt6.qtbase ] ++ (with pkgs.python3Packages; [ pyqtgraph pyqt6 qasync pglive aenum sipyco]);
dontWrapQtApps = true; dontWrapQtApps = true;
postFixup = '' postFixup = ''
@ -94,7 +94,7 @@
buildInputs = with pkgs; [ buildInputs = with pkgs; [
rust openocd dfu-util glibc rust openocd dfu-util glibc
] ++ (with python3Packages; [ ] ++ (with python3Packages; [
numpy matplotlib pyqtgraph setuptools pyqt6 qasync pglive aenum numpy matplotlib pyqtgraph setuptools pyqt6 qasync pglive aenum sipyco
]); ]);
shellHook= shellHook=
'' ''

View File

@ -2,147 +2,135 @@ from pprint import pp
from driver.kirdy import Kirdy, FilterConfig from driver.kirdy import Kirdy, FilterConfig
import signal import signal
import time import time
import asyncio
""" async def enter_dfu_mode(kirdy: Kirdy):
Enter Device Firmware Upgrade(Dfu) mode """
Please see README.md for flash instructions. Enter Device Firmware Upgrade(Dfu) mode
""" Please refer to README.md for firmware update instructions.
def enter_dfu_mode(kirdy: Kirdy): """
kirdy.device.dfu() await kirdy.device.dfu()
kirdy.end_session()
""" async def active_report(kirdy: Kirdy):
Configure Kirdy to actively report status """
Press Ctrl + C to exit active report mode Configure Kirdy to actively report status to connected socket
""" Press Ctrl + C to exit active report mode
def active_report(kirdy: Kirdy): """
class SignalHandler: async for data in kirdy.report_mode():
def __init__(self):
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
kirdy.stop_report_mode()
signal_handler = SignalHandler()
for data in kirdy.get_report_stream():
pp(data) pp(data)
""" async def device_cfg(kirdy: Kirdy):
Configure Kirdy network and board specific transconductance settings. """
These configs are saved to flash immediately after command is processed. Configure Kirdy's network and board specific transconductance settings.
""" These configs are saved to flash immediately after command is processed.
def device_cfg(kirdy: Kirdy): """
# Kirdy rev0_3's gain and transconductance varies between boards to maximize the # Kirdy rev0_3's gain and transconductance varies between boards to maximize the
# PD current range resolution. # PD current range resolution.
kirdy.device.set_pd_mon_fin_gain(1.0) await kirdy.device.set_pd_mon_fin_gain(1.0)
kirdy.device.set_pd_mon_transconductance(1/1000.0) await kirdy.device.set_pd_mon_transconductance(1/1000.0)
# Network Settings will be updated on next reboot. # Network Settings will be updated on next reboot.
kirdy.device.set_ip_settings( await kirdy.device.set_ip_settings(
addr="192.168.1.128", addr="192.168.1.128",
port=1337, port=1337,
prefix_len=24, prefix_len=24,
gateway="192.168.1.1" gateway="192.168.1.1"
) )
# Hard reset Kirdy. # Hard reset Kirdy.
kirdy.device.hard_reset() await kirdy.device.hard_reset()
""" async def ld_thermostat_cfg(kirdy: Kirdy):
Control and config laser diode and thermostat parameters. """
""" Control and config laser diode and thermostat parameters.
def ld_thermostat_cfg(kirdy: Kirdy): """
# Load the laser diode and thermostat settings from flash # Load the laser diode and thermostat settings from flash memory.
kirdy.device.restore_settings_from_flash() await kirdy.device.restore_settings_from_flash()
# Power off the laser diode and thermostat and clear alarm (if any) # Power off the laser diode & thermostat and clear any asserted alarm
kirdy.laser.set_power_on(False) await kirdy.laser.set_power_on(False)
kirdy.laser.clear_alarm() await kirdy.laser.clear_alarm()
kirdy.thermostat.set_power_on(False) await kirdy.thermostat.set_power_on(False)
kirdy.thermostat.clear_alarm() await kirdy.thermostat.clear_alarm()
# Set the laser diode terminals not to be shorted # Set the laser diode terminals not to be shorted
kirdy.laser.set_ld_terms_short(False) await kirdy.laser.set_ld_terms_short(False)
# Do not power up the laser & thermostat during initial startup # Do not power up the laser & thermostat during initial startup
kirdy.laser.set_default_pwr_on(False) await kirdy.laser.set_default_pwr_on(False)
kirdy.thermostat.set_default_pwr_on(False) await kirdy.thermostat.set_default_pwr_on(False)
kirdy.laser.set_i(0) await kirdy.laser.set_i(0)
# Configure the laser diode output power limit and photodiode parameters # Configure the laser diode output power limit and photodiode parameters
# Exceeding the measured power limit triggers overpower protection alarm. # Exceeding the power limit triggers overpower protection alarm.
# The laser diode power will be turned off while the thermostat power remains unchanged. # The laser diode power will be cut off upon alarm assertion while the thermostat power remains unchanged.
kirdy.laser.set_ld_pwr_limit(0.0) await kirdy.laser.set_ld_pwr_limit(0.0)
kirdy.laser.set_pd_mon_dark_current(0.0) await kirdy.laser.set_pd_mon_dark_current(0.0)
kirdy.laser.set_pd_mon_responsitivity(0.0) await kirdy.laser.set_pd_mon_responsitivity(0.0)
# Configure the thermostat NTC thermistor parameters. # Configure the thermostat NTC thermistor parameters.
kirdy.thermostat.set_sh_r0(10.0 * 1000) await kirdy.thermostat.set_sh_r0(10.0 * 1000)
kirdy.thermostat.set_sh_t0(25) await kirdy.thermostat.set_sh_t0(25)
kirdy.thermostat.set_sh_beta(3900) await kirdy.thermostat.set_sh_beta(3900)
# Configure the thermostat output limits. # Configure the thermostat TEC settings.
# The actual output current is limited by the hardware limit set below. # The actual output current is limited by value set below.
kirdy.thermostat.set_tec_max_cooling_i(1.0) await kirdy.thermostat.set_tec_max_cooling_i(1.0)
kirdy.thermostat.set_tec_max_heating_i(1.0) await kirdy.thermostat.set_tec_max_heating_i(1.0)
kirdy.thermostat.set_tec_max_v(4.0) await kirdy.thermostat.set_tec_max_v(4.0)
# Configure the thermostat temperature monitor limit. # Configure the thermostat temperature monitor limits.
# Exceeding the limit will trigger over temperature protection alarm. # Exceeding the temperature limits trigger over temperature protection alarm.
# The laser diode and thermostat power will be turned off. # Both laser diode and thermostat power will be cut off upon alarm assertion.
kirdy.thermostat.set_temp_mon_upper_limit(70) await kirdy.thermostat.set_temp_mon_upper_limit(70)
kirdy.thermostat.set_temp_mon_lower_limit(0) await kirdy.thermostat.set_temp_mon_lower_limit(0)
# Configure the thermostat PID related parameter. # Configure the thermostat PID parameters.
# You can configure the PID parameter with the autotune tool. # You can configure the PID parameter by the included autotune script.
# Here provides an example if it is configured manually. await kirdy.thermostat.set_temperature_setpoint(25)
kirdy.thermostat.set_temperature_setpoint(25) await kirdy.thermostat.set_pid_kp(0.15668282198105507)
kirdy.thermostat.set_pid_kp(0.15668282198105507) await kirdy.thermostat.set_pid_ki(0.002135962407793784)
kirdy.thermostat.set_pid_ki(0.002135962407793784) await kirdy.thermostat.set_pid_kd(0.829254515277143)
kirdy.thermostat.set_pid_kd(0.829254515277143) await kirdy.thermostat.set_pid_output_max(1.0)
kirdy.thermostat.set_pid_output_max(1.0) await kirdy.thermostat.set_pid_output_min(-1.0)
kirdy.thermostat.set_pid_output_min(-1.0)
# Configure the thermostat ADC Filter Setting / PID Update Rate / Report Rate. # Configure the thermostat ADC Filter Setting / PID Update Rate / Report Rate.
# The ADC sampling rate determines the report and pid update rate. # The ADC sampling rate determines the report and pid update rate.
# The chosen filter and sampling rate affects the noise of the readings. # The chosen filter and sampling rate affects the noise of the readings.
# For details, please refer to the AD7172 da`tas`heet. # For details, please refer to the AD7172 datasheet.
kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps) await kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps)
# Configure thermostat to run in PID control mode # Configure thermostat to run in PID control mode
kirdy.thermostat.set_pid_control_mode() await kirdy.thermostat.set_pid_control_mode()
# When control mode is switched from PID to constant current(CC) control mode, # When control mode is switched from PID to constant current(CC) control mode,
# the thermostat keeps its instantaneous output current unchanged. # thermostat keeps its instantaneous output current unchanged.
# Thermostat output current should only be set if it is in CC control mode # Thermostat output current should only be set if it is in CC control mode
# or the value set will not be overwritten by PID output. # or the value set will be overwritten by PID output.
kirdy.thermostat.set_constant_current_control_mode() await kirdy.thermostat.set_constant_current_control_mode()
kirdy.thermostat.set_tec_i_out(0.0) await kirdy.thermostat.set_tec_i_out(0.0)
# Save the above settings configured into the flash # Save the current settings to flash memory
kirdy.device.save_current_settings_to_flash() await kirdy.device.save_current_settings_to_flash()
# Power on the laser diode and thermostat # Power on the laser diode and thermostat
kirdy.laser.set_power_on(True) await kirdy.laser.set_power_on(True)
kirdy.thermostat.set_power_on(True) await kirdy.thermostat.set_power_on(True)
pp(kirdy.device.get_settings_summary()) pp(await kirdy.device.get_settings_summary())
pp(kirdy.device.get_status_report()) pp(await kirdy.device.get_status_report())
def main(): async def main():
kirdy = Kirdy() kirdy = Kirdy()
kirdy.start_session(host='192.168.1.128', port=1337) kirdy.start_session(host='192.168.1.128', port=1337)
await kirdy.wait_until_connected()
while not(kirdy.connected()): await ld_thermostat_cfg(kirdy)
pass # await active_report(kirdy)
ld_thermostat_cfg(kirdy)
# active_report(kirdy)
# await device_cfg(kirdy) # await device_cfg(kirdy)
# enter_dfu_mode(kirdy) # await enter_dfu_mode(kirdy)
kirdy.end_session(block=True) await kirdy.end_session()
if __name__ == "__main__": if __name__ == "__main__":
main() asyncio.run(main())

View File

@ -1,3 +1,4 @@
import types
import socket import socket
import json import json
import logging import logging
@ -25,6 +26,7 @@ class CmdList:
SetPdFinGain = _dt.f32 SetPdFinGain = _dt.f32
SetPdTransconductance = _dt.f32 SetPdTransconductance = _dt.f32
SetActiveReportMode = _dt.bool SetActiveReportMode = _dt.bool
GetHwRev = _dt.none
GetStatusReport = _dt.none GetStatusReport = _dt.none
GetSettingsSummary = _dt.none GetSettingsSummary = _dt.none
Dfu = _dt.none Dfu = _dt.none
@ -153,15 +155,14 @@ class InvalidCmd(Exception):
pass pass
class Device: class Device:
def __init__(self, send_cmd_handler, send_raw_cmd_handler, read_msg_queue): def __init__(self, send_cmd_handler, send_raw_cmd_handler):
self._cmd = CmdList.device self._cmd = CmdList.device
self._send_cmd = send_cmd_handler self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler self._send_raw_cmd = send_raw_cmd_handler
self._read_msg_queue = read_msg_queue
def set_ip_settings(self, addr="192.168.1.128", port=1337, prefix_len=24, gateway="192.168.1.1"): async def set_ip_settings(self, addr="192.168.1.128", port=1337, prefix_len=24, gateway="192.168.1.1"):
""" """
After calling this fn, the ip settings are immediately saved into flash and will be effective on next reboot. Upon command execution, the ip settings are saved into flash and are effective upon next reboot.
""" """
try: try:
socket.inet_aton(addr) socket.inet_aton(addr)
@ -175,7 +176,7 @@ class Device:
if not(isinstance(port, int) and isinstance(prefix_len, int)): if not(isinstance(port, int) and isinstance(prefix_len, int)):
raise InvalidDataType raise InvalidDataType
return self._send_raw_cmd( return await self._send_raw_cmd(
{ {
"device_cmd": "SetIPSettings", "device_cmd": "SetIPSettings",
"ip_settings": { "ip_settings": {
@ -187,32 +188,45 @@ class Device:
} }
) )
def set_active_report_mode(self, on): async def set_active_report_mode(self, on):
""" """
Set active report to be on. If it is on, Kirdy will send status report Set active report to be on. If it is on, Kirdy will send status report
to ALL client socket connections according to the temperature polling rate set. to the client socket according to the temperature polling rate set.
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetActiveReportMode, on) return await self._send_cmd(self._cmd._target, self._cmd.SetActiveReportMode, on)
def set_pd_mon_fin_gain(self, gain): async def set_pd_mon_fin_gain(self, gain):
""" """
Configure the photodiode monitor final analog front-end stage gain Configure the photodiode monitor final analog front-end stage gain.
- gain: unitless - gain: unitless
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPdFinGain, gain) return await self._send_cmd(self._cmd._target, self._cmd.SetPdFinGain, gain)
def set_pd_mon_transconductance(self, transconductance): async def set_pd_mon_transconductance(self, transconductance):
""" """
Configure the photodiode monitor transconductance Configure the photodiode monitor transconductance value.
- transconductance: 1/Ohm - transconductance: 1/Ohm
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPdTransconductance, transconductance) return await self._send_cmd(self._cmd._target, self._cmd.SetPdTransconductance, transconductance)
def get_status_report(self, sig=None): async def get_hw_rev(self):
""" """
Get status of all peripherals in a json object Get hardware revision of the connected Kirdy
{
'msg_type': 'HwRev',
'hw_rev': {
'major': 0,
'minor': 3
}
}
"""
return await self._send_cmd(self._cmd._target, self._cmd.GetHwRev, msg_type="HwRev")
async def get_status_report(self, sig=None):
"""
Get status of all peripherals in a json object.
Example of yielded data::
{ {
'ts': 227657, # Relative Timestamp (ms) 'ts': 227657, # Relative Timestamp (ms)
'msg_type': 'Report' # Indicate it is a 'Report' json object 'msg_type': 'Report' # Indicate it is a 'Report' json object
@ -222,13 +236,17 @@ class Device:
'ld_i_set': 0.0, # Laser Diode Output Current (A) 'ld_i_set': 0.0, # Laser Diode Output Current (A)
'pd_i': 2.0000002e-06, # Internal Photodiode Monitor current (A) 'pd_i': 2.0000002e-06, # Internal Photodiode Monitor current (A)
'pd_pwr': None, # Power Readings from Internal Photodiode (W). Return None if pd_mon parameter(s) are not defined. 'pd_pwr': None, # Power Readings from Internal Photodiode (W). Return None if pd_mon parameter(s) are not defined.
'term_50ohm': 'Is50Ohm' # Is the Low Frequency Modulation Input's Impedance 50 Ohm? (On/Off) 'term_50ohm': 'On' # Is the Low Frequency Modulation Input's Impedance 50 Ohm? ("On"/"Off")
}, },
'thermostat': { 'thermostat': {
'pwr_on': False, # Tec Power is On (True/False) 'pwr_on': False, # Tec Power is On (True/False)
'pid_engaged': False, # Is Pid_Engaged. If False, it is in Constant Current Mode (True/False) 'pid_engaged': False, # Is Pid_Engaged. If False, it is in Constant Current Mode (True/False)
'temp_mon_status': { # Temperature Monitor: 'temp_mon_status': { # Temperature Monitor:
'status': 'Off', # (To be revised) 'status': 'Off', # "Off": Power is Off
# "ConstantCurrentMode": Thermostat is regulated in CC mode
# "PidStartUp": PID Regulation is not stable
# "PidStable": PID Regulation is stable and the temperature is within +-1mK to the setpoint
# "OverTempAlarm": Overtemperature Alarm is triggered
'over_temp_alarm': False # Was Laser Diode experienced an Overtemperature condition (True/False) 'over_temp_alarm': False # Was Laser Diode experienced an Overtemperature condition (True/False)
}, },
'temperature': 25.03344, # Temperature Readings (Degree Celsius) 'temperature': 25.03344, # Temperature Readings (Degree Celsius)
@ -238,15 +256,11 @@ class Device:
} }
} }
""" """
if sig is None: return await self._send_cmd(self._cmd._target, self._cmd.GetStatusReport, msg_type="Report", sig=sig)
self._send_cmd(self._cmd._target, self._cmd.GetStatusReport, msg_type="Report", sig=sig)
return self._read_msg_queue()
else:
return self._send_cmd(self._cmd._target, self._cmd.GetStatusReport, msg_type="Report", sig=sig)
def get_settings_summary(self, sig=None): async def get_settings_summary(self, sig=None):
""" """
Get the current settings of laser and thermostat in a json object Get the current settings of laser and thermostat in a json object.
{ {
'msg_type': 'Settings', # Indicate it is a 'Settings' json object 'msg_type': 'Settings', # Indicate it is a 'Settings' json object
@ -255,13 +269,13 @@ class Device:
'ld_drive_current': { # Laser Diode Output Current(A) 'ld_drive_current': { # Laser Diode Output Current(A)
'value': 0.0, # Value Set 'value': 0.0, # Value Set
'max': 0.3 # Max Value Settable 'max': 0.3 # Max Value Settable
, },
'pd_mon_params': { # Laser Diode Software Current Limit(A) 'pd_mon_params': { # Laser Diode Software Current Limit(A)
'responsitivity': None, # Value Set 'responsitivity': None, # Value Set
'i_dark': 0.0 # Max Value Settable 'i_dark': 0.0 # Max Value Settable
, },
'ld_pwr_limit': 0.0 # Laser Diode Power Limit(W) 'ld_pwr_limit': 0.0, # Laser Diode Power Limit(W)
'ld_terms_short: False # Is Laser Diode Terminals short? (True/False) 'ld_terms_short: False, # Is Laser Diode Terminals short? (True/False)
}, },
'thermostat': { 'thermostat': {
'default_pwr_on': True, # Power on Thermostat at Startup 'default_pwr_on': True, # Power on Thermostat at Startup
@ -312,106 +326,101 @@ class Device:
} }
} }
""" """
if sig is None: return await self._send_cmd(self._cmd._target, self._cmd.GetSettingsSummary, msg_type="Settings", sig=sig)
self._send_cmd(self._cmd._target, self._cmd.GetSettingsSummary, msg_type="Settings", sig=sig)
return self._read_msg_queue()
else:
return self._send_cmd(self._cmd._target, self._cmd.GetSettingsSummary, msg_type="Settings", sig=sig)
def dfu(self): async def dfu(self):
""" """
Issuing this cmd will HARD RESET the device and Hard reset and put the connected Kirdy into the Dfu mode for firmware update.
put Kirdy into Dfu mode for flashing firmware.
""" """
return self._send_cmd(self._cmd._target, self._cmd.Dfu, hard_reset=True) return await self._send_cmd(self._cmd._target, self._cmd.Dfu)
def save_current_settings_to_flash(self): async def save_current_settings_to_flash(self):
""" """
Save the current laser diode and thermostat configurations into flash. Save the current laser diode and thermostat configurations into flash.
""" """
return self._send_cmd(self._cmd._target, self._cmd.SaveFlashSettings) return await self._send_cmd(self._cmd._target, self._cmd.SaveFlashSettings)
def restore_settings_from_flash(self): async def restore_settings_from_flash(self):
""" """
Restore the laser diode and thermostat settings from flash Restore the laser diode and thermostat settings from flash.
""" """
return self._send_cmd(self._cmd._target, self._cmd.LoadFlashSettings) return await self._send_cmd(self._cmd._target, self._cmd.LoadFlashSettings)
def hard_reset(self): async def hard_reset(self):
""" """
Hard Reset Kirdy. The socket connection will be closed by Kirdy. Hard Reset Kirdy. The socket connection will be closed by Kirdy.
Laser diode power and Tec power will be turned off. Laser diode power and Tec power will be turned off.
Kirdy will send out a json({'msg_type': 'HardReset'}) to all sockets indicating. The device is being reset. Kirdy will send out a json({'msg_type': 'HardReset'}) to all sockets before hard reset take place.
""" """
return self._send_cmd(self._cmd._target, self._cmd.HardReset, hard_reset=True) return await self._send_cmd(self._cmd._target, self._cmd.HardReset)
class Laser: class Laser:
def __init__(self, send_cmd_handler): def __init__(self, send_cmd_handler):
self._cmd = CmdList.ld self._cmd = CmdList.ld
self._send_cmd = send_cmd_handler self._send_cmd = send_cmd_handler
def set_power_on(self, on): async def set_power_on(self, on):
""" """
Power Up or Power Down laser diode. Powering up the Laser Diode resets the pwr_excursion status Power Up or Power Down laser diode. Powering up the Laser Diode resets the pwr_excursion status
- on (True/False) - on (True/False)
""" """
if on: if on:
return self._send_cmd(self._cmd._target, self._cmd.PowerUp, None) return await self._send_cmd(self._cmd._target, self._cmd.PowerUp, None)
else: else:
return self._send_cmd(self._cmd._target, self._cmd.PowerDown, None) return await self._send_cmd(self._cmd._target, self._cmd.PowerDown, None)
def set_default_pwr_on(self, on): async def set_default_pwr_on(self, on):
""" """
Set whether laser diode is powered up at Startup Set whether laser diode is powered up at Startup.
- on (True/False) - on (True/False)
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on) return await self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on)
def set_ld_terms_short(self, short): async def set_ld_terms_short(self, short):
""" """
Open/Short laser diode terminals. Open/Short laser diode terminals.
- on (True/False) - on (True/False)
""" """
if short: if short:
return self._send_cmd(self._cmd._target, self._cmd.LdTermsShort, None) return await self._send_cmd(self._cmd._target, self._cmd.LdTermsShort, None)
else: else:
return self._send_cmd(self._cmd._target, self._cmd.LdTermsOpen, None) return await self._send_cmd(self._cmd._target, self._cmd.LdTermsOpen, None)
def set_i(self, i): async def set_i(self, i):
""" """
Set laser diode output current: Max(0, Min(i_set, i_soft_limit)) Set laser diode output current: Max(0, Min(i_set, 300mA)).
- i: A - i: A
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetI, i) return await self._send_cmd(self._cmd._target, self._cmd.SetI, i)
def set_pd_mon_responsitivity(self, responsitivity): async def set_pd_mon_responsitivity(self, responsitivity):
""" """
Configure the photodiode monitor responsitivity parameter Configure the photodiode monitor responsitivity parameter.
- responsitivity: A/W - responsitivity: A/W
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPdResponsitivity, responsitivity) return await self._send_cmd(self._cmd._target, self._cmd.SetPdResponsitivity, responsitivity)
def set_pd_mon_dark_current(self, dark_current): async def set_pd_mon_dark_current(self, dark_current):
""" """
Configure the photodiode monitor dark current parameter Configure the photodiode monitor dark current parameter.
- dark_current: A - dark_current: A
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPdDarkCurrent, dark_current) return await self._send_cmd(self._cmd._target, self._cmd.SetPdDarkCurrent, dark_current)
def set_ld_pwr_limit(self, pwr_limit): async def set_ld_pwr_limit(self, pwr_limit):
""" """
Set power limit for the power excursion monitor Set the power limit for the power excursion monitor.
If the calculated power with the params of pd_mon > pwr_limit, If the calculated power with the params of pd_mon > pwr_limit,
overpower protection is triggered. overpower protection is triggered.
- pwr_limit: W - pwr_limit: W
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetLdPwrLimit, pwr_limit) return await self._send_cmd(self._cmd._target, self._cmd.SetLdPwrLimit, pwr_limit)
def clear_alarm(self): async def clear_alarm(self):
""" """
Clear the power excursion monitor alarm Clear the power excursion monitor alarm.
""" """
return self._send_cmd(self._cmd._target, self._cmd.ClearAlarm) return await self._send_cmd(self._cmd._target, self._cmd.ClearAlarm)
class Thermostat: class Thermostat:
def __init__(self, send_cmd_handler, send_raw_cmd_handler): def __init__(self, send_cmd_handler, send_raw_cmd_handler):
@ -419,158 +428,158 @@ class Thermostat:
self._send_cmd = send_cmd_handler self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler self._send_raw_cmd = send_raw_cmd_handler
def set_power_on(self, on): async def set_power_on(self, on):
""" """
Power up or power down thermostat Power up or power down thermostat.
- Powering up the thermostat resets the pwr_excursion status - Powering up the thermostat resets the pwr_excursion status
""" """
if on: if on:
return self._send_cmd(self._cmd._target, self._cmd.PowerUp, None) return await self._send_cmd(self._cmd._target, self._cmd.PowerUp, None)
else: else:
return self._send_cmd(self._cmd._target, self._cmd.PowerDown, None) return await self._send_cmd(self._cmd._target, self._cmd.PowerDown, None)
def set_default_pwr_on(self, on): async def set_default_pwr_on(self, on):
""" """
Set whether thermostat is powered up at Startup Set whether thermostat is powered up at Startup.
- on: (True/False) - on: (True/False)
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on) return await self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on)
def set_tec_max_v(self, max_v): async def set_tec_max_v(self, max_v):
""" """
Set Tec Maximum Voltage Across the TEC Terminals Set Tec Maximum Voltage Across the TEC Terminals.
- max_v: V - max_v: V
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetTecMaxV, max_v) return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxV, max_v)
def set_tec_max_cooling_i(self, max_i_pos): async def set_tec_max_cooling_i(self, max_i_pos):
""" """
Set Tec maximum cooling current (Settable Range: 0.0 - 1.0) Set Tec maximum cooling current (Settable Range: 0.0 - 1.0)
- max_i_pos: A - max_i_pos: A
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetTecMaxIPos, max_i_pos) return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxIPos, max_i_pos)
def set_tec_max_heating_i(self, max_i_neg): async def set_tec_max_heating_i(self, max_i_neg):
""" """
Set Tec maximum heating current (Settable Range: 0.0 - 1.0) Set Tec maximum heating current (Settable Range: 0.0 - 1.0)
- max_i_neg: A - max_i_neg: A
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetTecMaxINeg, max_i_neg) return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxINeg, max_i_neg)
def set_tec_i_out(self, i_out): async def set_tec_i_out(self, i_out):
""" """
Set Tec Output Current Set Tec Output Current (Settable Range: 0.0 - 1.0)
This cmd is only effective in constant current control mode This cmd is only effective in constant current control mode
or your newly set value will be overwritten by PID Controller Output or your newly set value will be overwritten by PID Controller Output
- i_out: A - i_out: A
""" """
if isinstance(i_out, float): if isinstance(i_out, float):
return self._send_raw_cmd({"tec_set_i": i_out}) return await self._send_raw_cmd({"tec_set_i": i_out})
elif isinstance(i_out, int): elif isinstance(i_out, int):
return self._send_raw_cmd({"tec_set_i": float(i_out)}) return await self._send_raw_cmd({"tec_set_i": float(i_out)})
else: else:
raise InvalidDataType raise InvalidDataType
def set_constant_current_control_mode(self): async def set_constant_current_control_mode(self):
""" """
Disable PID Controller and output current can be controlled with set_tec_i_out() cmd. Disable PID Controller and output current can be controlled with set_tec_i_out() cmd.
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPidDisEngage, None) return await self._send_cmd(self._cmd._target, self._cmd.SetPidDisEngage, None)
def set_temperature_setpoint(self, temperature): async def set_temperature_setpoint(self, temperature):
""" """
Set Temperature Setpoint for PID Controller. This parameter is not active in constant current control mode Set Temperature Setpoint for PID Controller. This parameter is not active in constant current control mode
- temperature: Degree Celsius - temperature: Degree Celsius
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetTemperatureSetpoint, temperature) return await self._send_cmd(self._cmd._target, self._cmd.SetTemperatureSetpoint, temperature)
def set_pid_control_mode(self): async def set_pid_control_mode(self):
""" """
Enable PID Controller. Its PID Update Interval is controlled by the Temperature ADC polling rate. Enable PID Controller. Its PID Update Interval is controlled by the Temperature ADC polling rate.
Please refer to config_temp_adc_filter for the possible polling rate options Please refer to config_temp_adc_filter for the possible polling rate options
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPidEngage, None) return await self._send_cmd(self._cmd._target, self._cmd.SetPidEngage, None)
def set_pid_kp(self, kp): async def set_pid_kp(self, kp):
""" """
Set Kp parameter for PID Controller Set Kp parameter for PID Controller
kp: (unitless) kp: (unitless)
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPidKp, kp) return await self._send_cmd(self._cmd._target, self._cmd.SetPidKp, kp)
def set_pid_ki(self, ki): async def set_pid_ki(self, ki):
""" """
Set Ki parameter for PID Controller Set Ki parameter for PID Controller
ki: (unitless) ki: (unitless)
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPidKi, ki) return await self._send_cmd(self._cmd._target, self._cmd.SetPidKi, ki)
def set_pid_kd(self, kd): async def set_pid_kd(self, kd):
""" """
Set Kd parameter for PID Controller Set Kd parameter for PID Controller
kd: (unitless) kd: (unitless)
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPidKd, kd) return await self._send_cmd(self._cmd._target, self._cmd.SetPidKd, kd)
def set_pid_output_max(self, out_max): async def set_pid_output_max(self, out_max):
""" """
Set max output limit at the PID Output Set max output limit at the PID Output
- out_max: A - out_max: A
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPidOutMax, out_max) return await self._send_cmd(self._cmd._target, self._cmd.SetPidOutMax, out_max)
def set_pid_output_min(self, out_min): async def set_pid_output_min(self, out_min):
""" """
Set min output limit at the PID Output Set min output limit at the PID Output
- out_min: A - out_min: A
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetPidOutMin, out_min) return await self._send_cmd(self._cmd._target, self._cmd.SetPidOutMin, out_min)
def set_temp_mon_upper_limit(self, upper_limit): async def set_temp_mon_upper_limit(self, upper_limit):
""" """
Set Temperature Monitor Upper Limit Threshold. Exceeding the limit for too long Set Temperature Monitor Upper Limit Threshold. Exceeding the limit for too long
will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown
- upper_limit: Degree Celsius - upper_limit: Degree Celsius
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetTempMonUpperLimit, upper_limit) return await self._send_cmd(self._cmd._target, self._cmd.SetTempMonUpperLimit, upper_limit)
def set_temp_mon_lower_limit(self, lower_limit): async def set_temp_mon_lower_limit(self, lower_limit):
""" """
Set Temperature Monitor Lower Limit Threshold. Exceeding the limit for too long Set Temperature Monitor Lower Limit Threshold. Exceeding the limit for too long
will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown will force the TEC Controller, PID Controller and Laser Diode Power to Shutdown
- lower_limit: Degree Celsius - lower_limit: Degree Celsius
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetTempMonLowerLimit, lower_limit) return await self._send_cmd(self._cmd._target, self._cmd.SetTempMonLowerLimit, lower_limit)
def clear_alarm(self): async def clear_alarm(self):
""" """
Clear the temperature monitor alarm Clear the temperature monitor alarm
""" """
return self._send_cmd(self._cmd._target, self._cmd.ClearAlarm) return await self._send_cmd(self._cmd._target, self._cmd.ClearAlarm)
def set_sh_t0(self, t0): async def set_sh_t0(self, t0):
""" """
Set t0 Steinhart-Hart parameter for the laser diode NTC Set t0 Steinhart-Hart parameter for the laser diode NTC
- t0: Degree Celsius - t0: Degree Celsius
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetShT0, t0) return await self._send_cmd(self._cmd._target, self._cmd.SetShT0, t0)
def set_sh_r0(self, r0): async def set_sh_r0(self, r0):
""" """
Set r0 Steinhart-Hart parameter for the laser diode NTC Set r0 Steinhart-Hart parameter for the laser diode NTC
- r0: Ohm - r0: Ohm
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetShR0, r0) return await self._send_cmd(self._cmd._target, self._cmd.SetShR0, r0)
def set_sh_beta(self, beta): async def set_sh_beta(self, beta):
""" """
Set beta Steinhart-Hart parameter for the laser diode NTC Set beta Steinhart-Hart parameter for the laser diode NTC
- beta: (unitless) - beta: (unitless)
""" """
return self._send_cmd(self._cmd._target, self._cmd.SetShBeta, beta) return await self._send_cmd(self._cmd._target, self._cmd.SetShBeta, beta)
def config_temp_adc_filter(self, filter_config): async def config_temp_adc_filter(self, filter_config):
""" """
Configure the temperature adc filter type and sampling rate. Configure the temperature adc filter type and sampling rate.
Please refer to AD7172 datasheet for the usage of various types of filter. Please refer to AD7172 datasheet for the usage of various types of filter.
@ -591,29 +600,38 @@ class Thermostat:
filter_config._odr_type(): filter_config, filter_config._odr_type(): filter_config,
} }
return self._send_raw_cmd(cmd) return await self._send_raw_cmd(cmd)
class Kirdy: class Kirdy:
def __init__(self): def __init__(self):
self.device = Device(self._send_cmd_handler, self._send_raw_cmd_handler, self._get_msg) self.device = Device(self._send_cmd, self._send_raw_cmd)
self.laser = Laser(self._send_cmd_handler) self.laser = Laser(self._send_cmd)
self.thermostat = Thermostat(self._send_cmd_handler, self._send_raw_cmd_handler) self.thermostat = Thermostat(self._send_cmd, self._send_raw_cmd)
self.hw_rev = None
self._task_queue, self._msg_queue, self._int_msg_queue, self._report_queue = None, None, None, None self._task_queue, self._int_msg_queue, self._report_queue = None, None, None
self._timeout = 5.0
self._writer, self._reader = None, None self._writer, self._reader = None, None
self._event_loop = None self._event_loop = None
self._lock = asyncio.Lock()
self._msg_queue_get_report = False self._msg_queue_get_report = False
self._report_mode_on = False self._report_mode_on = False
self._state = State.disconnected
self.read_response_task, self.handler_task = None, None
self._lock = asyncio.Lock()
# PyQt Signal # PyQt Signal
self._report_sig = None # Dict self._report_sig = None # Dict
self._connected_sig = None # Bool self._connected_sig = None # Bool
self.connected_event = None
def get_hw_rev(self):
return self.hw_rev
def set_report_sig(self, sig): def set_report_sig(self, sig):
""" """
Connect a PyQt Signal to the active report output(dict) Connect a PyQt Signal to the active report output(dict). This should be configured before the session is started.
""" """
self._report_sig = sig self._report_sig = sig
@ -625,40 +643,41 @@ class Kirdy:
""" """
self._connected_sig = sig self._connected_sig = sig
def start_session(self, host='192.168.1.128', port=1337, timeout=5.0, con_retry=5.0): def start_session(self, host='192.168.1.128', port=1337):
""" """
Start Kirdy Connection Session. Start Kirdy Connection Session.
A new thread is started to handle TCP connection and task execution in the background. In case of disconnection, all the queued tasks are cleared and the handler task retries TCP connection indefinitely.
In case of disconnection, all the queued tasks are cleared and the thread retries TCP connection indefinitely.
- host: Kirdy's IP Address - host: Kirdy's IP Address
- port: Kirdy's TCP Port - port: Kirdy's Port Number
""" """
self._host, self._ctrl_port = host, port
if self._event_loop is None: if self._event_loop is None:
self._host, self._ctrl_port = host, port try:
self._timeout, self._con_retry = timeout, con_retry self._event_loop = asyncio.get_running_loop()
except:
self._event_loop = asyncio.new_event_loop() self._event_loop = asyncio.new_event_loop()
self._thread = Thread(target=self._event_loop.run_forever) self._event_loop.run_forever()
self._thread.start() self.connected_event = asyncio.Event()
asyncio.run_coroutine_threadsafe(self._handler(), self._event_loop) self.handler_task = asyncio.create_task(self._handler())
return True
else:
logging.warning("Helper Thread has been started.")
return False
def end_session(self, block=False): async def end_session(self, block=False):
""" """
Stop Kirdy's TCP connection and its associated thread. Stop Kirdy's TCP connection and its associated thread.
""" """
if self._event_loop is not None: if self._event_loop is not None:
if block: if block:
while not(self._task_queue.empty()): await self._task_queue.join()
pass
cancel_task = asyncio.run_coroutine_threadsafe(self._stop_handler(), self._event_loop) if self.read_response_task is not None:
while not(cancel_task.done()): self.read_response_task.cancel()
pass await self.read_response_task
self._thread.join() self.read_response_task = None
if self.handler_task is not None:
self.handler_task.cancel()
await self.handler_task
self.handler_task = None
self._writer = None self._writer = None
if self._connected_sig is not None: if self._connected_sig is not None:
@ -668,7 +687,7 @@ class Kirdy:
""" """
Return True if client is connecting Return True if client is connecting
""" """
return not self.connected() and self._event_loop is not None return self._state == State.disconnected and self.read_response_task is not None
def connected(self): def connected(self):
""" """
@ -676,28 +695,43 @@ class Kirdy:
""" """
return self._writer is not None return self._writer is not None
def get_report_stream(self): async def wait_until_connected(self):
""" if not(self.connected()):
Start reporting device status in json object. await self.connected_event.wait()
"""
if self.connected():
self._report_mode_on = True
self.device.set_active_report_mode(True) async def report_mode(self):
"""
Enable and retrieve active report from Kirdy
"""
if self.connected():
self._report_mode_on = True
await self.device.set_active_report_mode(True)
report = None
while self._report_mode_on: while self._report_mode_on:
report = self._report_queue.get() report = await self._report_queue.get()
if isinstance(report, Exception): if not(isinstance(report, dict)):
raise report self.stop_active_report()
yield report else:
yield report
self.device.set_active_report_mode(False)
if isinstance(report, dict):
await self.device.set_active_report_mode(False)
else: else:
raise ConnectionError raise ConnectionError
def stop_report_mode(self): def stop_report_mode(self):
self._report_mode_on = False self._report_mode_on = False
def task_dispatcher(self, awaitable_fn):
"""
Enqueue a task to be handled by the handler.
"""
if self.connected():
self._task_queue.put_nowait(lambda: awaitable_fn)
else:
raise ConnectionError
async def _sock_disconnection_handling(self): async def _sock_disconnection_handling(self):
# Reader needn't be closed # Reader needn't be closed
try: try:
@ -709,11 +743,6 @@ class Kirdy:
self._reader = None self._reader = None
self._writer = None self._writer = None
for i in range(self._msg_queue.maxsize):
if self._msg_queue.full():
self._msg_queue.get_nowait()
self._msg_queue.put_nowait(None)
for i in range(self._report_queue.maxsize): for i in range(self._report_queue.maxsize):
if self._report_queue.full(): if self._report_queue.full():
self._report_queue.get_nowait() self._report_queue.get_nowait()
@ -724,97 +753,93 @@ class Kirdy:
async def _handler(self): async def _handler(self):
try: try:
state = State.disconnected self._state = State.disconnected
first_con = True first_con = True
task = None task = None
while True: while True:
if state == State.disconnected: if self._state == State.disconnected:
try: try:
self.hw_rev = None
await self.__coninit(self._timeout) await self.__coninit(self._timeout)
read_response_fut = asyncio.run_coroutine_threadsafe(self._read_response_handler(), self._event_loop) self.read_response_task = asyncio.create_task(self._read_response_handler())
task = None task = None
logging.debug("Connected") logging.info("Connected to %s:%d", self._host, self._ctrl_port)
hw_rev = await self.device.get_hw_rev()
self.hw_rev = hw_rev["hw_rev"]
if self._connected_sig is not None:
self._connected_sig.emit(True)
self.connected_event.set()
# State Transition # State Transition
state = State.connected self._state = State.connected
except (OSError, TimeoutError): except (OSError, TimeoutError):
if first_con: if first_con:
first_con = False first_con = False
logging.warning("Cannot connect to %s:%d. Retrying in the background", self._host, self._ctrl_port) logging.warning("Cannot connect to %s:%d. Retrying in the background.", self._host, self._ctrl_port)
await asyncio.sleep(self._con_retry) await asyncio.sleep(5.0)
elif state == State.connected: elif self._state == State.connected:
try: try:
task = await self._task_queue.get() task = await self._task_queue.get()
if isinstance(task, Exception): if isinstance(task, Exception):
raise task raise task
await task()
response = await asyncio.wait_for(task(), self._timeout) self._task_queue.task_done()
if response is not None:
if response["msg_type"] != "Acknowledge":
self._msg_queue.put_nowait(response)
except (TimeoutError, ConnectionResetError): except (TimeoutError, ConnectionResetError, ConnectionError):
logging.warning("Kirdy connection is dropped.") logging.warning("Connection to Kirdy is dropped.")
first_con = True first_con = True
read_response_fut.cancel() self.read_response_task.cancel()
await self._sock_disconnection_handling()
# State Transition
state = State.disconnected
# State Transition
self._state = State.disconnected
await self._sock_disconnection_handling()
except asyncio.exceptions.CancelledError: except asyncio.exceptions.CancelledError:
logging.debug("Handler is canceling") pass
except: except:
logging.debug("Handler experienced an error. Exiting.", exc_info=True) logging.warning("Handler experienced an error.", exc_info=True)
await self._sock_disconnection_handling() await self._sock_disconnection_handling()
if self._event_loop is not None:
self._event_loop.call_soon_threadsafe(self._event_loop.stop)
self._event_loop = None
async def _read_response_handler(self): async def _read_response_handler(self):
try: try:
while True: while True:
if self._report_mode_on: if self._report_mode_on:
responses = await asyncio.wait_for(self._read_response(), 5.0) response = await asyncio.wait_for(self._read_response(), self._timeout)
else: else:
responses = await self._read_response() response = await self._read_response()
for response in responses:
if response["msg_type"] == 'Report' and not self._msg_queue_get_report: if response["msg_type"] == 'HardReset':
if self._report_sig is None: logging.warn("Kirdy is being hard reset.")
if self._report_queue.full(): raise asyncio.exceptions.CancelledError
self._report_queue.get_nowait() if response["msg_type"] == 'Dfu':
self._report_queue.put_nowait(response) logging.warn("Kirdy enters Dfu Mode.")
else: asyncio.create_task(self.end_session())
self._report_sig.emit(response) if response["msg_type"] == 'ConnectionClose':
logging.warn("Kirdy runs out of TCP sockets and closes this connected socket.")
asyncio.create_task(self.end_session())
if response["msg_type"] == 'Report' and not self._msg_queue_get_report:
if self._report_sig is None:
self._report_queue.put_nowait_overwrite(response)
else: else:
if self._msg_queue_get_report: self._report_sig.emit(response)
async with self._lock: else:
self._msg_queue_get_report = False if self._msg_queue_get_report:
if self._int_msg_queue.full(): self._msg_queue_get_report = False
logging.debug("_int_msg_queue is full") self._int_msg_queue.put_nowait_overwrite(response)
self._int_msg_queue.get_nowait()
self._int_msg_queue.put_nowait(response)
except asyncio.exceptions.CancelledError: except asyncio.exceptions.CancelledError:
logging.debug("Read Response Handler is canceling") pass
except TimeoutError: except (TimeoutError, ConnectionResetError, ConnectionError) as exec:
logging.warning("Read active report response timeout") self._task_queue.put_nowait_overwrite(exec)
if self._task_queue.full(): self._int_msg_queue.put_nowait_overwrite(exec)
logging.debug("_int_msg_queue is full") except Exception as exec:
self._task_queue.get_nowait()
self._task_queue.put_nowait(TimeoutError())
except ConnectionResetError:
logging.warning("Connection Reset by peer")
if self._task_queue.full():
logging.debug("_int_msg_queue is full")
self._task_queue.get_nowait()
self._task_queue.put_nowait(ConnectionResetError())
except:
logging.warn("Read Response Handler experienced an error. Exiting.", exc_info=True) logging.warn("Read Response Handler experienced an error. Exiting.", exc_info=True)
self._task_queue.put_nowait_overwrite(exec)
self._int_msg_queue.put_nowait_overwrite(exec)
if self._report_mode_on: if self._report_mode_on:
self._report_mode_on = False self._report_mode_on = False
self._report_queue.put_nowait(TimeoutError) self._report_queue.put_nowait_overwrite(TimeoutError)
async def _stop_handler(self): async def _stop_handler(self):
for task in asyncio.all_tasks(): for task in asyncio.all_tasks():
@ -822,71 +847,52 @@ class Kirdy:
await asyncio.gather(*asyncio.all_tasks(), loop=self._event_loop) await asyncio.gather(*asyncio.all_tasks(), loop=self._event_loop)
async def __coninit(self, timeout): async def __coninit(self, timeout):
self._task_queue = asyncio.Queue(maxsize=64) def _put_nowait_overwrite(self, item):
if self.full():
self.get_nowait()
self.put_nowait(item)
asyncio.Queue.put_nowait_overwrite = _put_nowait_overwrite
if self._task_queue is not None:
while not(self._task_queue.empty()):
task = self._task_queue.get_nowait()
if isinstance(task, types.FunctionType):
task().close()
else:
self._task_queue = asyncio.Queue(maxsize=16)
self._int_msg_queue = asyncio.Queue(maxsize=4) self._int_msg_queue = asyncio.Queue(maxsize=4)
self._msg_queue = queue.Queue(maxsize=64) self._report_queue = asyncio.Queue(maxsize=16)
self._report_queue = queue.Queue(maxsize=16)
self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection(self._host, self._ctrl_port), timeout) self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection(self._host, self._ctrl_port), timeout)
writer_sock = self._writer.get_extra_info("socket") writer_sock = self._writer.get_extra_info("socket")
writer_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) writer_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
if self._connected_sig is not None:
self._connected_sig.emit(True)
async def _read_response(self, buffer_size=16384): async def _read_response(self):
raw_response = b'' raw_response = b''
while len(raw_response) == 0: while len(raw_response) == 0:
# Ignore 0 size packet # Ignore 0 size packet
raw_response = await self._reader.read(buffer_size) raw_response = await self._reader.readuntil()
response = raw_response.decode('utf-8', errors='ignore').split("\n") response = raw_response.decode('utf-8', errors='ignore').split("\n")
response = response[:-1] return json.loads(response[0])
items = []
for item in response:
items.append(json.loads(item))
return items
def _get_msg(self):
msg = self._msg_queue.get()
return msg
def _send_raw_cmd_handler(self, cmd, msg_type="Acknowledge", sig=None):
if self.connected():
self._event_loop.call_soon_threadsafe(self._task_queue.put_nowait, lambda: self._send_raw_cmd(cmd, msg_type, sig=sig))
else:
raise ConnectionError
# If the cmd involves a cmd specific data type, async def _send_raw_cmd(self, cmd, msg_type="Acknowledge", sig=None):
# checking is done separately within the functions being called
async def _send_raw_cmd(self, cmd, msg_type, sig=None):
if self.connected(): if self.connected():
self._writer.write(bytes(json.dumps(cmd), "UTF-8")) async with self._lock:
await self._writer.drain() self._writer.write(bytes(json.dumps(cmd), "UTF-8"))
response = await self._int_msg_queue.get() await self._writer.drain()
if response["msg_type"] == msg_type: msg = await asyncio.wait_for(self._int_msg_queue.get(), self._timeout)
if msg["msg_type"] == msg_type:
if sig is not None: if sig is not None:
sig.emit(response) sig.emit(msg)
return {"msg_type": "Acknowledge"} return {"msg_type": "Acknowledge"}
return response return msg
else: else:
raise InvalidCmd raise InvalidCmd
else: else:
raise ConnectionError raise ConnectionError
def _send_cmd_handler(self, target, cmd, data=None, msg_type="Acknowledge", sig=None, hard_reset=False): async def _send_cmd(self, target, cmd, data=None, msg_type="Acknowledge", sig=None):
if self.connected():
if hard_reset:
while not(self._task_queue.empty()):
pass
self._event_loop.call_soon_threadsafe(self._task_queue.put_nowait, lambda: self._send_cmd(target, cmd, data, msg_type, sig=sig))
if hard_reset:
# Wait 1s for Kirdy to hard reset
time.sleep(1.0)
else:
raise ConnectionError
async def _send_cmd(self, target, cmd, data, msg_type, sig=None):
cmd_dict = {} cmd_dict = {}
cmd_dict[target] = cmd.name cmd_dict[target] = cmd.name
@ -904,17 +910,21 @@ class Kirdy:
pass pass
if msg_type == 'Report': if msg_type == 'Report':
async with self._lock: self._msg_queue_get_report = True
self._msg_queue_get_report = True
self._writer.write(bytes(json.dumps(cmd_dict), "UTF-8")) async with self._lock:
await self._writer.drain() self._writer.write(bytes(json.dumps(cmd_dict), "UTF-8"))
await self._writer.drain()
msg = await asyncio.wait_for(self._int_msg_queue.get(), self._timeout)
if isinstance(msg, Exception):
raise msg
msg = await self._int_msg_queue.get()
if msg['msg_type'] == msg_type: if msg['msg_type'] == msg_type:
if sig is not None: if sig is not None:
sig.emit(msg) sig.emit(msg)
return {"msg_type": "Acknowledge"} return {"msg_type": "Acknowledge"}
return msg else:
return msg
else: else:
raise InvalidCmd raise InvalidCmd

View File

@ -20,7 +20,7 @@ import logging
import asyncio import asyncio
from driver.kirdy import Kirdy as Kirdy_Driver from driver.kirdy import Kirdy as Kirdy_Driver
import qasync import qasync
from qasync import asyncClose from qasync import asyncClose, asyncSlot
from collections import deque from collections import deque
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from time import time from time import time
@ -96,15 +96,16 @@ class Kirdy(QObject):
def end_session(self): def end_session(self):
if self._timer.isActive(): if self._timer.isActive():
self._timer.stop() self._timer.stop()
self._kirdy.end_session() asyncio.get_running_loop().create_task(self._kirdy.end_session())
@pyqtSlot(bool) @pyqtSlot(bool)
def connected_setup(self, connected): def connected_setup(self, connected):
if connected: if connected:
self._kirdy.device.set_active_report_mode(True) self._kirdy.task_dispatcher(self._kirdy.device.set_active_report_mode(True))
self._kirdy._report_mode_on = True
def timerEvent(self, event): def timerEvent(self, event):
self._kirdy.device.get_settings_summary(sig=self.setting_update_sig) self._kirdy.task_dispatcher(self._kirdy.device.get_settings_summary(sig=self.setting_update_sig))
@pyqtSlot(bool) @pyqtSlot(bool)
def start_polling(self, start): def start_polling(self, start):
@ -127,6 +128,9 @@ class Kirdy(QObject):
self.start_polling() self.start_polling()
else: else:
logging.debug("Attempt to update polling timer when it is stopped") logging.debug("Attempt to update polling timer when it is stopped")
def get_hw_rev(self):
return self._kirdy.get_hw_rev()
class Graphs: class Graphs:
def __init__(self, ld_i_set_graph, pd_mon_pwr_graph, tec_i_graph, tec_temp_graph, max_samples=1000): def __init__(self, ld_i_set_graph, pd_mon_pwr_graph, tec_i_graph, tec_temp_graph, max_samples=1000):
@ -524,12 +528,12 @@ class MainWindow(QtWidgets.QMainWindow):
def setup_menu_bar(self): def setup_menu_bar(self):
@pyqtSlot(bool) @pyqtSlot(bool)
def about_kirdy(_): def about_kirdy(_):
# TODO: Replace the hardware revision placeholder hw_rev = self.kirdy_handler.get_hw_rev()
QtWidgets.QMessageBox.about( QtWidgets.QMessageBox.about(
self, self,
"About Kirdy", "About Kirdy",
f""" f"""
<h1>Sinara 1550 Kirdy v"major rev"."minor rev"</h1> <h1>Sinara 1550 Kirdy v{hw_rev["major"]}.{hw_rev["minor"]}</h1>
""" """
) )
self.menu_action_about_kirdy.triggered.connect(about_kirdy) self.menu_action_about_kirdy.triggered.connect(about_kirdy)
@ -548,19 +552,19 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot(bool) @pyqtSlot(bool)
def dfu_mode(_): def dfu_mode(_):
self.kirdy.device.dfu() self.kirdy.task_dispatcher(self.kirdy.device.dfu())
self.kirdy_handler.end_session() self.kirdy_handler.end_session()
self.menu_action_dfu_mode.triggered.connect(dfu_mode) self.menu_action_dfu_mode.triggered.connect(dfu_mode)
@pyqtSlot(bool) @pyqtSlot(bool)
def reset_kirdy(_): def reset_kirdy(_):
self.kirdy.device.hard_reset() self.kirdy.task_dispatcher(self.kirdy.device.hard_reset())
self.kirdy_handler.end_session() self.kirdy_handler.end_session()
self.menu_action_hard_reset.triggered.connect(reset_kirdy) self.menu_action_hard_reset.triggered.connect(reset_kirdy)
@pyqtSlot(bool) @pyqtSlot(bool)
def save_settings(_): def save_settings(_):
self.kirdy.device.save_current_settings_to_flash() self.kirdy.task_dispatcher(self.kirdy.device.save_current_settings_to_flash())
saved = QtWidgets.QMessageBox(self) saved = QtWidgets.QMessageBox(self)
saved.setWindowTitle("Config saved") saved.setWindowTitle("Config saved")
saved.setText(f"Laser diode and thermostat configs have been saved into flash.") saved.setText(f"Laser diode and thermostat configs have been saved into flash.")
@ -570,7 +574,7 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot(bool) @pyqtSlot(bool)
def load_settings(_): def load_settings(_):
self.kirdy.device.restore_settings_from_flash() self.kirdy.task_dispatcher(self.kirdy.device.restore_settings_from_flash())
loaded = QtWidgets.QMessageBox(self) loaded = QtWidgets.QMessageBox(self)
loaded.setWindowTitle("Config loaded") loaded.setWindowTitle("Config loaded")
loaded.setText(f"Laser Diode and Thermostat configs have been loaded from flash.") loaded.setText(f"Laser Diode and Thermostat configs have been loaded from flash.")
@ -596,32 +600,32 @@ class MainWindow(QtWidgets.QMainWindow):
def _set_up_ctrl_btns(self): def _set_up_ctrl_btns(self):
@pyqtSlot(bool) @pyqtSlot(bool)
def ld_pwr_on(_): def ld_pwr_on(_):
self.kirdy.laser.set_power_on(True) self.kirdy.task_dispatcher(self.kirdy.laser.set_power_on(True))
self.ld_pwr_on_btn.clicked.connect(ld_pwr_on) self.ld_pwr_on_btn.clicked.connect(ld_pwr_on)
@pyqtSlot(bool) @pyqtSlot(bool)
def ld_pwr_off(_): def ld_pwr_off(_):
self.kirdy.laser.set_power_on(False) self.kirdy.task_dispatcher(self.kirdy.laser.set_power_on(False))
self.ld_pwr_off_btn.clicked.connect(ld_pwr_off) self.ld_pwr_off_btn.clicked.connect(ld_pwr_off)
@pyqtSlot(bool) @pyqtSlot(bool)
def ld_clear_alarm(_): def ld_clear_alarm(_):
self.kirdy.laser.clear_alarm() self.kirdy.task_dispatcher(self.kirdy.laser.clear_alarm())
self.ld_clear_alarm_btn.clicked.connect(ld_clear_alarm) self.ld_clear_alarm_btn.clicked.connect(ld_clear_alarm)
@pyqtSlot(bool) @pyqtSlot(bool)
def tec_pwr_on(_): def tec_pwr_on(_):
self.kirdy.thermostat.set_power_on(True) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_power_on(True))
self.tec_pwr_on_btn.clicked.connect(tec_pwr_on) self.tec_pwr_on_btn.clicked.connect(tec_pwr_on)
@pyqtSlot(bool) @pyqtSlot(bool)
def tec_pwr_off(_): def tec_pwr_off(_):
self.kirdy.thermostat.set_power_on(False) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_power_on(False))
self.tec_pwr_off_btn.clicked.connect(tec_pwr_off) self.tec_pwr_off_btn.clicked.connect(tec_pwr_off)
@pyqtSlot(bool) @pyqtSlot(bool)
def tec_clear_alarm(_): def tec_clear_alarm(_):
self.kirdy.thermostat.clear_alarm() self.kirdy.task_dispatcher(self.kirdy.thermostat.clear_alarm())
self.tec_clear_alarm_btn.clicked.connect(tec_clear_alarm) self.tec_clear_alarm_btn.clicked.connect(tec_clear_alarm)
def _set_up_plot_menu(self): def _set_up_plot_menu(self):
@ -665,11 +669,11 @@ class MainWindow(QtWidgets.QMainWindow):
tree.setParameters(self.params[3], showTop=False) tree.setParameters(self.params[3], showTop=False)
self.params[3].sigTreeStateChanged.connect(self.send_command) self.params[3].sigTreeStateChanged.connect(self.send_command)
@pyqtSlot() @asyncSlot()
def autotune(param): async def autotune(param):
match self.autotuner.state(): match self.autotuner.state():
case PIDAutotuneState.STATE_OFF: case PIDAutotuneState.STATE_OFF:
settings = self.kirdy.device.get_settings_summary() settings = await self.kirdy.device.get_settings_summary()
self.autotuner.setParam( self.autotuner.setParam(
param.parent().child('Target Temperature').value(), param.parent().child('Target Temperature').value(),
param.parent().child('Test Current').value() / 1000, param.parent().child('Test Current').value() / 1000,
@ -678,7 +682,7 @@ class MainWindow(QtWidgets.QMainWindow):
param.parent().child('Lookback').value()) param.parent().child('Lookback').value())
self.autotuner.setReady() self.autotuner.setReady()
param.setOpts(title="Stop") param.setOpts(title="Stop")
self.kirdy.thermostat.set_constant_current_control_mode() self.kirdy.task_dispatcher(self.kirdy.thermostat.set_constant_current_control_mode())
self.kirdy_handler.report_update_sig.connect(self.autotune_tick) self.kirdy_handler.report_update_sig.connect(self.autotune_tick)
self.loading_spinner.show() self.loading_spinner.show()
self.loading_spinner.start() self.loading_spinner.start()
@ -686,7 +690,7 @@ class MainWindow(QtWidgets.QMainWindow):
case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN: case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN:
self.autotuner.setOff() self.autotuner.setOff()
param.setOpts(title="Run") param.setOpts(title="Run")
self.kirdy.thermostat.set_tec_i_out(0.0) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_tec_i_out(0.0))
self.kirdy_handler.report_update_sig.disconnect(self.autotune_tick) self.kirdy_handler.report_update_sig.disconnect(self.autotune_tick)
self.background_task_lbl.setText("Ready.") self.background_task_lbl.setText("Ready.")
self.loading_spinner.stop() self.loading_spinner.stop()
@ -698,16 +702,16 @@ class MainWindow(QtWidgets.QMainWindow):
match self.autotuner.state(): match self.autotuner.state():
case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN: case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN:
self.autotuner.run(report['thermostat']['temperature'], report['ts']/1000) self.autotuner.run(report['thermostat']['temperature'], report['ts']/1000)
self.kirdy.thermostat.set_tec_i_out(self.autotuner.output()) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_tec_i_out(self.autotuner.output()))
case PIDAutotuneState.STATE_SUCCEEDED: case PIDAutotuneState.STATE_SUCCEEDED:
kp, ki, kd = self.autotuner.get_tec_pid() kp, ki, kd = self.autotuner.get_tec_pid()
self.autotuner.setOff() self.autotuner.setOff()
self.params[3].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run") self.params[3].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run")
self.kirdy.thermostat.set_pid_kp(kp) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_pid_kp(kp))
self.kirdy.thermostat.set_pid_ki(ki) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_pid_ki(ki))
self.kirdy.thermostat.set_pid_kd(kd) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_pid_kd(kd))
self.kirdy.thermostat.set_pid_control_mode() self.kirdy.task_dispatcher(self.kirdy.thermostat.set_pid_control_mode())
self.kirdy.thermostat.set_temperature_setpoint(self.params[3].child('PID Config', 'PID Auto Tune', 'Target Temperature').value()) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_temperature_setpoint(self.params[3].child('PID Config', 'PID Auto Tune', 'Target Temperature').value()))
self.kirdy_handler.report_update_sig.disconnect(self.autotune_tick) self.kirdy_handler.report_update_sig.disconnect(self.autotune_tick)
self.background_task_lbl.setText("Ready.") self.background_task_lbl.setText("Ready.")
self.loading_spinner.stop() self.loading_spinner.stop()
@ -719,7 +723,7 @@ class MainWindow(QtWidgets.QMainWindow):
case PIDAutotuneState.STATE_FAILED: case PIDAutotuneState.STATE_FAILED:
self.autotuner.setOff() self.autotuner.setOff()
self.params[3].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run") self.params[3].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run")
self.kirdy.thermostat.set_tec_i_out(0.0) self.kirdy.task_dispatcher(self.kirdy.thermostat.set_tec_i_out(0.0))
self.kirdy_handler.report_update_sig.disconnect(self.autotune_tick) self.kirdy_handler.report_update_sig.disconnect(self.autotune_tick)
self.background_task_lbl.setText("Ready.") self.background_task_lbl.setText("Ready.")
self.loading_spinner.stop() self.loading_spinner.stop()
@ -768,7 +772,6 @@ class MainWindow(QtWidgets.QMainWindow):
if result: if result:
self.connect_btn.setText("Disconnect") self.connect_btn.setText("Disconnect")
self.connect_btn.clicked.connect(self.kirdy_handler.end_session) self.connect_btn.clicked.connect(self.kirdy_handler.end_session)
# TODO: self.hw_rev_data = self.kirdy.hw_rev()
self._status() self._status()
else: else:
if self.kirdy_handler.connecting(): if self.kirdy_handler.connecting():
@ -782,10 +785,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.connect_btn.clicked.connect(self.kirdy_handler.end_session) self.connect_btn.clicked.connect(self.kirdy_handler.end_session)
def _status(self): def _status(self):
# TODO: Get rev no from Kirdy and then add revision into the text
host = self.ip_addr host = self.ip_addr
port = self.port port = self.port
self.status_lbl.setText(f"Connected to {host}:{port}") hw_rev = self.kirdy_handler.get_hw_rev()
self.status_lbl.setText(f"Connected to Kirdy v{hw_rev['major']}.{hw_rev['minor']} @ {host}:{port}")
def clear_graphs(self): def clear_graphs(self):
self.graphs.clear_data_pts() self.graphs.clear_data_pts()
@ -899,7 +902,7 @@ class MainWindow(QtWidgets.QMainWindow):
port = net_settings["port"] port = net_settings["port"]
prefix_len = net_settings["prefix_len"] prefix_len = net_settings["prefix_len"]
gateway = net_settings["gateway_addr"] gateway = net_settings["gateway_addr"]
self.kirdy.device.set_ip_settings(addr, port, prefix_len, gateway) self.kirdy.task_dispatcher(self.kirdy.device.set_ip_settings(addr, port, prefix_len, gateway))
self.status_lbl.setText("IP Settings is Updated") self.status_lbl.setText("IP Settings is Updated")
@pyqtSlot() @pyqtSlot()
@ -931,7 +934,7 @@ class MainWindow(QtWidgets.QMainWindow):
target, action = inner_param.opts['target_action_pair'][inner_param.opts['limits'].index(data)] target, action = inner_param.opts['target_action_pair'][inner_param.opts['limits'].index(data)]
cmd = getattr(getattr(self.kirdy, target), action) cmd = getattr(getattr(self.kirdy, target), action)
param.child(*param.childPath(inner_param)).setOpts(lock=True) param.child(*param.childPath(inner_param)).setOpts(lock=True)
cmd() self.kirdy.task_dispatcher(cmd())
param.child(*param.childPath(inner_param)).setOpts(lock=False) param.child(*param.childPath(inner_param)).setOpts(lock=False)
continue continue
""" cmd translation from non-mutex type parameter""" """ cmd translation from non-mutex type parameter"""
@ -942,25 +945,27 @@ class MainWindow(QtWidgets.QMainWindow):
data = siEval(str(data)+inner_param.opts["unit"], regex=FLOAT_REGEX, suffix=suffix) data = siEval(str(data)+inner_param.opts["unit"], regex=FLOAT_REGEX, suffix=suffix)
cmd = getattr(getattr(self.kirdy, inner_param.opts["target"]), inner_param.opts["action"]) cmd = getattr(getattr(self.kirdy, inner_param.opts["target"]), inner_param.opts["action"])
param.child(*param.childPath(inner_param)).setOpts(lock=True) param.child(*param.childPath(inner_param)).setOpts(lock=True)
cmd(data) self.kirdy.task_dispatcher(cmd(data))
param.child(*param.childPath(inner_param)).setOpts(lock=False) param.child(*param.childPath(inner_param)).setOpts(lock=False)
continue continue
def coro_main(): async def coro_main():
args = get_argparser().parse_args() args = get_argparser().parse_args()
if args.logLevel: if args.logLevel:
logging.basicConfig(level=getattr(logging, args.logLevel)) logging.basicConfig(level=getattr(logging, args.logLevel))
app = QtWidgets.QApplication(sys.argv) app_quit_event = asyncio.Event()
app = QtWidgets.QApplication.instance()
app.aboutToQuit.connect(app_quit_event.set)
main_window = MainWindow(args) main_window = MainWindow(args)
main_window.show() main_window.show()
app.aboutToQuit.connect(main_window.kirdy_handler.end_session) await app_quit_event.wait()
app.exec()
def main(): def main():
coro_main() qasync.run(coro_main())
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -8,6 +8,7 @@ import time
import signal import signal
from driver.kirdy import Kirdy, FilterConfig from driver.kirdy import Kirdy, FilterConfig
import asyncio import asyncio
from sipyco.asyncio_tools import SignalHandler
# Based on hirshmann pid-autotune libiary # Based on hirshmann pid-autotune libiary
# See https://github.com/hirschmann/pid-autotune # See https://github.com/hirschmann/pid-autotune
@ -76,6 +77,10 @@ class PIDAutotune:
def setOff(self): def setOff(self):
self._state = PIDAutotuneState.STATE_OFF self._state = PIDAutotuneState.STATE_OFF
def setFailed(self):
self._state = PIDAutotuneState.STATE_FAILED
self._peak_count = 30
def state(self): def state(self):
"""Get the current state.""" """Get the current state."""
return self._state return self._state
@ -246,7 +251,7 @@ class PIDAutotune:
return False return False
def main(): async def main():
""" """
PID AutoTune Tools for Kirdy PID AutoTune Tools for Kirdy
The obtained temperature works best at the target temperature specified. The obtained temperature works best at the target temperature specified.
@ -271,46 +276,46 @@ def main():
noiseband = 2.0 noiseband = 2.0
kirdy = Kirdy() kirdy = Kirdy()
kirdy.start_session(host='192.168.1.128', port=1337) kirdy.start_session(host='192.168.1.126', port=1337)
await kirdy.wait_until_connected()
while not(kirdy.connected()): while not(kirdy.connected()):
pass pass
kirdy.laser.set_power_on(False) await kirdy.laser.set_power_on(False)
kirdy.laser.set_i(0) await kirdy.laser.set_i(0)
kirdy.thermostat.set_power_on(False) await kirdy.thermostat.set_power_on(False)
kirdy.thermostat.set_constant_current_control_mode() await kirdy.thermostat.set_constant_current_control_mode()
kirdy.thermostat.set_tec_i_out(0) await kirdy.thermostat.set_tec_i_out(0)
kirdy.thermostat.clear_alarm() await kirdy.thermostat.clear_alarm()
class SignalHandler:
KEEP_PROCESSING = True
def __init__(self):
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
self.KEEP_PROCESSING = False
signal_handler = SignalHandler() signal_handler = SignalHandler()
signal_handler.setup()
kirdy.device.set_active_report_mode(False) async def sig_handling():
await signal_handler.wait_terminate()
tuner.setFailed()
asyncio.create_task(sig_handling())
await kirdy.device.set_active_report_mode(False)
# Configure the Thermistor Parameters # Configure the Thermistor Parameters
kirdy.thermostat.set_sh_beta(3950) await kirdy.thermostat.set_sh_beta(3950)
kirdy.thermostat.set_sh_r0(10.0 * 1000) await kirdy.thermostat.set_sh_r0(10.0 * 1000)
kirdy.thermostat.set_sh_t0(25) await kirdy.thermostat.set_sh_t0(25)
# Set a large enough temperature range so that it won't trigger overtemperature protection # Set a large enough temperature range so that it won't trigger overtemperature protection
kirdy.thermostat.set_temp_mon_upper_limit(target_temperature + 20) await kirdy.thermostat.set_temp_mon_upper_limit(target_temperature + 20)
kirdy.thermostat.set_temp_mon_lower_limit(target_temperature - 20) await kirdy.thermostat.set_temp_mon_lower_limit(target_temperature - 20)
kirdy.thermostat.set_tec_max_cooling_i(output_step) await kirdy.thermostat.set_tec_max_cooling_i(output_step)
kirdy.thermostat.set_tec_max_heating_i(output_step) await kirdy.thermostat.set_tec_max_heating_i(output_step)
# The Polling Rate of Temperature Adc is equal to the PID Update Interval # The Polling Rate of Temperature Adc is equal to the PID Update Interval
kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps) await kirdy.thermostat.config_temp_adc_filter(FilterConfig.Sinc5Sinc1With50hz60HzRejection.f16sps)
settings = kirdy.device.get_settings_summary() settings = await kirdy.device.get_settings_summary()
sampling_rate = settings["thermostat"]["temp_adc_settings"]["rate"] sampling_rate = settings["thermostat"]["temp_adc_settings"]["rate"]
print("Settings: {0}".format(settings)) print("Settings: {0}".format(settings))
@ -318,10 +323,10 @@ def main():
tuner = PIDAutotune(target_temperature, output_step, tuner = PIDAutotune(target_temperature, output_step,
lookback, noiseband, 1/sampling_rate) lookback, noiseband, 1/sampling_rate)
kirdy.thermostat.set_power_on(True) await kirdy.thermostat.set_power_on(True)
while True and signal_handler.KEEP_PROCESSING: while True:
status_report = kirdy.device.get_status_report() status_report = await kirdy.device.get_status_report()
temperature = status_report["thermostat"]["temperature"] temperature = status_report["thermostat"]["temperature"]
ts = status_report['ts'] ts = status_report['ts']
@ -332,19 +337,20 @@ def main():
break break
tuner_out = tuner.output() tuner_out = tuner.output()
kirdy.thermostat.set_tec_i_out(float(tuner_out)) await kirdy.thermostat.set_tec_i_out(float(tuner_out))
kirdy.thermostat.set_tec_i_out(0) await kirdy.thermostat.set_tec_i_out(0)
kirdy.thermostat.set_power_on(False) await kirdy.thermostat.set_power_on(False)
pid_params = tuner.get_pid_parameters(tuning_rule="tyreus-luyben") if tuner.state() == PIDAutotuneState.STATE_SUCCEEDED:
kirdy.thermostat.set_pid_kp(pid_params.Kp) pid_params = tuner.get_pid_parameters(tuning_rule="tyreus-luyben")
kirdy.thermostat.set_pid_ki(pid_params.Ki) await kirdy.thermostat.set_pid_kp(pid_params.Kp)
kirdy.thermostat.set_pid_kd(pid_params.Kd) await kirdy.thermostat.set_pid_ki(pid_params.Ki)
kirdy.thermostat.set_pid_output_max(1.0) await kirdy.thermostat.set_pid_kd(pid_params.Kd)
kirdy.thermostat.set_pid_output_min(1.0) await kirdy.thermostat.set_pid_output_max(1.0)
await kirdy.thermostat.set_pid_output_min(1.0)
kirdy.end_session(block=True) await kirdy.end_session()
if __name__ == "__main__": if __name__ == "__main__":
main() asyncio.run(main())

View File

@ -9,7 +9,8 @@ use uom::si::{electric_current::{ampere, milliampere},
f32::ElectricCurrent}; f32::ElectricCurrent};
use super::{gpio, sys_timer, usb}; use super::{gpio, sys_timer, usb};
use crate::{device::flash_store::{self, FlashStore}, use crate::{device::{flash_store::{self, FlashStore},
hw_rev::HWRev},
laser_diode::{laser_diode::LdDrive, ld_ctrl::*}, laser_diode::{laser_diode::LdDrive, ld_ctrl::*},
net::net::{IpSettings, ServerHandle}, net::net::{IpSettings, ServerHandle},
thermostat::{max1968::MAX1968, thermostat::Thermostat}, thermostat::{max1968::MAX1968, thermostat::Thermostat},
@ -23,7 +24,7 @@ const WATCHDOG_PERIOD: u32 = 30000;
pub fn bootup( pub fn bootup(
mut core_perif: CorePeripherals, mut core_perif: CorePeripherals,
perif: Peripherals, perif: Peripherals,
) -> (IndependentWatchdog, FlashStore, LdDrive, Thermostat) { ) -> (IndependentWatchdog, FlashStore, HWRev, LdDrive, Thermostat) {
core_perif.SCB.enable_icache(); core_perif.SCB.enable_icache();
core_perif.SCB.enable_dcache(&mut core_perif.CPUID); core_perif.SCB.enable_dcache(&mut core_perif.CPUID);
@ -119,5 +120,5 @@ pub fn bootup(
info!("Kirdy setup complete"); info!("Kirdy setup complete");
(wd, flash_store, laser, thermostat) (wd, flash_store, hw_rev, laser, thermostat)
} }

View File

@ -1,4 +1,6 @@
use crc::{Crc, CRC_24_BLE}; use crc::{Crc, CRC_24_BLE};
use miniconf::Tree;
use serde::{Deserialize, Serialize};
use stm32f4xx_hal::{gpio::{Input, PE10, PE11, PE8, PE9}, use stm32f4xx_hal::{gpio::{Input, PE10, PE11, PE8, PE9},
signature}; signature};
@ -11,6 +13,7 @@ pub struct HwRevPins {
pub h3: PE11<Input>, pub h3: PE11<Input>,
} }
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct HWRev { pub struct HWRev {
pub major: u8, pub major: u8,
pub minor: u8, pub minor: u8,

View File

@ -1,7 +1,7 @@
#![cfg_attr(not(test), no_main)] #![cfg_attr(not(test), no_main)]
#![cfg_attr(not(test), no_std)] #![cfg_attr(not(test), no_std)]
use core::marker::PhantomData; use core::{marker::PhantomData, u32};
use cortex_m_rt::entry; use cortex_m_rt::entry;
use log::{debug, info}; use log::{debug, info};
@ -52,6 +52,7 @@ pub enum State {
SaveLdThermostatSettings, SaveLdThermostatSettings,
SaveDeviceSettings, SaveDeviceSettings,
PrepareForHardReset, PrepareForHardReset,
PrepareForDfu,
HardReset, HardReset,
} }
@ -64,7 +65,7 @@ fn main() -> ! {
let core_perif = CorePeripherals::take().unwrap(); let core_perif = CorePeripherals::take().unwrap();
let perif = Peripherals::take().unwrap(); let perif = Peripherals::take().unwrap();
let (mut wd, mut flash_store, mut laser, mut thermostat) = bootup(core_perif, perif); let (mut wd, mut flash_store, mut hw_rev, mut laser, mut thermostat) = bootup(core_perif, perif);
let mut device_settings = DeviceSettings { let mut device_settings = DeviceSettings {
ip_settings: IpSettings::default(), ip_settings: IpSettings::default(),
@ -82,6 +83,8 @@ fn main() -> ! {
let eth_data_buffer = unsafe { addr_of_mut!(ETH_DATA_BUFFER).as_mut().unwrap() }; let eth_data_buffer = unsafe { addr_of_mut!(ETH_DATA_BUFFER).as_mut().unwrap() };
let mut sock_ts: [u32; net::net::NUM_OF_SOCKETS] = [0; net::net::NUM_OF_SOCKETS];
loop { loop {
wd.feed(); wd.feed();
@ -183,6 +186,10 @@ fn main() -> ! {
thermostat.start_tec_readings_conversion(); thermostat.start_tec_readings_conversion();
} }
let mut num_of_connected_sock: u8 = 0;
let mut oldest_connected_sock_ts: u32 = u32::MAX;
let mut oldest_connected_sock_id: usize = 0;
net::net::for_each(|mut socket, id| { net::net::for_each(|mut socket, id| {
if net::net::eth_is_socket_active(socket) && net::net::eth_is_socket_connected(socket) { if net::net::eth_is_socket_active(socket) && net::net::eth_is_socket_connected(socket) {
if net::net::eth_can_sock_recv(socket) && net::net::eth_can_sock_send(socket) { if net::net::eth_can_sock_recv(socket) && net::net::eth_can_sock_send(socket) {
@ -195,6 +202,7 @@ fn main() -> ! {
eth_data_buffer, eth_data_buffer,
bytes, bytes,
&mut socket, &mut socket,
&mut hw_rev,
&mut laser, &mut laser,
&mut thermostat, &mut thermostat,
&mut state, &mut state,
@ -203,8 +211,35 @@ fn main() -> ! {
); );
} }
} }
num_of_connected_sock += 1;
if sock_ts[id] == 0 {
sock_ts[id] = sys_timer::now();
}
if oldest_connected_sock_ts > sock_ts[id] {
oldest_connected_sock_ts = sock_ts[id];
oldest_connected_sock_id = id;
}
} else {
sock_ts[id] = 0;
} }
}) });
if num_of_connected_sock == net::net::NUM_OF_SOCKETS as u8 {
let mut sock_handle = net::net::eth_get_sock_handle(oldest_connected_sock_id);
net::cmd_handler::send_response(
eth_data_buffer,
net::cmd_handler::ResponseEnum::ConnectionClose,
None,
&mut sock_handle,
);
net::net::eth_poll_iface();
debug!("Waiting for ConnectionClose msg to be sent");
while !net::net::eth_is_data_sent() { }
net::net::eth_close_socket_by_id(oldest_connected_sock_id);
debug!("Closing socket id: {:?}", oldest_connected_sock_id);
}
} }
State::SaveLdThermostatSettings => { State::SaveLdThermostatSettings => {
// State Transition // State Transition
@ -261,9 +296,30 @@ fn main() -> ! {
None, None,
&mut socket, &mut socket,
); );
net::net::eth_poll_iface();
} }
}); });
} }
State::PrepareForDfu => {
// State Transition
state = State::HardReset;
wd.feed();
laser.power_down();
thermostat.power_down();
net::net::for_each(|mut socket, _| {
if net::net::eth_is_socket_active(socket) {
net::cmd_handler::send_response(
eth_data_buffer,
net::cmd_handler::ResponseEnum::Dfu,
None,
&mut socket,
);
net::net::eth_poll_iface();
}
});
}
State::HardReset => { State::HardReset => {
wd.feed(); wd.feed();
laser.power_down(); laser.power_down();

View File

@ -10,7 +10,7 @@ use uom::si::{electric_current::{ampere, ElectricCurrent},
electrical_resistance::{ohm, ElectricalResistance}, electrical_resistance::{ohm, ElectricalResistance},
power::{watt, Power}}; power::{watt, Power}};
use crate::{device::{dfu, sys_timer}, use crate::{device::{dfu, hw_rev::HWRev, sys_timer},
laser_diode::{laser_diode::{LdDrive, LdSettingsSummary, StatusReport as LdStatusReport}, laser_diode::{laser_diode::{LdDrive, LdSettingsSummary, StatusReport as LdStatusReport},
pd_mon_params::{self, ResponsitivityUnit}}, pd_mon_params::{self, ResponsitivityUnit}},
net::net, net::net,
@ -26,10 +26,13 @@ pub enum ResponseEnum {
Reserved, Reserved,
Settings, Settings,
Report, Report,
HwRev,
Acknowledge, Acknowledge,
InvalidDatatype, InvalidDatatype,
InvalidCmd, InvalidCmd,
HardReset, HardReset,
Dfu,
ConnectionClose,
} }
pub type MsgType = Option<&'static str>; pub type MsgType = Option<&'static str>;
@ -59,6 +62,7 @@ pub struct ResponseObj<'a> {
enum DeviceCmd { enum DeviceCmd {
#[default] #[default]
Reserved, Reserved,
GetHwRev,
SetIPSettings, SetIPSettings,
SetActiveReportMode, SetActiveReportMode,
SetPdFinGain, SetPdFinGain,
@ -173,6 +177,17 @@ pub struct SettingsSummaryObj {
json: SettingsSummary, json: SettingsSummary,
} }
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct HwRevType {
msg_type: ResponseEnum,
hw_rev: HWRev,
}
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Tree)]
pub struct HwRevObj {
json: HwRevType,
}
pub fn send_response(buffer: &mut [u8], msg_type: ResponseEnum, msg: MsgType, socket: &mut SocketHandle) { pub fn send_response(buffer: &mut [u8], msg_type: ResponseEnum, msg: MsgType, socket: &mut SocketHandle) {
let response = ResponseObj { let response = ResponseObj {
json: Response { json: Response {
@ -227,6 +242,18 @@ pub fn send_status_report(
net::eth_send(buffer, num_bytes, *socket); net::eth_send(buffer, num_bytes, *socket);
} }
pub fn send_hw_rev(buffer: &mut [u8], hw_rev_o: &mut HWRev, socket: &mut SocketHandle) {
let hw_rev = HwRevObj {
json: HwRevType {
msg_type: ResponseEnum::HwRev,
hw_rev: *hw_rev_o,
},
};
let mut num_bytes = hw_rev.get_json("/json", buffer).unwrap();
buffer[num_bytes] = b'\n';
num_bytes += 1;
net::eth_send(buffer, num_bytes, *socket);
}
// Use a minimal struct for high speed cmd ctrl to reduce processing overhead // Use a minimal struct for high speed cmd ctrl to reduce processing overhead
#[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)] #[derive(Deserialize, Serialize, Copy, Clone, Debug, Default, Tree)]
pub struct TecSetICmdJson { pub struct TecSetICmdJson {
@ -245,6 +272,7 @@ pub fn execute_cmd(
buffer: &mut [u8], buffer: &mut [u8],
buffer_size: usize, buffer_size: usize,
socket: &mut SocketHandle, socket: &mut SocketHandle,
hw_rev: &mut HWRev,
laser: &mut LdDrive, laser: &mut LdDrive,
thermostat: &mut Thermostat, thermostat: &mut Thermostat,
state: &mut State, state: &mut State,
@ -296,7 +324,8 @@ pub fn execute_cmd(
unsafe { unsafe {
dfu::set_dfu_trigger(); dfu::set_dfu_trigger();
} }
*state = State::HardReset; net::eth_poll_iface();
*state = State::PrepareForDfu;
} }
Some(DeviceCmd::SetActiveReportMode) => match cmd.json.data_bool { Some(DeviceCmd::SetActiveReportMode) => match cmd.json.data_bool {
Some(val) => { Some(val) => {
@ -348,6 +377,9 @@ pub fn execute_cmd(
); );
} }
}, },
Some(DeviceCmd::GetHwRev) => {
send_hw_rev(buffer, hw_rev, socket);
}
Some(DeviceCmd::GetStatusReport) => { Some(DeviceCmd::GetStatusReport) => {
send_status_report(buffer, laser, thermostat, socket); send_status_report(buffer, laser, thermostat, socket);
} }
@ -364,6 +396,7 @@ pub fn execute_cmd(
} }
Some(DeviceCmd::HardReset) => { Some(DeviceCmd::HardReset) => {
send_response(buffer, ResponseEnum::Acknowledge, None, socket); send_response(buffer, ResponseEnum::Acknowledge, None, socket);
net::eth_poll_iface();
*state = State::PrepareForHardReset; *state = State::PrepareForHardReset;
} }
None => { /* Do Nothing */ } None => { /* Do Nothing */ }

View File

@ -47,6 +47,7 @@ pub struct ServerHandle {
dma: EthernetDMA<'static, 'static>, dma: EthernetDMA<'static, 'static>,
phy: EthernetPhy<EthernetMACWithMii<Pin<'A', 2, Alternate<11>>, Pin<'C', 1, Alternate<11>>>>, phy: EthernetPhy<EthernetMACWithMii<Pin<'A', 2, Alternate<11>>, Pin<'C', 1, Alternate<11>>>>,
link_was_up: bool, link_was_up: bool,
data_sent: bool,
} }
pub type EthernetPins = EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>; pub type EthernetPins = EthPins<PA1<Input>, PA7<Input>, PB11<Input>, PB12<Input>, PB13<Input>, PC4<Input>, PC5<Input>>;
pub struct EthernetMgmtPins { pub struct EthernetMgmtPins {
@ -55,7 +56,7 @@ pub struct EthernetMgmtPins {
} }
pub type EthInterface = Interface; pub type EthInterface = Interface;
pub const NUM_OF_SOCKETS: usize = 4; pub const NUM_OF_SOCKETS: usize = 4 + 1;
const TCP_BUFFER_SIZE: usize = 4096; const TCP_BUFFER_SIZE: usize = 4096;
static mut RX_RING: Option<[RxRingEntry; 8]> = None; static mut RX_RING: Option<[RxRingEntry; 8]> = None;
static mut TX_RING: Option<[TxRingEntry; 8]> = None; static mut TX_RING: Option<[TxRingEntry; 8]> = None;
@ -138,7 +139,7 @@ impl ServerHandle {
let tcp_handles = { let tcp_handles = {
// Do not use NUM_OF_SOCKETS to define array size to // Do not use NUM_OF_SOCKETS to define array size to
// remind developers to create/remove tcp_handles accordingly after changing NUM_OF_SOCKETS // remind developers to create/remove tcp_handles accordingly after changing NUM_OF_SOCKETS
let mut tcp_handles: [MaybeUninit<SocketHandle>; 4] = unsafe { MaybeUninit::uninit().assume_init() }; let mut tcp_handles: [MaybeUninit<SocketHandle>; 5] = unsafe { MaybeUninit::uninit().assume_init() };
macro_rules! create_tcp_handle { macro_rules! create_tcp_handle {
($rx_storage:ident, $tx_storage:ident, $handle:expr) => { ($rx_storage:ident, $tx_storage:ident, $handle:expr) => {
@ -155,8 +156,9 @@ impl ServerHandle {
create_tcp_handle!(RX_STORAGE1, TX_STORAGE1, tcp_handles[1]); create_tcp_handle!(RX_STORAGE1, TX_STORAGE1, tcp_handles[1]);
create_tcp_handle!(RX_STORAGE2, TX_STORAGE2, tcp_handles[2]); create_tcp_handle!(RX_STORAGE2, TX_STORAGE2, tcp_handles[2]);
create_tcp_handle!(RX_STORAGE3, TX_STORAGE3, tcp_handles[3]); create_tcp_handle!(RX_STORAGE3, TX_STORAGE3, tcp_handles[3]);
create_tcp_handle!(RX_STORAGE4, TX_STORAGE4, tcp_handles[4]);
unsafe { mem::transmute::<_, [SocketHandle; 4]>(tcp_handles) } unsafe { mem::transmute::<_, [SocketHandle; 5]>(tcp_handles) }
}; };
for i in 0..NUM_OF_SOCKETS { for i in 0..NUM_OF_SOCKETS {
@ -189,6 +191,7 @@ impl ServerHandle {
dma: dma, dma: dma,
phy: phy, phy: phy,
link_was_up: false, link_was_up: false,
data_sent: true,
}; };
unsafe { unsafe {
@ -246,14 +249,17 @@ impl ServerHandle {
pub fn send(&mut self, buffer: &mut [u8], num_bytes: usize, socket_handles: SocketHandle) { pub fn send(&mut self, buffer: &mut [u8], num_bytes: usize, socket_handles: SocketHandle) {
let socket = self.socket_set.get_mut::<Socket>(socket_handles); let socket = self.socket_set.get_mut::<Socket>(socket_handles);
if num_bytes > 0 { if num_bytes > 0 {
match socket.send_slice(&buffer[..num_bytes]) { cortex_m::interrupt::free(|_| {
Ok(_) => { match socket.send_slice(&buffer[..num_bytes]) {
info!("Enqueued {} bytes.", num_bytes); Ok(_) => {
} self.data_sent = false;
Err(err) => { info!("Enqueued {} bytes.", num_bytes);
info!("Bytes cannot be sent. Error: {:?}", err) }
} Err(err) => {
}; info!("Bytes cannot be sent. Error: {:?}", err)
}
};
});
} }
} }
@ -433,7 +439,7 @@ pub fn eth_update_iface_poll_timer() {
} }
} }
fn eth_poll_iface() { pub fn eth_poll_iface() {
unsafe { unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE { if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.poll_iface(); server_handle.poll_iface();
@ -499,6 +505,46 @@ pub fn eth_close_socket(socket_handles: SocketHandle) {
} }
} }
pub fn eth_get_sock_handle(id: usize) -> SocketHandle {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.socket_handles[id]
} else {
panic!("eth_get_sock_handle is called before init");
}
}
}
pub fn eth_close_socket_by_id(id: usize) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.close_socket(server_handle.socket_handles[id])
} else {
panic!("eth_close_socket_by_id is called before init");
}
}
}
pub fn eth_is_data_sent() -> bool {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.data_sent
} else {
panic!("eth_is_data_sent is called before init");
}
}
}
fn eth_set_data_sent(val: bool) {
unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE {
server_handle.data_sent = val;
} else {
panic!("eth_is_data_sent is called before init");
}
}
}
pub fn for_each<F: FnMut(SocketHandle, usize)>(mut callback: F) { pub fn for_each<F: FnMut(SocketHandle, usize)>(mut callback: F) {
unsafe { unsafe {
if let Some(ref mut server_handle) = SERVER_HANDLE { if let Some(ref mut server_handle) = SERVER_HANDLE {
@ -519,6 +565,9 @@ fn ETH() {
if interrupt_reason.rx { if interrupt_reason.rx {
eth_poll_iface(); eth_poll_iface();
} }
if interrupt_reason.tx {
eth_set_data_sent(true);
}
debug!("Ethernet Interrupt{:?}", interrupt_reason); debug!("Ethernet Interrupt{:?}", interrupt_reason);
} }