Skip to content

Commit a5b86ed

Browse files
committed
feat(docs): add sphinx doc builder
Krux lack of a techinical documentation. This commit add `sphinx` module to generate a proper [readthedocs](https://readsthedocs.com/) documentation for Krux firmware (and not a user documentation) and a setup for install aditional deps, generation of rst files, build to html files and serve.
1 parent 97d1fb3 commit a5b86ed

13 files changed

Lines changed: 907 additions & 166 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,6 @@ krux-*/ktool*
8686

8787
# IDE files
8888
.vscode
89+
90+
# Readthedocs
91+
docs/_build/*

.readthedocs.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Read the Docs configuration file
2+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3+
4+
# Required
5+
version: 2
6+
7+
# Set the OS, Python version, and other tools you might need
8+
build:
9+
os: ubuntu-24.04
10+
tools:
11+
python: "3.13"
12+
13+
# Build documentation in the "docs/" directory with Sphinx
14+
sphinx:
15+
configuration: docs/conf.py
16+
17+
# Optionally, but recommended,
18+
# declare the Python requirements required to build your documentation
19+
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
20+
python:
21+
install:
22+
- requirements: docs/requirements.txt
23+
24+

docs/Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Minimal makefile for Sphinx documentation
2+
#
3+
4+
# You can set these variables from the command line, and also
5+
# from the environment for the first two.
6+
SPHINXOPTS ?=
7+
SPHINXBUILD ?= sphinx-build
8+
SOURCEDIR = source
9+
BUILDDIR = build
10+
11+
# Put it first so that "make" without argument is like "make help".
12+
help:
13+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14+
15+
.PHONY: help Makefile
16+
17+
# Catch-all target: route all unknown targets to Sphinx using the new
18+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19+
%: Makefile
20+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

docs/conf.py

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# Configuration file for the Sphinx documentation builder.
2+
#
3+
# For the full list of built-in configuration values, see the documentation:
4+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
5+
6+
import os
7+
import sys
8+
import time as _time
9+
from unittest.mock import MagicMock
10+
11+
"""
12+
`sphinx-*` tools used on `pyproject.toml` (specificaly, on `readthedocs` poe
13+
task) will execute the to bedocumented code before document it. But some of
14+
them are hardware specific (micropython selfcustody fork) and cannot be loaded.
15+
16+
A way to "skip" them is to mock the libraries that we do not want to execute
17+
(hardware specific, embit, thirdparty, etc). After that we can configure
18+
so `sphinx` could translate them to html.
19+
"""
20+
21+
22+
# -- Mock hacks----- ---------------------------------------------------------
23+
class MockDisplay:
24+
"""Mock display with proper return types."""
25+
26+
def height(self):
27+
return 320
28+
29+
def width(self):
30+
return 480
31+
32+
def draw_centered_text(self, *args, **kwargs):
33+
pass
34+
35+
def clear(self):
36+
pass
37+
38+
def fill_rectangle(self, *args, **kwargs):
39+
pass
40+
41+
def draw_hcentered_text(self, *args, **kwargs):
42+
pass
43+
44+
def draw_qr_code(self, *args, **kwargs):
45+
pass
46+
47+
def to_landscape(self):
48+
pass
49+
50+
def to_portrait(self):
51+
pass
52+
53+
def __getattr__(self, name):
54+
return MagicMock()
55+
56+
57+
class HashableMock:
58+
"""A simple hashable mock that can be used as dictionary keys."""
59+
60+
_counter = 0
61+
62+
def __init__(self, name="mock"):
63+
HashableMock._counter += 1
64+
self._name = name
65+
self._id = HashableMock._counter
66+
67+
def __hash__(self):
68+
return self._id
69+
70+
def __eq__(self, other):
71+
if isinstance(other, HashableMock):
72+
return self._id == other._id
73+
return False
74+
75+
def __repr__(self):
76+
return f"<HashableMock {self._name}>"
77+
78+
def __call__(self, *args, **kwargs):
79+
return HashableMock(self._name)
80+
81+
def __getattr__(self, name):
82+
return HashableMock(f"{self._name}.{name}")
83+
84+
def __int__(self):
85+
return 0
86+
87+
def __bool__(self):
88+
return False
89+
90+
91+
class MockModule(MagicMock):
92+
"""Mock module that returns HashableMock for attributes that might be used as dict keys."""
93+
94+
# Attributes known to be used as dict keys
95+
HASHABLE_ATTRS = {
96+
"MODE_AES",
97+
"MODE_CTR",
98+
"MODE_CBC",
99+
"MODE_ECB",
100+
"AES",
101+
"CTR",
102+
"CBC",
103+
"ECB",
104+
}
105+
106+
def __getattr__(self, name):
107+
if name in self.HASHABLE_ATTRS or name.startswith("MODE_"):
108+
return HashableMock(name)
109+
return MagicMock()
110+
111+
112+
class MockWordlist:
113+
"""Mock for embit.wordlists.bip39.WORDLIST - returns list of strings."""
114+
115+
WORDLIST = ["abandon"] * 2048 # Dummy wordlist
116+
117+
118+
class MockTime:
119+
"""Mock time module with MicroPython-specific functions."""
120+
121+
def __getattr__(self, name):
122+
if hasattr(_time, name):
123+
return getattr(_time, name)
124+
return MagicMock()
125+
126+
@staticmethod
127+
def ticks_ms():
128+
return 0
129+
130+
@staticmethod
131+
def ticks_us():
132+
return 0
133+
134+
@staticmethod
135+
def ticks_diff(a, b):
136+
return 0
137+
138+
@staticmethod
139+
def ticks_add(a, b):
140+
return 0
141+
142+
@staticmethod
143+
def sleep_ms(ms):
144+
pass
145+
146+
@staticmethod
147+
def sleep_us(us):
148+
pass
149+
150+
151+
# Create mock for board with required config structure
152+
class MockBoardConfig(dict):
153+
"""Mock board. config as a real dict."""
154+
155+
def __init__(self):
156+
super().__init__(
157+
{
158+
"type": "rtd",
159+
"lcd": {
160+
"height": 320,
161+
"width": 480,
162+
"invert": 0,
163+
"dir": 40,
164+
"lcd_type": 1,
165+
},
166+
"sdcard": {"sclk": 11, "mosi": 10, "miso": 6, "cs": 26},
167+
"board_info": {
168+
"BOOT_KEY": 23,
169+
"CONNEXT_A": 7,
170+
"CONNEXT_B": 9,
171+
"LED_R": 14,
172+
"LED_G": 15,
173+
"LED_B": 17,
174+
"LED_W": 32,
175+
"BACK": 23,
176+
"ENTER": 16,
177+
"NEXT": 20,
178+
"WIFI_TX": 6,
179+
"WIFI_RX": 7,
180+
"WIFI_EN": 8,
181+
"I2S0_MCLK": 13,
182+
"I2S0_SCLK": 21,
183+
"I2S0_WS": 18,
184+
"I2S0_IN_D0": 35,
185+
"I2S0_OUT_D2": 34,
186+
"I2C_SDA": 27,
187+
"I2C_SCL": 24,
188+
"SPI_SCLK": 11,
189+
"SPI_MOSI": 10,
190+
"SPI_MISO": 6,
191+
"SPI_CS": 12,
192+
},
193+
"krux": {
194+
"pins": {
195+
"BUTTON_A": 16,
196+
"BUTTON_B": 20,
197+
"BUTTON_C": 23,
198+
"TOUCH_IRQ": 33,
199+
"LED_W": 32,
200+
"BACK": 23,
201+
"ENTER": 16,
202+
"NEXT": 20,
203+
"WIFI_TX": 6,
204+
"WIFI_RX": 7,
205+
"WIFI_EN": 8,
206+
"I2C_SDA": 27,
207+
"I2C_SCL": 24,
208+
"ENCODER": [10, 11],
209+
},
210+
"display": {"touch": True, "font": [12, 24], "font_wide": [24, 24]},
211+
},
212+
}
213+
)
214+
215+
216+
class MockBoard:
217+
"""Mock board module."""
218+
219+
config = MockBoardConfig()
220+
221+
def __getattr__(self, name):
222+
return MagicMock()
223+
224+
225+
# Create mock instances
226+
MOCK_TIME = MockTime()
227+
MOCK_UCRYPTOLIB = MockModule()
228+
MOCK_UCRYPTOLIB.aes = MockModule()
229+
230+
# Mock for embit.wordlists.bip39
231+
MOCK_BIP39 = type("MockBip39", (), {"WORDLIST": ["abandon"] * 2048})()
232+
233+
234+
def setup_mocks():
235+
"""Setup all mocked modules."""
236+
237+
mock_display_module = MagicMock()
238+
mock_display_module.display = MockDisplay()
239+
mock_display_module.SPLASH = "Krux"
240+
mock_display_module.FONT_HEIGHT = 24
241+
mock_display_module.FONT_WIDTH = 12
242+
243+
mocks = {
244+
# MicroPython built-ins
245+
"machine": MagicMock(),
246+
"board": MockBoard(),
247+
"pmu": MagicMock(),
248+
"time": MockTime(),
249+
"lcd": MagicMock(),
250+
"ujson": MagicMock(),
251+
"urandom": MagicMock(),
252+
"ucryptolib": MockModule(),
253+
"uhashlib": MagicMock(),
254+
"uhashlib_hw": MagicMock(),
255+
"uctypes": MagicMock(),
256+
"uos": MagicMock(),
257+
"usys": MagicMock(),
258+
"gc": MagicMock(),
259+
# K210/Maix specific
260+
"Maix": MagicMock(),
261+
"fpioa_manager": MagicMock(),
262+
"sensor": MagicMock(),
263+
"image": MagicMock(),
264+
"flash": MagicMock(),
265+
# Crypto/QR
266+
"secp256k1": MagicMock(),
267+
"qrcode": MagicMock(),
268+
# Embit (Bitcoin library)
269+
"embit": MagicMock(),
270+
"embit.wordlists": MagicMock(),
271+
"embit.wordlists.bip39": MockWordlist(),
272+
"embit.psbt": MagicMock(),
273+
"embit.networks": MagicMock(),
274+
"embit.descriptor": MagicMock(),
275+
"embit.descriptor.descriptor": MagicMock(),
276+
"embit.descriptor.arguments": MagicMock(),
277+
# Scientific (if needed)
278+
"numpy": MagicMock(),
279+
"scipy": MagicMock(),
280+
"scipy.linalg": MagicMock(),
281+
"scipy.signal": MagicMock(),
282+
# Krux specific
283+
"krux.display": mock_display_module,
284+
}
285+
286+
for mod_name, mock_obj in mocks.items():
287+
sys.modules[mod_name] = mock_obj
288+
289+
290+
setup_mocks()
291+
292+
# Add source path
293+
sys.path.insert(0, os.path.abspath("../"))
294+
sys.path.insert(0, os.path.abspath("../src/"))
295+
296+
# -- Project information -----------------------------------------------------
297+
project = "Krux"
298+
copyright = "2022, MIT"
299+
author = "Selfcustody"
300+
release = "2022"
301+
302+
# -- General configuration ---------------------------------------------------
303+
extensions = [
304+
"sphinx.ext.autodoc",
305+
"sphinx.ext.coverage",
306+
"sphinx.ext.napoleon",
307+
"sphinx.ext.viewcode",
308+
]
309+
napoleon_google_docstring = False
310+
napoleon_use_param = False
311+
napoleon_use_ivar = True
312+
templates_path = ["_templates"]
313+
exclude_patterns = ["snippets", "_static", "Thumbs.db", ".DS_Store"]
314+
315+
# Suppress warnings
316+
suppress_warnings = ["autodoc", "autodoc.import_object"]
317+
318+
# -- Options for HTML output -------------------------------------------------
319+
html_theme = "sphinx_rtd_theme"
320+
html_static_path = []
321+
322+
323+
def skip_mock_members(app, what, name, obj, skip, options):
324+
"""Skip mock objects that can't be documented."""
325+
if "Mock" in str(type(obj)):
326+
return True
327+
return skip
328+
329+
330+
def setup(app):
331+
app.connect("autodoc-skip-member", skip_mock_members)

0 commit comments

Comments
 (0)