arthexis 0.1.9__py3-none-any.whl → 0.1.26__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.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- arthexis-0.1.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
nodes/feature_checks.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, Dict, Iterable, Optional
|
|
5
|
+
|
|
6
|
+
from django.contrib import messages
|
|
7
|
+
|
|
8
|
+
if False: # pragma: no cover - typing imports only
|
|
9
|
+
from .models import Node, NodeFeature
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class FeatureCheckResult:
|
|
14
|
+
"""Outcome of a feature validation."""
|
|
15
|
+
|
|
16
|
+
success: bool
|
|
17
|
+
message: str
|
|
18
|
+
level: int = messages.INFO
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FeatureCheck = Callable[["NodeFeature", Optional["Node"]], Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FeatureCheckRegistry:
|
|
25
|
+
"""Registry for feature validation callbacks."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._checks: Dict[str, FeatureCheck] = {}
|
|
29
|
+
self._default_check: Optional[FeatureCheck] = None
|
|
30
|
+
|
|
31
|
+
def register(self, slug: str) -> Callable[[FeatureCheck], FeatureCheck]:
|
|
32
|
+
"""Register ``func`` as the validator for ``slug``."""
|
|
33
|
+
|
|
34
|
+
def decorator(func: FeatureCheck) -> FeatureCheck:
|
|
35
|
+
self._checks[slug] = func
|
|
36
|
+
return func
|
|
37
|
+
|
|
38
|
+
return decorator
|
|
39
|
+
|
|
40
|
+
def register_default(self, func: FeatureCheck) -> FeatureCheck:
|
|
41
|
+
"""Register ``func`` as the fallback validator."""
|
|
42
|
+
|
|
43
|
+
self._default_check = func
|
|
44
|
+
return func
|
|
45
|
+
|
|
46
|
+
def get(self, slug: str) -> Optional[FeatureCheck]:
|
|
47
|
+
return self._checks.get(slug)
|
|
48
|
+
|
|
49
|
+
def items(self) -> Iterable[tuple[str, FeatureCheck]]:
|
|
50
|
+
return self._checks.items()
|
|
51
|
+
|
|
52
|
+
def run(
|
|
53
|
+
self, feature: "NodeFeature", *, node: Optional["Node"] = None
|
|
54
|
+
) -> Optional[FeatureCheckResult]:
|
|
55
|
+
check = self._checks.get(feature.slug)
|
|
56
|
+
if check is None:
|
|
57
|
+
check = self._default_check
|
|
58
|
+
if check is None:
|
|
59
|
+
return None
|
|
60
|
+
result = check(feature, node)
|
|
61
|
+
return self._normalize_result(feature, result)
|
|
62
|
+
|
|
63
|
+
def _normalize_result(
|
|
64
|
+
self, feature: "NodeFeature", result: Any
|
|
65
|
+
) -> FeatureCheckResult:
|
|
66
|
+
if isinstance(result, FeatureCheckResult):
|
|
67
|
+
return result
|
|
68
|
+
if result is None:
|
|
69
|
+
return FeatureCheckResult(
|
|
70
|
+
True,
|
|
71
|
+
f"{feature.display} check completed successfully.",
|
|
72
|
+
messages.SUCCESS,
|
|
73
|
+
)
|
|
74
|
+
if isinstance(result, tuple) and len(result) >= 2:
|
|
75
|
+
success, message, *rest = result
|
|
76
|
+
level = rest[0] if rest else (
|
|
77
|
+
messages.SUCCESS if success else messages.ERROR
|
|
78
|
+
)
|
|
79
|
+
return FeatureCheckResult(bool(success), str(message), int(level))
|
|
80
|
+
if isinstance(result, bool):
|
|
81
|
+
message = (
|
|
82
|
+
f"{feature.display} check {'passed' if result else 'failed'}."
|
|
83
|
+
)
|
|
84
|
+
level = messages.SUCCESS if result else messages.ERROR
|
|
85
|
+
return FeatureCheckResult(result, message, level)
|
|
86
|
+
raise TypeError(
|
|
87
|
+
f"Unsupported feature check result type: {type(result)!r}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
feature_checks = FeatureCheckRegistry()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@feature_checks.register_default
|
|
95
|
+
def _default_feature_check(
|
|
96
|
+
feature: "NodeFeature", node: Optional["Node"]
|
|
97
|
+
) -> FeatureCheckResult:
|
|
98
|
+
from .models import Node
|
|
99
|
+
|
|
100
|
+
target: Optional["Node"] = node or Node.get_local()
|
|
101
|
+
if target is None:
|
|
102
|
+
return FeatureCheckResult(
|
|
103
|
+
False,
|
|
104
|
+
f"No local node is registered; cannot verify {feature.display}.",
|
|
105
|
+
messages.WARNING,
|
|
106
|
+
)
|
|
107
|
+
try:
|
|
108
|
+
enabled = feature.is_enabled
|
|
109
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
110
|
+
return FeatureCheckResult(
|
|
111
|
+
False,
|
|
112
|
+
f"{feature.display} check failed: {exc}",
|
|
113
|
+
messages.ERROR,
|
|
114
|
+
)
|
|
115
|
+
if enabled:
|
|
116
|
+
return FeatureCheckResult(
|
|
117
|
+
True,
|
|
118
|
+
f"{feature.display} is enabled on {target.hostname}.",
|
|
119
|
+
messages.SUCCESS,
|
|
120
|
+
)
|
|
121
|
+
return FeatureCheckResult(
|
|
122
|
+
False,
|
|
123
|
+
f"{feature.display} is not enabled on {target.hostname}.",
|
|
124
|
+
messages.WARNING,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"FeatureCheck",
|
|
130
|
+
"FeatureCheckRegistry",
|
|
131
|
+
"FeatureCheckResult",
|
|
132
|
+
"feature_checks",
|
|
133
|
+
]
|
nodes/lcd.py
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
"""Minimal driver for PCF8574/PCF8574A I2C LCD1602 displays.
|
|
2
|
-
|
|
3
|
-
The implementation is adapted from the example provided in the
|
|
4
|
-
instructions. It is intentionally lightweight and only implements the
|
|
5
|
-
operations required for this project: initialisation, clearing the
|
|
6
|
-
screen and writing text to a specific position.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import subprocess
|
|
12
|
-
import time
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
|
|
15
|
-
try: # pragma: no cover - hardware dependent
|
|
16
|
-
import smbus # type: ignore
|
|
17
|
-
except Exception: # pragma: no cover - missing dependency
|
|
18
|
-
try: # pragma: no cover - hardware dependent
|
|
19
|
-
import smbus2 as smbus # type: ignore
|
|
20
|
-
except Exception: # pragma: no cover - missing dependency
|
|
21
|
-
smbus = None # type: ignore
|
|
22
|
-
|
|
23
|
-
SMBUS_HINT = (
|
|
24
|
-
"smbus module not found. Enable the I2C interface and install the dependencies.\n"
|
|
25
|
-
"For Debian/Ubuntu run: sudo apt-get install i2c-tools python3-smbus\n"
|
|
26
|
-
"Within the virtualenv: pip install smbus2"
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class LCDUnavailableError(RuntimeError):
|
|
31
|
-
"""Raised when the LCD cannot be initialised."""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@dataclass
|
|
35
|
-
class _BusWrapper:
|
|
36
|
-
"""Wrapper around :class:`smbus.SMBus` to allow mocking in tests."""
|
|
37
|
-
|
|
38
|
-
channel: int
|
|
39
|
-
|
|
40
|
-
def write_byte(
|
|
41
|
-
self, addr: int, data: int
|
|
42
|
-
) -> None: # pragma: no cover - thin wrapper
|
|
43
|
-
if smbus is None:
|
|
44
|
-
raise LCDUnavailableError(SMBUS_HINT)
|
|
45
|
-
bus = smbus.SMBus(self.channel)
|
|
46
|
-
bus.write_byte(addr, data)
|
|
47
|
-
bus.close()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class CharLCD1602:
|
|
51
|
-
"""Minimal driver for PCF8574/PCF8574A I2C backpack (LCD1602)."""
|
|
52
|
-
|
|
53
|
-
def __init__(self, bus: _BusWrapper | None = None) -> None:
|
|
54
|
-
if smbus is None: # pragma: no cover - hardware dependent
|
|
55
|
-
raise LCDUnavailableError(SMBUS_HINT)
|
|
56
|
-
self.bus = bus or _BusWrapper(1)
|
|
57
|
-
self.BLEN = 1
|
|
58
|
-
self.PCF8574_address = 0x27
|
|
59
|
-
self.PCF8574A_address = 0x3F
|
|
60
|
-
self.LCD_ADDR = self.PCF8574_address
|
|
61
|
-
|
|
62
|
-
def _write_word(self, addr: int, data: int) -> None:
|
|
63
|
-
if self.BLEN:
|
|
64
|
-
data |= 0x08
|
|
65
|
-
else:
|
|
66
|
-
data &= 0xF7
|
|
67
|
-
self.bus.write_byte(addr, data)
|
|
68
|
-
|
|
69
|
-
def _pulse_enable(self, data: int) -> None:
|
|
70
|
-
self._write_word(self.LCD_ADDR, data | 0x04)
|
|
71
|
-
time.sleep(0.0005)
|
|
72
|
-
self._write_word(self.LCD_ADDR, data & ~0x04)
|
|
73
|
-
time.sleep(0.0001)
|
|
74
|
-
|
|
75
|
-
def send_command(self, cmd: int) -> None:
|
|
76
|
-
high = cmd & 0xF0
|
|
77
|
-
low = (cmd << 4) & 0xF0
|
|
78
|
-
self._write_word(self.LCD_ADDR, high)
|
|
79
|
-
self._pulse_enable(high)
|
|
80
|
-
self._write_word(self.LCD_ADDR, low)
|
|
81
|
-
self._pulse_enable(low)
|
|
82
|
-
# Give the LCD time to process the command to avoid garbled output.
|
|
83
|
-
time.sleep(0.001)
|
|
84
|
-
|
|
85
|
-
def send_data(self, data: int) -> None:
|
|
86
|
-
high = (data & 0xF0) | 0x01
|
|
87
|
-
low = ((data << 4) & 0xF0) | 0x01
|
|
88
|
-
self._write_word(self.LCD_ADDR, high)
|
|
89
|
-
self._pulse_enable(high)
|
|
90
|
-
self._write_word(self.LCD_ADDR, low)
|
|
91
|
-
self._pulse_enable(low)
|
|
92
|
-
# Allow the LCD controller to catch up between data writes.
|
|
93
|
-
time.sleep(0.001)
|
|
94
|
-
|
|
95
|
-
def i2c_scan(self) -> list[str]: # pragma: no cover - requires hardware
|
|
96
|
-
"""Return a list of detected I2C addresses.
|
|
97
|
-
|
|
98
|
-
The implementation relies on the external ``i2cdetect`` command. On
|
|
99
|
-
systems where ``i2c-tools`` is not installed or the command cannot be
|
|
100
|
-
executed (e.g. insufficient permissions), the function returns an empty
|
|
101
|
-
list so callers can fall back to a sensible default address.
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
output = subprocess.check_output(["i2cdetect", "-y", "1"], text=True)
|
|
106
|
-
except Exception: # pragma: no cover - depends on environment
|
|
107
|
-
return []
|
|
108
|
-
|
|
109
|
-
addresses: list[str] = []
|
|
110
|
-
for line in output.splitlines()[1:]:
|
|
111
|
-
parts = line.split()
|
|
112
|
-
for token in parts[1:]:
|
|
113
|
-
if token != "--":
|
|
114
|
-
addresses.append(token)
|
|
115
|
-
return addresses
|
|
116
|
-
|
|
117
|
-
def init_lcd(self, addr: int | None = None, bl: int = 1) -> None:
|
|
118
|
-
self.BLEN = 1 if bl else 0
|
|
119
|
-
if addr is None:
|
|
120
|
-
try:
|
|
121
|
-
found = self.i2c_scan()
|
|
122
|
-
except Exception: # pragma: no cover - i2c detection issues
|
|
123
|
-
found = []
|
|
124
|
-
if "3f" in found or "3F" in found:
|
|
125
|
-
self.LCD_ADDR = self.PCF8574A_address
|
|
126
|
-
else:
|
|
127
|
-
# Default to the common PCF8574 address (0x27) when detection
|
|
128
|
-
# fails or returns no recognised addresses. This mirrors the
|
|
129
|
-
# behaviour prior to introducing automatic address detection and
|
|
130
|
-
# prevents the display from remaining uninitialised on systems
|
|
131
|
-
# without ``i2c-tools``.
|
|
132
|
-
self.LCD_ADDR = self.PCF8574_address
|
|
133
|
-
else:
|
|
134
|
-
self.LCD_ADDR = addr
|
|
135
|
-
|
|
136
|
-
time.sleep(0.05)
|
|
137
|
-
self.send_command(0x33)
|
|
138
|
-
self.send_command(0x32)
|
|
139
|
-
self.send_command(0x28)
|
|
140
|
-
self.send_command(0x0C)
|
|
141
|
-
self.send_command(0x06)
|
|
142
|
-
self.clear()
|
|
143
|
-
self._write_word(self.LCD_ADDR, 0x00)
|
|
144
|
-
|
|
145
|
-
def clear(self) -> None:
|
|
146
|
-
self.send_command(0x01)
|
|
147
|
-
time.sleep(0.002)
|
|
148
|
-
|
|
149
|
-
def reset(self) -> None:
|
|
150
|
-
"""Re-run the initialisation sequence to recover the display."""
|
|
151
|
-
self.init_lcd(addr=self.LCD_ADDR, bl=self.BLEN)
|
|
152
|
-
|
|
153
|
-
def set_backlight(
|
|
154
|
-
self, on: bool = True
|
|
155
|
-
) -> None: # pragma: no cover - hardware dependent
|
|
156
|
-
self.BLEN = 1 if on else 0
|
|
157
|
-
self._write_word(self.LCD_ADDR, 0x00)
|
|
158
|
-
|
|
159
|
-
def write(self, x: int, y: int, s: str) -> None:
|
|
160
|
-
x = max(0, min(15, int(x)))
|
|
161
|
-
y = 0 if int(y) <= 0 else 1
|
|
162
|
-
addr = 0x80 + 0x40 * y + x
|
|
163
|
-
self.send_command(addr)
|
|
164
|
-
for ch in str(s):
|
|
165
|
-
self.send_data(ord(ch))
|
|
1
|
+
"""Minimal driver for PCF8574/PCF8574A I2C LCD1602 displays.
|
|
2
|
+
|
|
3
|
+
The implementation is adapted from the example provided in the
|
|
4
|
+
instructions. It is intentionally lightweight and only implements the
|
|
5
|
+
operations required for this project: initialisation, clearing the
|
|
6
|
+
screen and writing text to a specific position.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
try: # pragma: no cover - hardware dependent
|
|
16
|
+
import smbus # type: ignore
|
|
17
|
+
except Exception: # pragma: no cover - missing dependency
|
|
18
|
+
try: # pragma: no cover - hardware dependent
|
|
19
|
+
import smbus2 as smbus # type: ignore
|
|
20
|
+
except Exception: # pragma: no cover - missing dependency
|
|
21
|
+
smbus = None # type: ignore
|
|
22
|
+
|
|
23
|
+
SMBUS_HINT = (
|
|
24
|
+
"smbus module not found. Enable the I2C interface and install the dependencies.\n"
|
|
25
|
+
"For Debian/Ubuntu run: sudo apt-get install i2c-tools python3-smbus\n"
|
|
26
|
+
"Within the virtualenv: pip install smbus2"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LCDUnavailableError(RuntimeError):
|
|
31
|
+
"""Raised when the LCD cannot be initialised."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class _BusWrapper:
|
|
36
|
+
"""Wrapper around :class:`smbus.SMBus` to allow mocking in tests."""
|
|
37
|
+
|
|
38
|
+
channel: int
|
|
39
|
+
|
|
40
|
+
def write_byte(
|
|
41
|
+
self, addr: int, data: int
|
|
42
|
+
) -> None: # pragma: no cover - thin wrapper
|
|
43
|
+
if smbus is None:
|
|
44
|
+
raise LCDUnavailableError(SMBUS_HINT)
|
|
45
|
+
bus = smbus.SMBus(self.channel)
|
|
46
|
+
bus.write_byte(addr, data)
|
|
47
|
+
bus.close()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CharLCD1602:
|
|
51
|
+
"""Minimal driver for PCF8574/PCF8574A I2C backpack (LCD1602)."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, bus: _BusWrapper | None = None) -> None:
|
|
54
|
+
if smbus is None: # pragma: no cover - hardware dependent
|
|
55
|
+
raise LCDUnavailableError(SMBUS_HINT)
|
|
56
|
+
self.bus = bus or _BusWrapper(1)
|
|
57
|
+
self.BLEN = 1
|
|
58
|
+
self.PCF8574_address = 0x27
|
|
59
|
+
self.PCF8574A_address = 0x3F
|
|
60
|
+
self.LCD_ADDR = self.PCF8574_address
|
|
61
|
+
|
|
62
|
+
def _write_word(self, addr: int, data: int) -> None:
|
|
63
|
+
if self.BLEN:
|
|
64
|
+
data |= 0x08
|
|
65
|
+
else:
|
|
66
|
+
data &= 0xF7
|
|
67
|
+
self.bus.write_byte(addr, data)
|
|
68
|
+
|
|
69
|
+
def _pulse_enable(self, data: int) -> None:
|
|
70
|
+
self._write_word(self.LCD_ADDR, data | 0x04)
|
|
71
|
+
time.sleep(0.0005)
|
|
72
|
+
self._write_word(self.LCD_ADDR, data & ~0x04)
|
|
73
|
+
time.sleep(0.0001)
|
|
74
|
+
|
|
75
|
+
def send_command(self, cmd: int) -> None:
|
|
76
|
+
high = cmd & 0xF0
|
|
77
|
+
low = (cmd << 4) & 0xF0
|
|
78
|
+
self._write_word(self.LCD_ADDR, high)
|
|
79
|
+
self._pulse_enable(high)
|
|
80
|
+
self._write_word(self.LCD_ADDR, low)
|
|
81
|
+
self._pulse_enable(low)
|
|
82
|
+
# Give the LCD time to process the command to avoid garbled output.
|
|
83
|
+
time.sleep(0.001)
|
|
84
|
+
|
|
85
|
+
def send_data(self, data: int) -> None:
|
|
86
|
+
high = (data & 0xF0) | 0x01
|
|
87
|
+
low = ((data << 4) & 0xF0) | 0x01
|
|
88
|
+
self._write_word(self.LCD_ADDR, high)
|
|
89
|
+
self._pulse_enable(high)
|
|
90
|
+
self._write_word(self.LCD_ADDR, low)
|
|
91
|
+
self._pulse_enable(low)
|
|
92
|
+
# Allow the LCD controller to catch up between data writes.
|
|
93
|
+
time.sleep(0.001)
|
|
94
|
+
|
|
95
|
+
def i2c_scan(self) -> list[str]: # pragma: no cover - requires hardware
|
|
96
|
+
"""Return a list of detected I2C addresses.
|
|
97
|
+
|
|
98
|
+
The implementation relies on the external ``i2cdetect`` command. On
|
|
99
|
+
systems where ``i2c-tools`` is not installed or the command cannot be
|
|
100
|
+
executed (e.g. insufficient permissions), the function returns an empty
|
|
101
|
+
list so callers can fall back to a sensible default address.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
output = subprocess.check_output(["i2cdetect", "-y", "1"], text=True)
|
|
106
|
+
except Exception: # pragma: no cover - depends on environment
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
addresses: list[str] = []
|
|
110
|
+
for line in output.splitlines()[1:]:
|
|
111
|
+
parts = line.split()
|
|
112
|
+
for token in parts[1:]:
|
|
113
|
+
if token != "--":
|
|
114
|
+
addresses.append(token)
|
|
115
|
+
return addresses
|
|
116
|
+
|
|
117
|
+
def init_lcd(self, addr: int | None = None, bl: int = 1) -> None:
|
|
118
|
+
self.BLEN = 1 if bl else 0
|
|
119
|
+
if addr is None:
|
|
120
|
+
try:
|
|
121
|
+
found = self.i2c_scan()
|
|
122
|
+
except Exception: # pragma: no cover - i2c detection issues
|
|
123
|
+
found = []
|
|
124
|
+
if "3f" in found or "3F" in found:
|
|
125
|
+
self.LCD_ADDR = self.PCF8574A_address
|
|
126
|
+
else:
|
|
127
|
+
# Default to the common PCF8574 address (0x27) when detection
|
|
128
|
+
# fails or returns no recognised addresses. This mirrors the
|
|
129
|
+
# behaviour prior to introducing automatic address detection and
|
|
130
|
+
# prevents the display from remaining uninitialised on systems
|
|
131
|
+
# without ``i2c-tools``.
|
|
132
|
+
self.LCD_ADDR = self.PCF8574_address
|
|
133
|
+
else:
|
|
134
|
+
self.LCD_ADDR = addr
|
|
135
|
+
|
|
136
|
+
time.sleep(0.05)
|
|
137
|
+
self.send_command(0x33)
|
|
138
|
+
self.send_command(0x32)
|
|
139
|
+
self.send_command(0x28)
|
|
140
|
+
self.send_command(0x0C)
|
|
141
|
+
self.send_command(0x06)
|
|
142
|
+
self.clear()
|
|
143
|
+
self._write_word(self.LCD_ADDR, 0x00)
|
|
144
|
+
|
|
145
|
+
def clear(self) -> None:
|
|
146
|
+
self.send_command(0x01)
|
|
147
|
+
time.sleep(0.002)
|
|
148
|
+
|
|
149
|
+
def reset(self) -> None:
|
|
150
|
+
"""Re-run the initialisation sequence to recover the display."""
|
|
151
|
+
self.init_lcd(addr=self.LCD_ADDR, bl=self.BLEN)
|
|
152
|
+
|
|
153
|
+
def set_backlight(
|
|
154
|
+
self, on: bool = True
|
|
155
|
+
) -> None: # pragma: no cover - hardware dependent
|
|
156
|
+
self.BLEN = 1 if on else 0
|
|
157
|
+
self._write_word(self.LCD_ADDR, 0x00)
|
|
158
|
+
|
|
159
|
+
def write(self, x: int, y: int, s: str) -> None:
|
|
160
|
+
x = max(0, min(15, int(x)))
|
|
161
|
+
y = 0 if int(y) <= 0 else 1
|
|
162
|
+
addr = 0x80 + 0x40 * y + x
|
|
163
|
+
self.send_command(addr)
|
|
164
|
+
for ch in str(s):
|
|
165
|
+
self.send_data(ord(ch))
|