scietex.hal.vfd 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ """Scietex VFD HAL package."""
2
+
3
+ from .version import __version__ as __version__
@@ -0,0 +1,47 @@
1
+ """
2
+ Base VFD subpackage.
3
+
4
+ Provides the transport-agnostic driver contract, the capability model, the
5
+ shared data structures, and the RS485 base class the direct-serial drivers
6
+ derive from.
7
+
8
+ Classes:
9
+ VFD: Base class for drivers that own a serial connection.
10
+ VFDCapabilities: The control surface of one drive.
11
+ VFDError: A fault code paired with its message.
12
+ VFDParameters: A snapshot of the drive's operating parameters.
13
+ VFDProtocol: The contract every driver implements.
14
+ VFDState: The drive run state.
15
+
16
+ Modules:
17
+ protocol: Defines the `VFDProtocol` driver contract.
18
+ capabilities: Defines the `VFDCapabilities` model.
19
+ data: Defines the data structures shared by every driver.
20
+ rs485: Defines the `VFD` serial base class.
21
+ """
22
+
23
+ from .capabilities import VFDCapabilities
24
+ from .data import (
25
+ VFDClearErrorCMD,
26
+ VFDError,
27
+ VFDParameters,
28
+ VFDSetFrequencyCMD,
29
+ VFDStartCMD,
30
+ VFDState,
31
+ VFDStopCMD,
32
+ )
33
+ from .protocol import VFDProtocol
34
+ from .rs485 import VFD
35
+
36
+ __all__ = [
37
+ "VFD",
38
+ "VFDCapabilities",
39
+ "VFDClearErrorCMD",
40
+ "VFDError",
41
+ "VFDParameters",
42
+ "VFDProtocol",
43
+ "VFDSetFrequencyCMD",
44
+ "VFDStartCMD",
45
+ "VFDState",
46
+ "VFDStopCMD",
47
+ ]
@@ -0,0 +1,49 @@
1
+ """
2
+ VFD capability model.
3
+
4
+ A drive advertises what it can do through a single :class:`VFDCapabilities`
5
+ value. The fields are the operations a caller can dispatch without inspecting
6
+ the driver: whether the drive accepts a direction choice, a jog command, a
7
+ coasting stop, a fault reset, and a frequency setpoint.
8
+
9
+ The set is a frozen dataclass so a driver declares it once as a class attribute
10
+ and callers cannot mutate it.
11
+
12
+ Capabilities describe the *control* surface only. Whether a drive reports a
13
+ particular *reading* — a heatsink temperature, a DC bus voltage — is answered by
14
+ the corresponding :class:`scietex.hal.vfd.base.data.VFDParameters` field being
15
+ ``None``, not by a flag here. One source of truth cannot drift from the driver.
16
+
17
+ Classes:
18
+ VFDCapabilities: The control surface of one drive.
19
+ """
20
+
21
+ from dataclasses import dataclass
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class VFDCapabilities:
26
+ """
27
+ The control surface of one drive.
28
+
29
+ Every field defaults to "unsupported", so a driver declares only what its
30
+ hardware exposes. Read-side operations (parameters, state, error code) are
31
+ mandatory for every driver and are therefore not advertised here.
32
+
33
+ Attributes:
34
+ direction (bool): Whether the drive can be started in either rotation
35
+ direction. ``False`` means forward only.
36
+ jog (bool): Whether the drive accepts a jog command.
37
+ freewheel_stop (bool): Whether the drive can stop by coasting instead of
38
+ ramping down.
39
+ fault_reset (bool): Whether the drive accepts an explicit fault reset.
40
+ frequency_setpoint (bool): Whether the drive accepts a frequency
41
+ setpoint over the communication protocol, independent of the
42
+ keypad or an analog input.
43
+ """
44
+
45
+ direction: bool = True
46
+ jog: bool = False
47
+ freewheel_stop: bool = False
48
+ fault_reset: bool = False
49
+ frequency_setpoint: bool = False
@@ -0,0 +1,122 @@
1
+ """VFD data structures.
2
+
3
+ :class:`VFDParameters` names the operating frequency ``frequency_hz`` so it
4
+ cannot be confused with ``frequency_percent``, which is the unit some drives
5
+ report natively. The two frequency fields are the only ones guaranteed to be
6
+ populated; every other field is optional and stays ``None`` when the drive's
7
+ protocol does not expose it.
8
+
9
+ ``None`` and ``0.0`` mean different things throughout this module: ``None`` is
10
+ "this drive does not report the value", ``0.0`` is "the drive reports zero".
11
+ A caller that plots a temperature must check for ``None`` rather than treating
12
+ a missing reading as a cold drive.
13
+ """
14
+
15
+ from enum import Enum
16
+
17
+ import msgspec
18
+
19
+
20
+ class VFDState(Enum):
21
+ """Possible states of a Variable Frequency Drive (VFD)."""
22
+
23
+ UNKNOWN = 0
24
+ STOPPED = 1
25
+ RUNNING_FORWARD = 2
26
+ RUNNING_BACKWARD = 3 # or RUNNING_REVERSE
27
+
28
+ def __str__(self) -> str:
29
+ """Return a human-readable string representation."""
30
+ return self.name.replace("_", " ").title()
31
+
32
+
33
+ class VFDError(msgspec.Struct, frozen=True):
34
+ """Model of a VFD error with code and message."""
35
+
36
+ code: int
37
+ message: str = "Unknown error"
38
+
39
+ def __str__(self) -> str:
40
+ return f"VFDError(code={self.code}, message='{self.message}')"
41
+
42
+
43
+ class VFDParameters(msgspec.Struct, frozen=True):
44
+ """Model for VFD operational parameters.
45
+
46
+ Attributes:
47
+ frequency_hz: Output frequency in Hz.
48
+ frequency_percent: Output frequency as a percentage of the configured
49
+ maximum, 0-100.
50
+ output_current: Output current in Amps.
51
+ output_voltage: Output voltage in Volts.
52
+ output_power: Output power in kW.
53
+ dc_bus_voltage: DC bus (link) voltage in Volts, or ``None`` when the
54
+ drive does not report it.
55
+ heatsink_temperature_c: Power-module or heatsink temperature in degrees
56
+ Celsius, or ``None`` when the drive does not report it. A drive that
57
+ signals overheat only through a fault code reports ``None`` here:
58
+ a fault is not a temperature.
59
+ motor_temperature_c: Motor winding or PTC temperature in degrees
60
+ Celsius, or ``None`` when the drive does not report it.
61
+ state: Current VFD state.
62
+ """
63
+
64
+ frequency_hz: float # Current operating frequency in Hz
65
+ frequency_percent: float # Frequency as a percentage of max (0-100%)
66
+ output_current: float = 0.0 # Output current in Amps
67
+ output_voltage: float = 0.0 # Output voltage in Volts
68
+ output_power: float = 0.0 # Output power in kW
69
+ dc_bus_voltage: float | None = None # DC bus voltage in Volts
70
+ heatsink_temperature_c: float | None = None # Power-module temperature, deg C
71
+ motor_temperature_c: float | None = None # Motor temperature, deg C
72
+ state: VFDState = VFDState.STOPPED # Current VFD state
73
+
74
+ def __str__(self) -> str:
75
+ return (
76
+ f"VFDParameters(frequency={self.frequency_hz} Hz, "
77
+ f"frequency_percent={self.frequency_percent}%, "
78
+ f"current={self.output_current} A, "
79
+ f"voltage={self.output_voltage} V, "
80
+ f"power={self.output_power} kW, "
81
+ f"dc_bus={self.dc_bus_voltage} V, "
82
+ f"heatsink={self.heatsink_temperature_c} C, "
83
+ f"motor={self.motor_temperature_c} C, "
84
+ f"state={self.state})"
85
+ )
86
+
87
+
88
+ class VFDStartCMD(msgspec.Struct, frozen=True):
89
+ """VFD start command."""
90
+
91
+ forward: bool = True
92
+ slow: bool = False
93
+
94
+ def __str__(self) -> str:
95
+ return f"VFDStartCMD(forward={self.forward}, slow={self.slow})"
96
+
97
+
98
+ class VFDStopCMD(msgspec.Struct, frozen=True):
99
+ """VFD stop command."""
100
+
101
+ freewheel: bool = False
102
+
103
+ def __str__(self) -> str:
104
+ return f"VFDStopCMD(freewheel={self.freewheel})"
105
+
106
+
107
+ class VFDClearErrorCMD(msgspec.Struct, frozen=True):
108
+ """VFD clear error command."""
109
+
110
+ communication: bool = False
111
+
112
+ def __str__(self) -> str:
113
+ return f"VFDClearErrorCMD(communication={self.communication})"
114
+
115
+
116
+ class VFDSetFrequencyCMD(msgspec.Struct, frozen=True):
117
+ """VFD frequency setpoint command."""
118
+
119
+ frequency_hz: float = 0.0
120
+
121
+ def __str__(self) -> str:
122
+ return f"VFDSetFrequencyCMD(frequency_hz={self.frequency_hz})"
@@ -0,0 +1,129 @@
1
+ """
2
+ VFD protocol.
3
+
4
+ This module defines the transport-agnostic contract every variable frequency
5
+ drive driver implements, plus the capability model that tells a caller which
6
+ optional control operations a given drive supports. The contract is deliberately
7
+ narrow: a driver must be able to connect, disconnect, read the operating
8
+ parameters and state, read and reset the fault code, start and stop the motor,
9
+ and set the frequency. Everything else — a jog command, for example — is
10
+ advertised through :attr:`VFDProtocol.capabilities`, so a caller can reject an
11
+ unsupported command before dispatching it instead of catching an exception after
12
+ the fact.
13
+
14
+ The same contract covers both transports in this package: the direct RS485
15
+ clients derived from :class:`scietex.hal.vfd.base.rs485.VFD` and the Modbus/TCP
16
+ clients that address a drive through the Scietex Modbus gateway.
17
+
18
+ Classes:
19
+ VFDProtocol: The runtime-checkable protocol every drive driver satisfies.
20
+ """
21
+
22
+ from typing import Any, Protocol, runtime_checkable
23
+
24
+ from .capabilities import VFDCapabilities
25
+ from .data import VFDParameters, VFDState
26
+
27
+
28
+ @runtime_checkable
29
+ class VFDProtocol(Protocol):
30
+ """
31
+ Contract implemented by every variable frequency drive driver.
32
+
33
+ Attributes:
34
+ label (str): Human-readable drive label, unique within a service.
35
+ capabilities (VFDCapabilities): What this drive can do.
36
+ display_name (str): Model name reported to the user interface.
37
+ """
38
+
39
+ label: str
40
+
41
+ capabilities: VFDCapabilities = VFDCapabilities()
42
+ display_name: str = "VFD"
43
+
44
+ async def connect(self) -> None:
45
+ """Open the transport to the drive."""
46
+
47
+ async def close(self) -> None:
48
+ """Close the transport to the drive."""
49
+
50
+ async def read_data(self) -> dict[str, Any]:
51
+ """
52
+ Read a snapshot of the drive's current values.
53
+
54
+ Returns:
55
+ dict[str, Any]: Field name to value mapping. ``state`` is always
56
+ present; the remaining keys depend on the driver.
57
+ """
58
+
59
+ async def read_parameters(self) -> VFDParameters:
60
+ """
61
+ Read the drive's operating parameters.
62
+
63
+ Returns:
64
+ VFDParameters: Frequency in both Hz and percent of the configured
65
+ maximum, output current, voltage, power and state, plus the
66
+ optional DC bus voltage and temperatures the drive reports.
67
+ """
68
+
69
+ async def read_state(self) -> VFDState:
70
+ """
71
+ Read the drive's run state.
72
+
73
+ Returns:
74
+ VFDState: The state, or ``VFDState.UNKNOWN`` when the drive reports
75
+ a value the protocol does not define.
76
+ """
77
+
78
+ async def read_error_code(self, comm: bool = False) -> int:
79
+ """
80
+ Read the latched fault code.
81
+
82
+ Args:
83
+ comm: Read the communication-protocol fault code instead of the
84
+ drive fault code.
85
+
86
+ Returns:
87
+ int: The fault code; ``0`` means no fault.
88
+ """
89
+
90
+ async def clear_error(self, comm: bool = False) -> int:
91
+ """
92
+ Reset the latched fault and read the code back.
93
+
94
+ Args:
95
+ comm: Reset the communication-protocol fault instead of the drive
96
+ fault.
97
+
98
+ Returns:
99
+ int: The fault code read after the reset; ``0`` means the fault is
100
+ gone.
101
+ """
102
+
103
+ async def start(self, forward: bool = True, slow: bool = False) -> None:
104
+ """
105
+ Start the motor.
106
+
107
+ Args:
108
+ forward: Run in the forward direction; ``False`` runs in reverse.
109
+ slow: Use the jog (slow) command instead of the normal run command.
110
+ """
111
+
112
+ async def stop(self, freewheel: bool = False) -> None:
113
+ """
114
+ Stop the motor.
115
+
116
+ Args:
117
+ freewheel: Coast to a stop instead of ramping down.
118
+ """
119
+
120
+ async def set_frequency(self, frequency_hz: float) -> None:
121
+ """
122
+ Set the frequency setpoint.
123
+
124
+ The value is in Hz, not percent: the caller works in engineering units
125
+ and each driver converts with its own maximum-frequency parameter.
126
+
127
+ Args:
128
+ frequency_hz: The requested output frequency in Hz.
129
+ """
@@ -0,0 +1,153 @@
1
+ """Basic VFD device prototype class to be inherited by particular implementations."""
2
+
3
+ from logging import Logger
4
+ from typing import Any
5
+
6
+ from scietex.hal.serial import (
7
+ ModbusSerialConnectionConfig,
8
+ RS485Client,
9
+ SerialConnectionConfig,
10
+ )
11
+
12
+ from .capabilities import VFDCapabilities
13
+ from .data import (
14
+ VFDClearErrorCMD,
15
+ VFDError,
16
+ VFDParameters,
17
+ VFDSetFrequencyCMD,
18
+ VFDStartCMD,
19
+ VFDState,
20
+ VFDStopCMD,
21
+ )
22
+ from .protocol import VFDProtocol
23
+
24
+
25
+ class VFD(RS485Client, VFDProtocol):
26
+ """VFD control base class"""
27
+
28
+ capabilities = VFDCapabilities()
29
+ display_name = "VFD"
30
+
31
+ def __init__(
32
+ self,
33
+ con_params: SerialConnectionConfig | ModbusSerialConnectionConfig,
34
+ address: int = 1,
35
+ label: str = "VFD",
36
+ logger: Logger | None = None,
37
+ **kwargs,
38
+ ):
39
+ super().__init__(
40
+ con_params=con_params,
41
+ address=address,
42
+ label=label,
43
+ logger=logger,
44
+ **kwargs,
45
+ )
46
+ self.error_codes: dict[int, VFDError] = {0: VFDError(code=0, message="No error")}
47
+ self.error_codes_com: dict[int, VFDError] = {0: VFDError(code=0, message="No error")}
48
+
49
+ async def connect(self) -> None:
50
+ """Nothing to do: the serial client opens the port once per operation."""
51
+ self.logger.debug("%s: Connect (per-operation serial client).", self.label)
52
+
53
+ # Errors processing methods
54
+ async def read_error_code(self, comm: bool = False) -> int:
55
+ """Read error code from the VFD"""
56
+ code = 0
57
+ if comm:
58
+ self.logger.debug("%s: Communication error code %d.", self.label, code)
59
+ else:
60
+ self.logger.debug("%s: Error code %d.", self.label, code)
61
+ return code
62
+
63
+ def parse_error_code(self, code: int, comm: bool = False) -> VFDError:
64
+ """Parse error code from the VFD"""
65
+ if comm:
66
+ if code in self.error_codes_com:
67
+ return self.error_codes_com[code]
68
+ else:
69
+ if code in self.error_codes:
70
+ return self.error_codes[code]
71
+ return VFDError(code=code, message="Unknown error")
72
+
73
+ async def clear_error(self, comm: bool = False) -> int:
74
+ """Clear error from the VFD"""
75
+ if comm:
76
+ self.logger.debug("%s: Clear communication error.", self.label)
77
+ else:
78
+ self.logger.debug("%s: Clear error.", self.label)
79
+ return await self.read_error_code(comm)
80
+
81
+ # Parameters monitoring methods
82
+ async def read_parameters(self) -> VFDParameters:
83
+ """Read the VFD operating parameters"""
84
+ parameters = VFDParameters(frequency_hz=0.0, frequency_percent=0.0)
85
+ self.logger.debug("%s: %s", self.label, parameters)
86
+ return parameters
87
+
88
+ async def read_state(self) -> VFDState:
89
+ """Read the VFD run state"""
90
+ state = VFDState.UNKNOWN
91
+ self.logger.debug("%s: Current state: %s", self.label, state)
92
+ return state
93
+
94
+ # VFD Control methods
95
+ async def start(self, forward: bool = True, slow: bool = False) -> None:
96
+ """Start the VFD"""
97
+ self.logger.debug("%s: Start (forward: %s, slow: %s)", self.label, forward, slow)
98
+
99
+ async def stop(self, freewheel: bool = False) -> None:
100
+ """Stop the VFD"""
101
+ self.logger.debug("%s: Stop (freewheel: %s)", self.label, freewheel)
102
+
103
+ async def set_frequency(self, frequency_hz: float) -> None:
104
+ """Set the VFD frequency setpoint"""
105
+ self.logger.debug("%s: Set frequency %s Hz.", self.label, frequency_hz)
106
+
107
+ async def read_data(self) -> dict[str, Any]:
108
+ """
109
+ Read the VFD operating parameters as a plain dictionary.
110
+
111
+ The return type is widened to ``Any`` because `VFDParameters` carries
112
+ optional fields (temperatures, DC bus voltage) that are ``None`` when the
113
+ drive does not report them, which the inherited `RS485Client` signature
114
+ does not allow.
115
+ """
116
+ parameters = await self.read_parameters()
117
+ return {f: getattr(parameters, f) for f in parameters.__struct_fields__}
118
+
119
+ async def process_message(self, message: dict[str, Any]) -> None:
120
+ await super().process_message(message)
121
+ if "cmd" in message:
122
+ if message["cmd"] == "start":
123
+ start_params = VFDStartCMD()
124
+ if "data" in message:
125
+ try:
126
+ start_params = VFDStartCMD(**message["data"])
127
+ except (TypeError, ValueError):
128
+ pass
129
+ await self.start(forward=start_params.forward, slow=start_params.slow)
130
+ elif message["cmd"] == "stop":
131
+ stop_params = VFDStopCMD()
132
+ if "data" in message:
133
+ try:
134
+ stop_params = VFDStopCMD(**message["data"])
135
+ except (TypeError, ValueError):
136
+ pass
137
+ await self.stop(freewheel=stop_params.freewheel)
138
+ elif message["cmd"] == "clear_error":
139
+ clear_params = VFDClearErrorCMD()
140
+ if "data" in message:
141
+ try:
142
+ clear_params = VFDClearErrorCMD(**message["data"])
143
+ except (TypeError, ValueError):
144
+ pass
145
+ await self.clear_error(comm=clear_params.communication)
146
+ elif message["cmd"] == "set_frequency":
147
+ frequency_params = VFDSetFrequencyCMD()
148
+ if "data" in message:
149
+ try:
150
+ frequency_params = VFDSetFrequencyCMD(**message["data"])
151
+ except (TypeError, ValueError):
152
+ pass
153
+ await self.set_frequency(frequency_hz=frequency_params.frequency_hz)
@@ -0,0 +1,5 @@
1
+ """Intek VFDs"""
2
+
3
+ from .spe_b import IntekSPEBModbusVFD, SPEvB
4
+
5
+ __all__ = ["IntekSPEBModbusVFD", "SPEvB"]
@@ -0,0 +1,26 @@
1
+ """
2
+ Intek SPE-B drivers.
3
+
4
+ Two transports are supported for the same register map:
5
+
6
+ - :class:`SPEvB` talks Modbus RTU to the drive over a direct serial connection.
7
+ - :class:`IntekSPEBModbusVFD` talks Modbus/TCP to the Scietex Modbus gateway,
8
+ which owns the serial bus and routes requests to the drive by unit id.
9
+
10
+ Modules:
11
+ registers: Register addresses, control and status codes, scaling helpers.
12
+ errors: Drive and communication fault code tables.
13
+ rs485: The direct-serial client.
14
+ client: The Modbus/TCP gateway client.
15
+ """
16
+
17
+ from .client import IntekSPEBModbusVFD
18
+ from .errors import SPE_B_ERROR_CODES, SPE_B_ERROR_CODES_COM
19
+ from .rs485 import SPEvB
20
+
21
+ __all__ = [
22
+ "IntekSPEBModbusVFD",
23
+ "SPE_B_ERROR_CODES",
24
+ "SPE_B_ERROR_CODES_COM",
25
+ "SPEvB",
26
+ ]