kirdy/pykirdy/driver/kirdy.py

982 lines
39 KiB
Python

import types
import socket
import json
import logging
import time
from threading import Thread
from aenum import StrEnum, NoAlias
import queue
import asyncio
class _dt(StrEnum):
ip_settings = "ip_settings"
temp_adc_filter = "temp_adc_filter"
f32 = "data_f32"
bool = "data_bool"
none = "None"
class State(StrEnum):
disconnected = "disconnected"
connected = "connected"
class CmdList:
class device(StrEnum, settings=NoAlias):
_target = "device_cmd"
SetIPSettings = _dt.ip_settings
SetPdFinGain = _dt.f32
SetPdTransconductance = _dt.f32
SetActiveReportMode = _dt.bool
GetHwRev = _dt.none
GetStatusReport = _dt.none
GetSettingsSummary = _dt.none
Dfu = _dt.none
SaveFlashSettings = _dt.none
LoadFlashSettings = _dt.none
HardReset = _dt.none
class ld(StrEnum, settings=NoAlias):
_target = "laser_diode_cmd"
SetDefaultPowerOn = _dt.bool
PowerUp = _dt.none
PowerDown = _dt.none
LdTermsShort = _dt.none
LdTermsOpen = _dt.none
SetI = _dt.f32
SetPdResponsitivity = _dt.f32
SetPdDarkCurrent = _dt.f32
ApplyPdParams = _dt.none
SetLdPwrLimit = _dt.f32
ClearAlarm = _dt.none
class thermostat(StrEnum, settings=NoAlias):
_target = "thermostat_cmd"
SetDefaultPowerOn = _dt.bool,
PowerUp = _dt.f32,
PowerDown = _dt.f32,
SetTecMaxV = _dt.f32,
SetTecMaxIPos = _dt.f32,
SetTecMaxINeg = _dt.f32,
SetTecIOut = _dt.f32,
SetTemperatureSetpoint = _dt.f32,
SetPidEngage = _dt.none,
SetPidDisEngage = _dt.none,
SetPidKp = _dt.f32,
SetPidKi = _dt.f32,
SetPidKd = _dt.f32,
SetPidOutMin = _dt.f32,
SetPidOutMax = _dt.f32,
ConfigTempAdcFilter = _dt.temp_adc_filter,
GetPollInterval = _dt.none,
SetTempMonUpperLimit = _dt.f32,
SetTempMonLowerLimit = _dt.f32,
ClearAlarm = _dt.none,
SetShT0 = _dt.f32,
SetShR0 = _dt.f32,
SetShBeta = _dt.f32,
class FilterConfig:
class Sinc5Sinc1With50hz60HzRejection(StrEnum):
f27sps = "F27SPS"
f21sps = "F21SPS"
f20sps = "F20SPS"
f16sps = "F16SPS"
_odr_type = "sinc5sinc1postfilter"
def _filter_type(self):
return "Sinc5Sinc1With50hz60HzRejection"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc5Sinc1(StrEnum):
f31250_0sps = "F31250_0SPS"
f15625_0sps = "F15625_0SPS"
f10417_0sps = "F10417_0SPS"
f5208_0sps = "F5208_0SPS"
f2597_0sps = "F2597_0SPS"
f1007_0sps = "F1007_0SPS"
f503_8sps = "F503_8SPS"
f381_0sps = "F381_0SPS"
f200_3sps = "F200_3SPS"
f100_2sps = "F100_2SPS"
f59_52sps = "F59_52SPS"
f49_68sps = "F49_68SPS"
f20_01sps = "F20_01SPS"
f16_63sps = "F16_63SPS"
f10_0sps = "F10_0SPS"
f5_0sps = "F5_0SPS"
f2_5sps = "F2_5SPS"
f1_25sps = "F1_25SPS"
_odr_type = "sinc5sinc1odr"
def _filter_type(self):
return "Sinc5Sinc1"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc3(StrEnum):
f31250_0sps = "F31250_0SPS"
f15625_0sps = "F15625_0SPS"
f10417_0sps = "F10417_0SPS"
f5208_0sps = "F5208_0SPS"
f2597_0sps = "F2597_0SPS"
f1007_0sps = "F1007_0SPS"
f503_8sps = "F503_8SPS"
f381_0sps = "F381_0SPS"
f200_3sps = "F200_3SPS"
f100_2sps = "F100_2SPS"
f59_52sps = "F59_52SPS"
f49_68sps = "F49_68SPS"
f20_01sps = "F20_01SPS"
f16_63sps = "F16_63SPS"
f10_0sps = "F10_0SPS"
f5_0sps = "F5_0SPS"
f2_5sps = "F2_5SPS"
f1_25sps = "F1_25SPS"
_odr_type = "sinc3odr"
def _filter_type(self):
return "Sinc3"
@classmethod
def get_list_of_settings(cls):
ret = []
for e in cls:
if e not in [cls._odr_type]:
ret.append(e)
return ret
class Sinc3WithFineODR():
upper_limit = 31250
lower_limit = 1.907465
_odr_type = "sinc3fineodr"
def __init__(self, rate):
assert rate >= self.lower_limit and rate <= self.upper_limit
self.rate = float(rate)
def _filter_type(self):
return "Sinc3WithFineODR"
class InvalidDataType(Exception):
pass
class InvalidCmd(Exception):
pass
class Device:
def __init__(self, send_cmd_handler, send_raw_cmd_handler):
self._cmd = CmdList.device
self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler
async def set_ip_settings(self, addr="192.168.1.128", port=1337, prefix_len=24, gateway="192.168.1.1"):
"""
Upon command execution, the ip settings are saved into flash and are effective upon next reboot.
"""
try:
socket.inet_aton(addr)
socket.inet_aton(gateway)
except OSError:
raise InvalidDataType
addr = addr.split(".")
gateway = gateway.split(".")
if not(isinstance(port, int) and isinstance(prefix_len, int)):
raise InvalidDataType
return await self._send_raw_cmd(
{
"device_cmd": "SetIPSettings",
"ip_settings": {
"addr": [int(x) for x in addr],
"port": port,
"prefix_len": prefix_len,
"gateway": [int(x) for x in gateway],
}
}
)
async def set_active_report_mode(self, on):
"""
Set active report to be on. If it is on, Kirdy will send status report
to the client socket according to the temperature polling rate set.
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetActiveReportMode, on)
async def set_pd_mon_fin_gain(self, gain):
"""
Configure the photodiode monitor final analog front-end stage gain.
- gain: unitless
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdFinGain, gain)
async def set_pd_mon_transconductance(self, transconductance):
"""
Configure the photodiode monitor transconductance value.
- transconductance: 1/Ohm
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdTransconductance, transconductance)
async def get_hw_rev(self):
"""
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.
{
'ts': 227657, # Relative Timestamp (ms)
'msg_type': 'Report' # Indicate it is a 'Report' json object
'laser': {
'pwr_on': False, # Laser Power is On (True/False)
'pwr_excursion': False, # Was Laser experienced an Overpowered Condition? (True/False)
'ld_i_set': 0.0, # Laser Diode Output 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.
'term_50ohm': 'On' # Is the Low Frequency Modulation Input's Impedance 50 Ohm? ("On"/"Off")
},
'thermostat': {
'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)
'temp_mon_status': { # Temperature Monitor:
'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)
},
'temperature': 25.03344, # Temperature Readings (Degree Celsius)
'i_set': 0.0, # Tec Current Set by User/PID Controller(A)
'tec_i': 0.0024998188, # Tec Current Readings (A)
'tec_v': -0.00399971 # Tec Voltage Readings (V)
}
}
"""
return await self._send_cmd(self._cmd._target, self._cmd.GetStatusReport, msg_type="Report", sig=sig)
async def get_settings_summary(self, sig=None):
"""
Get the current settings of laser and thermostat in a json object.
{
'msg_type': 'Settings', # Indicate it is a 'Settings' json object
'laser': {
'default_pwr_on': False, # Power On Laser Diode at Startup
'ld_drive_current': { # Laser Diode Output Current (A)
'value': 0.0, # Value Set
'max': 0.3 # Max Value Settable
},
'pd_mon_params': { # Photodiode Parameters
'transconductance': 0.000115258765 # Board Specific Transconductance (1/ohm)
'responsitivity': 0.0141, # Responsitivity (A/W)
'i_dark': 0.0 # Max Value Settable (A)
},
'ld_pwr_limit': { # Laser Diode Power Limit (W)
'value': 0.00975, # Value Set
'max': 0.023321507 # Max Value Settable
},
'ld_terms_short: False, # Is Laser Diode Terminals short? (True/False)
},
'thermostat': {
'default_pwr_on': True, # Power on Thermostat at Startup
'pid_engaged': True, # True: PID Control Mode | False Constant Current Mode
'temperature_setpoint': 25.0, # Temperature Setpoint (Degree Celsius)
'tec_settings': {
'i_set': { # Current TEC Current Set by PID Controller/User (A)
'value': 0.04330516, # Value Set
'max': 3.0 # Max Value Settable
},
'max_v': { # Max Voltage Across Tec Terminals (V)
'value': 4.00000000, # Value Set
'max': 4.3 # Max Value Settable
},
'max_i_pos': { # Max Cooling Current Across Tec Terminals (A)
'value': 0.99628574, # Value Set
'max': 3.0 # Max Value Settable
},
'max_i_neg': { # Max Heating Current Across Tec Terminals (A)
'value': 0.99628574, # Value Set
'max': 3.0 # Max Value Settable
}
},
'pid_params': { # PID Controller Parameters
'kp': 0.15668282, # Proportional Gain
'ki': 0.0021359625, # Integral Gain
'kd': 0.8292545, # Derivative Gain
'output_min': -1.0, # Minimum Current Output (A)
'output_max': 1.0 # Maximum Current Output (A)
},
'temp_adc_settings': { # Temperature ADC Settings (Please read AD7172-2 Documentation)
'filter_type': 'Sinc5Sinc1With50hz60HzRejection', # Filter Types
'sinc5sinc1odr': None, # (Unused)
'sinc3odr': None, # (Unused)
'sinc5sinc1postfilter': None, # (Unused)
'sinc3fineodr': None, # (Unused)
'rate': 16.67 # ADC Sampling Rate (Hz)
},
'temp_mon_settings': { # Temperature Monitor Settings
'upper_limit': 40.0, # Temperature Upper Limit (Degree Celsius)
'lower_limit': 10.0 # Temperature Lower Limit (Degree Celsius)
},
'thermistor_params': { # Thermistor Steinhart-Hart equation parameters
't0': 25.0, # t0: Degree Celsius
'r0': 10000.0, # r0: Ohm
'b': 3900.0 # b: (unitless)
}
}
}
"""
return await self._send_cmd(self._cmd._target, self._cmd.GetSettingsSummary, msg_type="Settings", sig=sig)
async def dfu(self):
"""
Hard reset and put the connected Kirdy into the Dfu mode for firmware update.
"""
return await self._send_cmd(self._cmd._target, self._cmd.Dfu)
async def save_current_settings_to_flash(self):
"""
Save the current laser diode and thermostat configurations into flash.
"""
return await self._send_cmd(self._cmd._target, self._cmd.SaveFlashSettings)
async def restore_settings_from_flash(self):
"""
Restore the laser diode and thermostat settings from flash.
"""
return await self._send_cmd(self._cmd._target, self._cmd.LoadFlashSettings)
async def hard_reset(self):
"""
Hard Reset Kirdy. The socket connection will be closed by Kirdy.
Laser diode power and Tec power will be turned off.
Kirdy will send out a json({'msg_type': 'HardReset'}) to all sockets before hard reset take place.
"""
return await self._send_cmd(self._cmd._target, self._cmd.HardReset)
class Laser:
def __init__(self, send_cmd_handler):
self._cmd = CmdList.ld
self._send_cmd = send_cmd_handler
async def set_power_on(self, on):
"""
Power Up or Power Down laser diode. Powering up the Laser Diode resets the pwr_excursion status
- on (True/False)
"""
if on:
return await self._send_cmd(self._cmd._target, self._cmd.PowerUp, None)
else:
return await self._send_cmd(self._cmd._target, self._cmd.PowerDown, None)
async def set_default_pwr_on(self, on):
"""
Set whether laser diode is powered up at Startup.
- on (True/False)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on)
async def set_ld_terms_short(self, short):
"""
Open/Short laser diode terminals.
- on (True/False)
"""
if short:
return await self._send_cmd(self._cmd._target, self._cmd.LdTermsShort, None)
else:
return await self._send_cmd(self._cmd._target, self._cmd.LdTermsOpen, None)
async def set_i(self, i):
"""
Set laser diode output current: Max(0, Min(i_set, 300mA)).
- i: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetI, i)
async def set_pd_mon_responsitivity(self, responsitivity):
"""
Configure the photodiode monitor responsitivity parameter.
The value is only effective if ApplyPdParams cmd is issued.
- responsitivity: A/W
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdResponsitivity, responsitivity)
async def set_pd_mon_dark_current(self, dark_current):
"""
Configure the photodiode monitor dark current parameter.
The value is only effective if ApplyPdParams cmd is issued.
- dark_current: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPdDarkCurrent, dark_current)
async def apply_pd_params(self):
"""
Evaluate and apply photodiode monitor parameters that are set with SetPdDarkCurrent and SetPdResponsitivity cmd.
After Kirdy receives the cmd, it will check if the current power limit is within the newly calculated
power limit range. If it is out of range, the photodiode monitor parameters remains unchanged and Kirdy
sends out a "InvalidSettings" message along with an error message.
"""
return await self._send_cmd(self._cmd._target, self._cmd.ApplyPdParams)
async def set_ld_pwr_limit(self, pwr_limit):
"""
Set the power limit for the power excursion monitor.
If the power limit settings is out of range, power limit remains unchanged and Kirdy
sends out a "InvalidSettings" message along with an error message.
If the calculated power with the params of pd_mon > pwr_limit,
overpower protection is triggered.
- pwr_limit: W
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetLdPwrLimit, pwr_limit)
async def clear_alarm(self):
"""
Clear the power excursion monitor alarm.
"""
return await self._send_cmd(self._cmd._target, self._cmd.ClearAlarm)
class Thermostat:
def __init__(self, send_cmd_handler, send_raw_cmd_handler):
self._cmd = CmdList.thermostat
self._send_cmd = send_cmd_handler
self._send_raw_cmd = send_raw_cmd_handler
async def set_power_on(self, on):
"""
Power up or power down thermostat.
- Powering up the thermostat resets the pwr_excursion status
"""
if on:
return await self._send_cmd(self._cmd._target, self._cmd.PowerUp, None)
else:
return await self._send_cmd(self._cmd._target, self._cmd.PowerDown, None)
async def set_default_pwr_on(self, on):
"""
Set whether thermostat is powered up at Startup.
- on: (True/False)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetDefaultPowerOn, on)
async def set_tec_max_v(self, max_v):
"""
Set Tec Maximum Voltage Across the TEC Terminals.
- max_v: V
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxV, max_v)
async def set_tec_max_cooling_i(self, max_i_pos):
"""
Set Tec maximum cooling current (Settable Range: 0.0 - 3.0)
- max_i_pos: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxIPos, max_i_pos)
async def set_tec_max_heating_i(self, max_i_neg):
"""
Set Tec maximum heating current (Settable Range: 0.0 - 3.0)
- max_i_neg: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTecMaxINeg, max_i_neg)
async def set_tec_i_out(self, i_out):
"""
Set Tec Output Current (Settable Range: 0.0 - 3.0)
This cmd is only effective in constant current control mode
or your newly set value will be overwritten by PID Controller Output
- i_out: A
"""
if isinstance(i_out, float):
return await self._send_raw_cmd({"tec_set_i": i_out})
elif isinstance(i_out, int):
return await self._send_raw_cmd({"tec_set_i": float(i_out)})
else:
raise InvalidDataType
async def set_constant_current_control_mode(self):
"""
Disable PID Controller and output current can be controlled with set_tec_i_out() cmd.
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidDisEngage, None)
async def set_temperature_setpoint(self, temperature):
"""
Set Temperature Setpoint for PID Controller. This parameter is not active in constant current control mode
- temperature: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTemperatureSetpoint, temperature)
async def set_pid_control_mode(self):
"""
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
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidEngage, None)
async def set_pid_kp(self, kp):
"""
Set Kp parameter for PID Controller
kp: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidKp, kp)
async def set_pid_ki(self, ki):
"""
Set Ki parameter for PID Controller
ki: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidKi, ki)
async def set_pid_kd(self, kd):
"""
Set Kd parameter for PID Controller
kd: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidKd, kd)
async def set_pid_output_max(self, out_max):
"""
Set max output limit at the PID Output
- out_max: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidOutMax, out_max)
async def set_pid_output_min(self, out_min):
"""
Set min output limit at the PID Output
- out_min: A
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetPidOutMin, out_min)
async def set_temp_mon_upper_limit(self, upper_limit):
"""
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
- upper_limit: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTempMonUpperLimit, upper_limit)
async def set_temp_mon_lower_limit(self, lower_limit):
"""
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
- lower_limit: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetTempMonLowerLimit, lower_limit)
async def clear_alarm(self):
"""
Clear the temperature monitor alarm
"""
return await self._send_cmd(self._cmd._target, self._cmd.ClearAlarm)
async def set_sh_t0(self, t0):
"""
Set t0 Steinhart-Hart parameter for the laser diode NTC
- t0: Degree Celsius
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetShT0, t0)
async def set_sh_r0(self, r0):
"""
Set r0 Steinhart-Hart parameter for the laser diode NTC
- r0: Ohm
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetShR0, r0)
async def set_sh_beta(self, beta):
"""
Set beta Steinhart-Hart parameter for the laser diode NTC
- beta: (unitless)
"""
return await self._send_cmd(self._cmd._target, self._cmd.SetShBeta, beta)
async def config_temp_adc_filter(self, filter_config):
"""
Configure the temperature adc filter type and sampling rate.
Please refer to AD7172 datasheet for the usage of various types of filter.
The actual temperature polling rate is bottlenecked by the processing speed of the MCU and
performs differently under different kinds of workload. Please verify the polling rate with
the timestamp.
"""
cmd = {}
cmd[self._cmd._target] = self._cmd.ConfigTempAdcFilter.name
if hasattr(filter_config, 'rate'):
cmd[self._cmd.ConfigTempAdcFilter] = {
"filter_type": filter_config._filter_type(),
filter_config._odr_type: filter_config.rate,
}
else:
cmd[self._cmd.ConfigTempAdcFilter] = {
"filter_type": filter_config._filter_type(),
filter_config._odr_type: filter_config,
}
return await self._send_raw_cmd(cmd)
async def get_poll_interval(self):
return await self._send_cmd(self._cmd._target, self._cmd.GetPollInterval, msg_type="Interval")
class Kirdy:
def __init__(self):
self.device = Device(self._send_cmd, self._send_raw_cmd)
self.laser = Laser(self._send_cmd)
self.thermostat = Thermostat(self._send_cmd, self._send_raw_cmd)
self.hw_rev = 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._event_loop = None
self._msg_queue_get_report = 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
self._report_sig = None # Dict
self._connected_sig = None # Bool
self._err_msg_sig = None # Str
self.connected_event = None
def get_hw_rev(self):
return self.hw_rev
def set_report_sig(self, sig):
"""
Connect a PyQt Signal to the active report output(dict). This should be configured before the session is started.
"""
self._report_sig = sig
def set_connected_sig(self, sig):
"""
Connect a PyQt Signal to connection status(bool)
- True: Connection is established
- False: Connection is dropped
"""
self._connected_sig = sig
def set_err_msg_sig(self, sig):
"""
Emit a error message to a PyQt Signal(str) when a cmd fails to execute
"""
self._err_msg_sig = sig
def start_session(self, host='192.168.1.128', port=1337):
"""
Start Kirdy Connection Session.
In case of disconnection, all the queued tasks are cleared and the handler task retries TCP connection indefinitely.
- host: Kirdy's IP Address
- port: Kirdy's Port Number
"""
self._host, self._ctrl_port = host, port
if self._event_loop is None:
try:
self._event_loop = asyncio.get_running_loop()
except:
self._event_loop = asyncio.new_event_loop()
self._event_loop.run_forever()
self.connected_event = asyncio.Event()
self.handler_task = asyncio.create_task(self._handler())
async def end_session(self, block=False):
"""
Stop Kirdy's TCP connection and its associated thread.
"""
if self._event_loop is not None:
if block:
await self._task_queue.join()
if self.read_response_task is not None:
self.read_response_task.cancel()
await self.read_response_task
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
if self._connected_sig is not None:
self._connected_sig.emit(False)
def connecting(self):
"""
Return True if client is connecting
"""
return self._state == State.disconnected and self.read_response_task is not None
def connected(self):
"""
Returns True if client is connected.
"""
return self._writer is not None
async def wait_until_connected(self):
if not(self.connected()):
await self.connected_event.wait()
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:
report = await self._report_queue.get()
if not(isinstance(report, dict)):
self.stop_active_report()
else:
yield report
if isinstance(report, dict):
await self.device.set_active_report_mode(False)
else:
raise ConnectionError
def stop_report_mode(self):
self._report_mode_on = False
def task_dispatcher(self, awaitable_fn):
"""
Enqueue a task to be handled by the handler.
"""
if self.connected():
try:
self._task_queue.put_nowait(lambda: awaitable_fn)
return True
except asyncio.queues.QueueFull:
return False
else:
raise ConnectionError
async def _sock_disconnection_handling(self):
# Reader needn't be closed
try:
self._writer.close()
await self._writer.wait_closed()
except:
# In Hard Reset/DFU cmd, Kirdy may close its socket first
pass
self._reader = None
self._writer = None
for i in range(self._report_queue.maxsize):
if self._report_queue.full():
self._report_queue.get_nowait()
self._report_queue.put_nowait(None)
if self._connected_sig is not None:
self._connected_sig.emit(False)
async def _handler(self):
try:
self._state = State.disconnected
first_con = True
task = None
while True:
if self._state == State.disconnected:
try:
self.hw_rev = None
await self.__coninit(self._timeout)
self.read_response_task = asyncio.create_task(self._read_response_handler())
task = None
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
self._state = State.connected
except (OSError, TimeoutError):
if first_con:
first_con = False
logging.warning("Cannot connect to %s:%d. Retrying in the background.", self._host, self._ctrl_port)
await asyncio.sleep(5.0)
elif self._state == State.connected:
try:
task = await self._task_queue.get()
if isinstance(task, Exception):
raise task
await task()
self._task_queue.task_done()
except (TimeoutError, ConnectionResetError, ConnectionError):
logging.warning("Connection to Kirdy is dropped.")
first_con = True
self.read_response_task.cancel()
# State Transition
self._state = State.disconnected
await self._sock_disconnection_handling()
except asyncio.exceptions.CancelledError:
pass
except:
logging.warning("Handler experienced an error.", exc_info=True)
await self._sock_disconnection_handling()
async def _read_response_handler(self):
try:
while True:
if self._report_mode_on:
response = await asyncio.wait_for(self._read_response(), self._timeout)
else:
response = await self._read_response()
if response["msg_type"] == 'HardReset':
logging.warn("Kirdy is being hard reset.")
raise asyncio.exceptions.CancelledError
if response["msg_type"] == 'Dfu':
logging.warn("Kirdy enters Dfu Mode.")
asyncio.create_task(self.end_session())
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:
self._report_sig.emit(response)
else:
if self._msg_queue_get_report:
self._msg_queue_get_report = False
self._int_msg_queue.put_nowait_overwrite(response)
except asyncio.exceptions.CancelledError:
pass
except (TimeoutError, ConnectionResetError, ConnectionError) as exec:
self._task_queue.put_nowait_overwrite(exec)
self._int_msg_queue.put_nowait_overwrite(exec)
except Exception as exec:
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:
self._report_mode_on = False
self._report_queue.put_nowait_overwrite(TimeoutError)
async def _stop_handler(self):
for task in asyncio.all_tasks():
task.cancel()
await asyncio.gather(*asyncio.all_tasks(), loop=self._event_loop)
async def __coninit(self, timeout):
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._report_queue = asyncio.Queue(maxsize=16)
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.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
async def _read_response(self):
raw_response = b''
while len(raw_response) == 0:
# Ignore 0 size packet
raw_response = await self._reader.readuntil()
response = raw_response.decode('utf-8', errors='ignore').split("\n")
return json.loads(response[0])
def _response_handling(self, msg, msg_type, sig=None):
if msg["msg_type"] in ["InvalidCmd", "InvalidDatatype"]:
raise InvalidCmd
elif msg["msg_type"] == msg_type:
if sig is not None:
sig.emit(msg)
else:
logging.warn(f"Commands fail to execute. {msg['msg_type']}:{msg['msg']}")
if self._err_msg_sig is not None and msg['msg'] is not None:
self._err_msg_sig.emit(msg['msg'])
return msg
async def _send_raw_cmd(self, cmd, msg_type="Acknowledge", sig=None):
if self.connected():
async with self._lock:
self._writer.write(bytes(json.dumps(cmd), "UTF-8"))
await self._writer.drain()
msg = await asyncio.wait_for(self._int_msg_queue.get(), self._timeout)
return self._response_handling(msg, msg_type, sig)
else:
raise ConnectionError
async def _send_cmd(self, target, cmd, data=None, msg_type="Acknowledge", sig=None):
cmd_dict = {}
cmd_dict[target] = cmd.name
if cmd == _dt.f32:
if isinstance(data, float):
cmd_dict[cmd] = data
elif isinstance(data, int):
cmd_dict[cmd] = float(data)
elif cmd == _dt.bool:
if isinstance(data, bool):
cmd_dict[cmd] = data
else:
raise InvalidDataType
elif cmd == "None":
pass
if msg_type == 'Report':
self._msg_queue_get_report = True
async with self._lock:
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
return self._response_handling(msg, msg_type, sig)