Skip to content

Commit 43e13ce

Browse files
committed
Add RTC Battery Charging
Previous impl. was setting a value for runtime config in hope that offspot-runtime would support it and enable the charger. While offspot-runtime may support it, given it is enabled in the firmware it would require a restart which would be unwanted in our provisioning scenario. This changes it it so charger is enabled from next boot. Other changes: - commented OffspotYAMLStep as it's now doing nothing - Moved boot order step last as this should only be done if all other steps succeeded. Fixes offspot/base-image#74
1 parent 7452d91 commit 43e13ce

4 files changed

Lines changed: 122 additions & 8 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
3+
from provisioner.constants import RTC_CHARGING_VOLTAGE
4+
from provisioner.context import Context
5+
from provisioner.provisioning.common import Step, StepResult
6+
from provisioner.utils.blk.misc import mount_to_temp, unmount
7+
from provisioner.utils.raspberry import BootConfig
8+
9+
context = Context.get()
10+
logger = context.logger
11+
12+
13+
class EnableChargerStep(Step):
14+
15+
ident: str = "charger"
16+
name: str = "Enable RTC Battery Charger"
17+
reports_progress: bool = False
18+
progress_interval_ms: int = 1000
19+
20+
def run(self, *, verbose: bool = False) -> StepResult:
21+
root_dev = self.environment.target_disk.path # /dev/nvme0n1
22+
boot_part_dev = root_dev.with_name(f"{root_dev.name}p1") # /dev/nvme0n1p1
23+
mountpoint = mount_to_temp(boot_part_dev, rw=True)
24+
try:
25+
config_path = mountpoint.joinpath("config.txt")
26+
config = BootConfig.parse(config_path.read_text())
27+
config.add_key(
28+
key="dtparam=rtc_bbat_vchg",
29+
value=str(int(RTC_CHARGING_VOLTAGE * 1000000)),
30+
cfilter="pi5",
31+
)
32+
config_path.write_text(config.serialize())
33+
os.sync()
34+
if verbose:
35+
logger.info(f"{config_path=}:\n{config_path.read_text()}")
36+
except Exception as exc:
37+
return StepResult(
38+
succeeded=False,
39+
error_text="Failed to enable RTC Battery charging",
40+
debug_text=str(exc),
41+
)
42+
finally:
43+
unmount(mountpoint)
44+
try:
45+
mountpoint.rmdir()
46+
except Exception:
47+
...
48+
return StepResult(succeeded=True)

src/provisioner/provisioning/manager.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
from provisioner.context import Context
55
from provisioner.provisioning.bootorder import BootOrderStep
6+
from provisioner.provisioning.charger import EnableChargerStep
67
from provisioner.provisioning.common import Environment, ImplementsProgress, Step
78
from provisioner.provisioning.docker import DockerStep
89
from provisioner.provisioning.hwclock import HardwareClockStep
910
from provisioner.provisioning.imager import PiImagerStep
10-
from provisioner.provisioning.offspotyaml import OffspotYAMLStep
1111
from provisioner.provisioning.resizepart import ResizePartitionStep
1212

1313
context = Context.get()
@@ -18,10 +18,11 @@ class ProvisionManager:
1818
STEPS: tuple[type[Step], ...] = (
1919
HardwareClockStep,
2020
PiImagerStep,
21-
BootOrderStep,
21+
EnableChargerStep,
2222
ResizePartitionStep,
23-
OffspotYAMLStep,
23+
# OffspotYAMLStep, # no update ATM
2424
DockerStep,
25+
BootOrderStep,
2526
)
2627

2728
def __init__(

src/provisioner/provisioning/offspotyaml.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22

3-
from provisioner.constants import RTC_CHARGING_VOLTAGE
43
from provisioner.context import Context
54
from provisioner.provisioning.common import Step, StepResult
65
from provisioner.utils.blk.misc import mount_to_temp, unmount
@@ -25,15 +24,18 @@ def run(self, *, verbose: bool = False) -> StepResult:
2524
offspot_yaml = mountpoint.joinpath("offspot.yaml")
2625
config = yaml_load(offspot_yaml.read_text())
2726

28-
# update
29-
# > set RTC charging voltage (to enable it)
30-
config["rtc"] = {"charger": int(RTC_CHARGING_VOLTAGE * 10000)}
31-
# country-update not required
27+
# ADD runtime update HERE
3228

3329
offspot_yaml.write_text(yaml_dump(config))
3430
os.sync()
3531
if verbose:
3632
logger.info(f"{offspot_yaml=}:\n{offspot_yaml.read_text()}")
33+
except Exception as exc:
34+
return StepResult(
35+
succeeded=False,
36+
error_text="Failed to enable RTC Battery charging",
37+
debug_text=str(exc),
38+
)
3739
finally:
3840
unmount(mountpoint)
3941
try:

src/provisioner/utils/raspberry.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import tempfile
44
from enum import StrEnum
55
from pathlib import Path
6+
from typing import Self
67

78
import attrs.validators
89
from attrs import define, field
@@ -110,3 +111,65 @@ def update_eeprom(*, verbose: bool = False):
110111
check=True,
111112
)
112113
config_path.unlink(missing_ok=True)
114+
115+
116+
class BootConfig(dict):
117+
"""Manager for content of /boot/firmware/config.txt firmware config file"""
118+
119+
@classmethod
120+
def parse(cls, text: str) -> Self:
121+
board_type_re = re.compile(r"^\[board-type=(?P<name>\d+)\]$")
122+
filter_re = re.compile(r"^\[(?P<name>[a-z0-9\+]+)\]$")
123+
data = {}
124+
currrent_filter = ""
125+
for line in text.splitlines():
126+
if m := board_type_re.match(line):
127+
currrent_filter = m.groupdict()["name"]
128+
elif m := filter_re.match(line):
129+
currrent_filter = m.groupdict()["name"]
130+
else:
131+
if currrent_filter not in data:
132+
data[currrent_filter] = []
133+
data[currrent_filter].append(line)
134+
return cls(data)
135+
136+
def remove_key(self, key: str, *, only_in: list[str] | None = None):
137+
"""remove line for this key in every filter or selected ones"""
138+
for fname, f_lines in self.items():
139+
if only_in and key not in only_in:
140+
continue
141+
for index, line in enumerate(f_lines):
142+
if line.startswith(f"{key}="):
143+
del self[fname][index]
144+
145+
def add_key(self, key: str, value: str, cfilter: str = "all"):
146+
if cfilter not in self:
147+
self[cfilter] = []
148+
self[cfilter].append(f"{key}={value}")
149+
150+
def serialize(self) -> str:
151+
# start with no filter ones (convewntion)
152+
lines: list[str] = []
153+
if "" in self:
154+
lines += self[""]
155+
156+
for f_name, f_lines in self.items():
157+
if f_name in ("", "all"):
158+
continue
159+
160+
# no need to fill empty filters
161+
if not f_lines:
162+
continue
163+
# add empty line before filter change is there was none
164+
if lines and lines[-1]:
165+
lines += [""]
166+
167+
lines += [f"[{f_name}]", *f_lines]
168+
169+
# end with the all ones (convention)
170+
if self.get("all", []):
171+
if lines and lines[-1]:
172+
lines += [""]
173+
lines += ["[all]", *self["all"]]
174+
175+
return "\n".join(lines)

0 commit comments

Comments
 (0)