bcmio 0.1.0__tar.gz

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.
bcmio-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: bcmio
3
+ Version: 0.1.0
4
+ Summary: Low-level GPIO library for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap
5
+ Author: bcmio contributors
6
+ License: MIT
7
+ Keywords: raspberrypi,gpio,bcm2711,mmap,embedded,linux
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: System :: Hardware
15
+ Classifier: Topic :: Software Development :: Embedded Systems
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: ruff>=0.5; extra == "dev"
21
+ Requires-Dist: mypy>=1.10; extra == "dev"
22
+
23
+ # bcmio
24
+
25
+ Biblioteca Python **low-level** para controle de GPIO do **Raspberry Pi 4 Model B (BCM2711)** usando
26
+ **acesso direto a registradores via `mmap`** e **`/dev/mem`** (MMIO).
27
+
28
+ > Aviso: acessar `/dev/mem` normalmente requer **root** (`sudo`). Use com cuidado: MMIO incorreto pode
29
+ > travar o sistema.
30
+
31
+ ## Objetivos
32
+
33
+ - Performance: escrita/leitura direto em registradores (`GPSET/GPCLR/GPLEV`)
34
+ - Arquitetura modular: separar low-level (MMIO/registradores) e high-level (abstrações)
35
+ - API familiar (inspirada em RPi.GPIO/pigpio/gpiozero), mas **sem dependências** dessas bibliotecas
36
+ - Código tipado, com docstrings, pronto para evoluir para PWM/SPI/I2C/UART/interrupts/DMA
37
+
38
+ ## Arquitetura (módulos)
39
+
40
+ - `bcmio/memory.py`: abertura de `/dev/mem`, `mmap`, leitura/escrita 32-bit
41
+ - `bcmio/constants.py`: offsets e constantes (GPIO modes, pulls, endereços base)
42
+ - `bcmio/gpio.py`: acesso aos registradores GPIO (FSEL, SET/CLR, LEV, PULL)
43
+ - `bcmio/pin.py`: classe `Pin` (alto nível) para um pino individual
44
+ - `bcmio/exceptions.py`: exceções customizadas
45
+ - `bcmio/utils.py`: helpers (validação, bit operations)
46
+ - `bcmio/pwm.py`, `bcmio/interrupts.py`: placeholders para evolução
47
+
48
+ ## Como `mmap` funciona (visão geral)
49
+
50
+ 1. Abrimos `/dev/mem` (arquivo especial que expõe memória física do SoC).
51
+ 2. Fazemos `mmap` de uma página (ou mais) a partir do **endereço físico** dos periféricos GPIO.
52
+ 3. A partir do ponteiro mapeado, fazemos leituras/escritas de 32 bits em offsets específicos.
53
+
54
+ No Linux, isso é MMIO (Memory Mapped I/O): escrever em um registrador mapeado altera o hardware.
55
+
56
+ ## Endereços base (BCM2711)
57
+
58
+ O datasheet usa endereços no **barramento** (bus) como `0x7E200000` para o bloco GPIO. No Raspberry Pi 4,
59
+ o endereço **físico** tipicamente é `0xFE200000` (peripheral base `0xFE000000` + GPIO offset `0x200000`).
60
+
61
+ Esta biblioteca:
62
+
63
+ - expõe ambos em `bcmio.constants` (`GPIO_BASE_BUS`, `GPIO_BASE_PHYS_DEFAULT`)
64
+ - por padrão usa o **físico** (`0xFE200000`)
65
+ - permite sobrescrever o endereço base via `GPIO.open(base_phys=...)`
66
+
67
+ ## Registradores GPIO usados (BCM2711)
68
+
69
+ Offsets relativos ao base do GPIO (bloco `GPIO`):
70
+
71
+ - `GPFSEL0..5` (Function Select): 3 bits por pino para definir `IN`, `OUT`, ou função alternativa
72
+ - `GPSET0..1` (Set): escrever `1` no bit seta o pino em nível alto (atômico)
73
+ - `GPCLR0..1` (Clear): escrever `1` no bit seta o pino em nível baixo (atômico)
74
+ - `GPLEV0..1` (Level): lê o nível atual do pino
75
+ - `GPIO_PUP_PDN_CNTRL_REG0..3`: 2 bits por pino para pull-up/pull-down/no-pull (BCM2711)
76
+
77
+ ## Uso
78
+
79
+ ### API estilo “módulo” (`GPIO`)
80
+
81
+ ```python
82
+ from bcmio import GPIO
83
+
84
+ GPIO.open() # inicializa /dev/mem + mmap
85
+ GPIO.setup(17, GPIO.OUT, pull=GPIO.PULL_NONE)
86
+ GPIO.write(17, GPIO.HIGH)
87
+ value = GPIO.read(17)
88
+ GPIO.cleanup()
89
+ ```
90
+
91
+ ### API orientada a objeto (`Pin`)
92
+
93
+ ```python
94
+ from bcmio import Pin
95
+
96
+ led = Pin(17, mode=Pin.OUT)
97
+ led.high()
98
+ led.low()
99
+ led.toggle()
100
+ led.close()
101
+ ```
102
+
103
+ ## Exemplos
104
+
105
+ Veja `exemplos/`:
106
+
107
+ - `exemplos/blink.py`
108
+ - `exemplos/button_read.py`
109
+ - `exemplos/toggle.py`
110
+ - `exemplos/read_digital.py`
111
+
112
+ ## Testes
113
+
114
+ Os testes em `testes/` não acessam `/dev/mem`. Eles usam um backend fake de memória para validar:
115
+
116
+ - cálculo de offsets e bitfields de `GPFSEL`
117
+ - `GPSET/GPCLR` e leitura em `GPLEV`
118
+ - configuração de pull em `GPIO_PUP_PDN_CNTRL_REGx`
119
+
120
+ Rodar:
121
+
122
+ ```bash
123
+ python -m pip install -e ".[dev]"
124
+ pytest -q
125
+ ```
126
+
127
+ ## Segurança e boas práticas
128
+
129
+ - Use `sudo` apenas quando necessário.
130
+ - Prefira isolar e revisar o endereço base antes de usar em produção.
131
+ - Em produção, considere um modo “safe” ou `/dev/gpiomem` (não implementado aqui por requisito).
132
+
bcmio-0.1.0/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # bcmio
2
+
3
+ Biblioteca Python **low-level** para controle de GPIO do **Raspberry Pi 4 Model B (BCM2711)** usando
4
+ **acesso direto a registradores via `mmap`** e **`/dev/mem`** (MMIO).
5
+
6
+ > Aviso: acessar `/dev/mem` normalmente requer **root** (`sudo`). Use com cuidado: MMIO incorreto pode
7
+ > travar o sistema.
8
+
9
+ ## Objetivos
10
+
11
+ - Performance: escrita/leitura direto em registradores (`GPSET/GPCLR/GPLEV`)
12
+ - Arquitetura modular: separar low-level (MMIO/registradores) e high-level (abstrações)
13
+ - API familiar (inspirada em RPi.GPIO/pigpio/gpiozero), mas **sem dependências** dessas bibliotecas
14
+ - Código tipado, com docstrings, pronto para evoluir para PWM/SPI/I2C/UART/interrupts/DMA
15
+
16
+ ## Arquitetura (módulos)
17
+
18
+ - `bcmio/memory.py`: abertura de `/dev/mem`, `mmap`, leitura/escrita 32-bit
19
+ - `bcmio/constants.py`: offsets e constantes (GPIO modes, pulls, endereços base)
20
+ - `bcmio/gpio.py`: acesso aos registradores GPIO (FSEL, SET/CLR, LEV, PULL)
21
+ - `bcmio/pin.py`: classe `Pin` (alto nível) para um pino individual
22
+ - `bcmio/exceptions.py`: exceções customizadas
23
+ - `bcmio/utils.py`: helpers (validação, bit operations)
24
+ - `bcmio/pwm.py`, `bcmio/interrupts.py`: placeholders para evolução
25
+
26
+ ## Como `mmap` funciona (visão geral)
27
+
28
+ 1. Abrimos `/dev/mem` (arquivo especial que expõe memória física do SoC).
29
+ 2. Fazemos `mmap` de uma página (ou mais) a partir do **endereço físico** dos periféricos GPIO.
30
+ 3. A partir do ponteiro mapeado, fazemos leituras/escritas de 32 bits em offsets específicos.
31
+
32
+ No Linux, isso é MMIO (Memory Mapped I/O): escrever em um registrador mapeado altera o hardware.
33
+
34
+ ## Endereços base (BCM2711)
35
+
36
+ O datasheet usa endereços no **barramento** (bus) como `0x7E200000` para o bloco GPIO. No Raspberry Pi 4,
37
+ o endereço **físico** tipicamente é `0xFE200000` (peripheral base `0xFE000000` + GPIO offset `0x200000`).
38
+
39
+ Esta biblioteca:
40
+
41
+ - expõe ambos em `bcmio.constants` (`GPIO_BASE_BUS`, `GPIO_BASE_PHYS_DEFAULT`)
42
+ - por padrão usa o **físico** (`0xFE200000`)
43
+ - permite sobrescrever o endereço base via `GPIO.open(base_phys=...)`
44
+
45
+ ## Registradores GPIO usados (BCM2711)
46
+
47
+ Offsets relativos ao base do GPIO (bloco `GPIO`):
48
+
49
+ - `GPFSEL0..5` (Function Select): 3 bits por pino para definir `IN`, `OUT`, ou função alternativa
50
+ - `GPSET0..1` (Set): escrever `1` no bit seta o pino em nível alto (atômico)
51
+ - `GPCLR0..1` (Clear): escrever `1` no bit seta o pino em nível baixo (atômico)
52
+ - `GPLEV0..1` (Level): lê o nível atual do pino
53
+ - `GPIO_PUP_PDN_CNTRL_REG0..3`: 2 bits por pino para pull-up/pull-down/no-pull (BCM2711)
54
+
55
+ ## Uso
56
+
57
+ ### API estilo “módulo” (`GPIO`)
58
+
59
+ ```python
60
+ from bcmio import GPIO
61
+
62
+ GPIO.open() # inicializa /dev/mem + mmap
63
+ GPIO.setup(17, GPIO.OUT, pull=GPIO.PULL_NONE)
64
+ GPIO.write(17, GPIO.HIGH)
65
+ value = GPIO.read(17)
66
+ GPIO.cleanup()
67
+ ```
68
+
69
+ ### API orientada a objeto (`Pin`)
70
+
71
+ ```python
72
+ from bcmio import Pin
73
+
74
+ led = Pin(17, mode=Pin.OUT)
75
+ led.high()
76
+ led.low()
77
+ led.toggle()
78
+ led.close()
79
+ ```
80
+
81
+ ## Exemplos
82
+
83
+ Veja `exemplos/`:
84
+
85
+ - `exemplos/blink.py`
86
+ - `exemplos/button_read.py`
87
+ - `exemplos/toggle.py`
88
+ - `exemplos/read_digital.py`
89
+
90
+ ## Testes
91
+
92
+ Os testes em `testes/` não acessam `/dev/mem`. Eles usam um backend fake de memória para validar:
93
+
94
+ - cálculo de offsets e bitfields de `GPFSEL`
95
+ - `GPSET/GPCLR` e leitura em `GPLEV`
96
+ - configuração de pull em `GPIO_PUP_PDN_CNTRL_REGx`
97
+
98
+ Rodar:
99
+
100
+ ```bash
101
+ python -m pip install -e ".[dev]"
102
+ pytest -q
103
+ ```
104
+
105
+ ## Segurança e boas práticas
106
+
107
+ - Use `sudo` apenas quando necessário.
108
+ - Prefira isolar e revisar o endereço base antes de usar em produção.
109
+ - Em produção, considere um modo “safe” ou `/dev/gpiomem` (não implementado aqui por requisito).
110
+
@@ -0,0 +1,16 @@
1
+ """
2
+ `bcmio` - GPIO low-level for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap.
3
+
4
+ Public API:
5
+ - GPIO: module-style API (class with classmethods)
6
+ - Pin: high-level per-pin abstraction
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .gpio import GPIO
12
+ from .pin import Pin
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = ["GPIO", "Pin", "__version__"]
@@ -0,0 +1,93 @@
1
+ """
2
+ Constants and register offsets for BCM2711 GPIO.
3
+
4
+ Notes about addresses:
5
+ - Datasheets often describe GPIO bus address as 0x7E200000.
6
+ - On Raspberry Pi 4 (BCM2711), physical peripheral base is typically 0xFE000000.
7
+ So GPIO physical base is typically 0xFE200000.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ # --- Base addresses ---
15
+
16
+ # GPIO base address as shown in the peripherals documentation (bus address)
17
+ GPIO_BASE_BUS: int = 0x7E200000
18
+
19
+ # Default physical base address for Raspberry Pi 4 (BCM2711).
20
+ # This is the typical address used with /dev/mem mappings.
21
+ GPIO_BASE_PHYS_DEFAULT: int = 0xFE200000
22
+
23
+ # --- Register offsets (relative to GPIO base) ---
24
+
25
+ GPFSEL0: int = 0x00
26
+ GPFSEL1: int = 0x04
27
+ GPFSEL2: int = 0x08
28
+ GPFSEL3: int = 0x0C
29
+ GPFSEL4: int = 0x10
30
+ GPFSEL5: int = 0x14
31
+
32
+ GPSET0: int = 0x1C
33
+ GPSET1: int = 0x20
34
+
35
+ GPCLR0: int = 0x28
36
+ GPCLR1: int = 0x2C
37
+
38
+ GPLEV0: int = 0x34
39
+ GPLEV1: int = 0x38
40
+
41
+ # BCM2711 pull-up/down control registers (2 bits per GPIO)
42
+ GPIO_PUP_PDN_CNTRL_REG0: int = 0xE4
43
+ GPIO_PUP_PDN_CNTRL_REG1: int = 0xE8
44
+ GPIO_PUP_PDN_CNTRL_REG2: int = 0xEC
45
+ GPIO_PUP_PDN_CNTRL_REG3: int = 0xF0
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class PinMode:
50
+ """GPIO function select values (3-bit fields in GPFSEL registers)."""
51
+
52
+ IN: int = 0b000
53
+ OUT: int = 0b001
54
+
55
+ # Alternate function values (kept for extensibility)
56
+ ALT0: int = 0b100
57
+ ALT1: int = 0b101
58
+ ALT2: int = 0b110
59
+ ALT3: int = 0b111
60
+ ALT4: int = 0b011
61
+ ALT5: int = 0b010
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class LogicLevel:
66
+ """Logical levels."""
67
+
68
+ LOW: int = 0
69
+ HIGH: int = 1
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class Pull:
74
+ """
75
+ Pull configuration values (2-bit fields in GPIO_PUP_PDN_CNTRL_REGx).
76
+
77
+ BCM2711 encoding (per pin):
78
+ 00 = No resistor
79
+ 01 = Pull-up
80
+ 10 = Pull-down
81
+ 11 = Reserved
82
+ """
83
+
84
+ NONE: int = 0b00
85
+ UP: int = 0b01
86
+ DOWN: int = 0b10
87
+
88
+
89
+ GPIO_MIN: int = 0
90
+ GPIO_MAX: int = 53 # BCM2711 exposes GPIO0..GPIO53 (some are not on header).
91
+
92
+ REGISTER_SIZE_BYTES: int = 4
93
+
@@ -0,0 +1,28 @@
1
+ """Custom exceptions for bcmio."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class BCMIOError(RuntimeError):
7
+ """Base error for bcmio."""
8
+
9
+
10
+ class PermissionDeniedError(BCMIOError):
11
+ """Raised when /dev/mem cannot be opened due to insufficient permissions."""
12
+
13
+
14
+ class DeviceNotFoundError(BCMIOError):
15
+ """Raised when expected device file is missing (e.g. /dev/mem)."""
16
+
17
+
18
+ class NotInitializedError(BCMIOError):
19
+ """Raised when GPIO is used before initialization/open()."""
20
+
21
+
22
+ class InvalidPinError(ValueError, BCMIOError):
23
+ """Raised when a GPIO pin number is invalid."""
24
+
25
+
26
+ class MMIOMapError(BCMIOError):
27
+ """Raised when mmap fails or MMIO access cannot be established."""
28
+
@@ -0,0 +1,231 @@
1
+ """
2
+ GPIO register access for BCM2711.
3
+
4
+ Implements:
5
+ - function select (input/output)
6
+ - atomic set/clear
7
+ - level read
8
+ - pull-up/pull-down configuration via GPIO_PUP_PDN_CNTRL_REG0..3 (BCM2711)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from dataclasses import dataclass
15
+ from threading import RLock
16
+ from typing import Optional
17
+
18
+ from .constants import (
19
+ GPIO_BASE_PHYS_DEFAULT,
20
+ GPIO_MAX,
21
+ GPFSEL0,
22
+ GPCLR0,
23
+ GPLEV0,
24
+ GPSET0,
25
+ GPIO_PUP_PDN_CNTRL_REG0,
26
+ LogicLevel,
27
+ PinMode,
28
+ Pull,
29
+ )
30
+ from .exceptions import NotInitializedError
31
+ from .memory import FakeMMIORegion, MemoryBackend, open_gpio_region
32
+ from .utils import bit_mask, replace_field, validate_gpio_pin
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ GPIO_BLOCK_SIZE: int = 0x1000 # enough to cover up to 0xF0 registers
38
+
39
+
40
+ def _fsel_reg_and_shift(pin: int) -> tuple[int, int]:
41
+ # Each GPFSEL register controls 10 pins, 3 bits per pin.
42
+ reg_index = pin // 10 # 0..5
43
+ shift = (pin % 10) * 3
44
+ offset = GPFSEL0 + (reg_index * 4)
45
+ return offset, shift
46
+
47
+
48
+ def _setclr_reg_and_bit(pin: int, base_offset: int) -> tuple[int, int]:
49
+ # GPSET0/GPCLR0 cover pins 0..31, GPSET1/GPCLR1 cover 32..53.
50
+ reg_index = pin // 32 # 0 or 1
51
+ bit = pin % 32
52
+ offset = base_offset + (reg_index * 4)
53
+ return offset, bit
54
+
55
+
56
+ def _lev_reg_and_bit(pin: int) -> tuple[int, int]:
57
+ reg_index = pin // 32
58
+ bit = pin % 32
59
+ offset = GPLEV0 + (reg_index * 4)
60
+ return offset, bit
61
+
62
+
63
+ def _pull_reg_and_shift(pin: int) -> tuple[int, int]:
64
+ # BCM2711: 2 bits per pin, 16 pins per 32-bit register.
65
+ reg_index = pin // 16 # 0..3
66
+ shift = (pin % 16) * 2
67
+ offset = GPIO_PUP_PDN_CNTRL_REG0 + (reg_index * 4)
68
+ return offset, shift
69
+
70
+
71
+ @dataclass(slots=True)
72
+ class GPIOConfig:
73
+ base_phys: int = GPIO_BASE_PHYS_DEFAULT
74
+ devmem_path: str = "/dev/mem"
75
+
76
+
77
+ class GPIO:
78
+ """
79
+ Module-style API for GPIO access.
80
+
81
+ This class manages a single global MMIO mapping (per-process) for simplicity.
82
+ """
83
+
84
+ IN: int = PinMode().IN
85
+ OUT: int = PinMode().OUT
86
+
87
+ LOW: int = LogicLevel().LOW
88
+ HIGH: int = LogicLevel().HIGH
89
+
90
+ PULL_NONE: int = Pull().NONE
91
+ PULL_UP: int = Pull().UP
92
+ PULL_DOWN: int = Pull().DOWN
93
+
94
+ _lock = RLock()
95
+ _region: Optional[MemoryBackend] = None
96
+ _config: GPIOConfig = GPIOConfig()
97
+
98
+ @classmethod
99
+ def open(
100
+ cls,
101
+ *,
102
+ base_phys: int = GPIO_BASE_PHYS_DEFAULT,
103
+ devmem_path: str = "/dev/mem",
104
+ backend: Optional[MemoryBackend] = None,
105
+ ) -> None:
106
+ """
107
+ Initialize MMIO mapping for GPIO.
108
+
109
+ Pass `backend` only for tests (e.g. FakeMMIORegion).
110
+ """
111
+ with cls._lock:
112
+ if cls._region is not None:
113
+ return
114
+ cls._config = GPIOConfig(base_phys=base_phys, devmem_path=devmem_path)
115
+ if backend is None:
116
+ cls._region = open_gpio_region(base_phys=base_phys, size=GPIO_BLOCK_SIZE, devmem_path=devmem_path)
117
+ else:
118
+ cls._region = open_gpio_region(base_phys=base_phys, size=GPIO_BLOCK_SIZE, backend=backend)
119
+ logger.info("GPIO MMIO opened at phys=0x%X (%s)", base_phys, devmem_path)
120
+
121
+ @classmethod
122
+ def is_open(cls) -> bool:
123
+ return cls._region is not None
124
+
125
+ @classmethod
126
+ def _require_open(cls) -> MemoryBackend:
127
+ if cls._region is None:
128
+ raise NotInitializedError("GPIO not initialized. Call GPIO.open() first.")
129
+ return cls._region
130
+
131
+ @classmethod
132
+ def close(cls) -> None:
133
+ """Unmap and close the MMIO region."""
134
+ with cls._lock:
135
+ if cls._region is None:
136
+ return
137
+ try:
138
+ cls._region.close()
139
+ finally:
140
+ cls._region = None
141
+ logger.info("GPIO MMIO closed")
142
+
143
+ @classmethod
144
+ def cleanup(cls) -> None:
145
+ """Alias for close()."""
146
+ cls.close()
147
+
148
+ @classmethod
149
+ def setup(cls, pin: int, mode: int, *, pull: int = Pull().NONE) -> None:
150
+ """Configure pin function (IN/OUT) and pull resistor."""
151
+ validate_gpio_pin(pin)
152
+ if mode not in (cls.IN, cls.OUT):
153
+ raise ValueError(f"Invalid mode: {mode}")
154
+ if pull not in (cls.PULL_NONE, cls.PULL_UP, cls.PULL_DOWN):
155
+ raise ValueError(f"Invalid pull: {pull}")
156
+
157
+ with cls._lock:
158
+ region = cls._require_open()
159
+ fsel_off, shift = _fsel_reg_and_shift(pin)
160
+ cur = region.read32(fsel_off)
161
+ cur = replace_field(cur, field_mask=0b111, field_value=mode, shift=shift)
162
+ region.write32(fsel_off, cur)
163
+
164
+ # Pull is independent of mode on BCM2711
165
+ cls._set_pull_locked(region, pin, pull)
166
+
167
+ @classmethod
168
+ def _set_pull_locked(cls, region: MemoryBackend, pin: int, pull: int) -> None:
169
+ off, shift = _pull_reg_and_shift(pin)
170
+ cur = region.read32(off)
171
+ cur = replace_field(cur, field_mask=0b11, field_value=pull, shift=shift)
172
+ region.write32(off, cur)
173
+
174
+ @classmethod
175
+ def set_pull(cls, pin: int, pull: int) -> None:
176
+ """Configure pull resistor for a pin (PULL_NONE, PULL_UP, PULL_DOWN)."""
177
+ validate_gpio_pin(pin)
178
+ if pull not in (cls.PULL_NONE, cls.PULL_UP, cls.PULL_DOWN):
179
+ raise ValueError(f"Invalid pull: {pull}")
180
+ with cls._lock:
181
+ region = cls._require_open()
182
+ cls._set_pull_locked(region, pin, pull)
183
+
184
+ @classmethod
185
+ def write(cls, pin: int, value: int) -> None:
186
+ """Write a logical level using atomic GPSET/GPCLR registers."""
187
+ validate_gpio_pin(pin)
188
+ if value not in (cls.LOW, cls.HIGH):
189
+ raise ValueError(f"Invalid value: {value}")
190
+
191
+ with cls._lock:
192
+ region = cls._require_open()
193
+ if value == cls.HIGH:
194
+ off, bit = _setclr_reg_and_bit(pin, GPSET0)
195
+ else:
196
+ off, bit = _setclr_reg_and_bit(pin, GPCLR0)
197
+ region.write32(off, bit_mask(bit))
198
+
199
+ @classmethod
200
+ def high(cls, pin: int) -> None:
201
+ cls.write(pin, cls.HIGH)
202
+
203
+ @classmethod
204
+ def low(cls, pin: int) -> None:
205
+ cls.write(pin, cls.LOW)
206
+
207
+ @classmethod
208
+ def read(cls, pin: int) -> int:
209
+ """Read the pin level from GPLEV registers."""
210
+ validate_gpio_pin(pin)
211
+ with cls._lock:
212
+ region = cls._require_open()
213
+ off, bit = _lev_reg_and_bit(pin)
214
+ level = region.read32(off)
215
+ return cls.HIGH if (level & bit_mask(bit)) else cls.LOW
216
+
217
+ @classmethod
218
+ def toggle(cls, pin: int) -> int:
219
+ """Toggle pin output based on current level. Returns new level."""
220
+ current = cls.read(pin)
221
+ new = cls.LOW if current == cls.HIGH else cls.HIGH
222
+ cls.write(pin, new)
223
+ return new
224
+
225
+ # --- Convenience for tests ---
226
+ @classmethod
227
+ def _open_fake(cls) -> FakeMMIORegion:
228
+ fake = FakeMMIORegion(GPIO_BLOCK_SIZE)
229
+ cls.open(backend=fake)
230
+ return fake
231
+
@@ -0,0 +1,9 @@
1
+ """Interrupts placeholder (future work)."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class Interrupts: # pragma: no cover
7
+ def __init__(self, *args: object, **kwargs: object) -> None:
8
+ raise NotImplementedError("Interrupts not implemented yet. Planned module placeholder.")
9
+
@@ -0,0 +1,208 @@
1
+ """
2
+ Memory-mapped I/O helpers for BCM2711 peripherals via /dev/mem.
3
+
4
+ This module intentionally focuses on:
5
+ - opening /dev/mem
6
+ - mapping a physical address range with mmap
7
+ - reading/writing 32-bit little-endian registers
8
+
9
+ It is kept separate from GPIO logic to enable future modules (PWM/SPI/I2C/UART/DMA).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import mmap
16
+ import os
17
+ import struct
18
+ from dataclasses import dataclass
19
+ from typing import Optional
20
+
21
+ from .exceptions import DeviceNotFoundError, MMIOMapError, PermissionDeniedError
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _page_align(addr: int, page_size: int) -> tuple[int, int]:
27
+ base = addr - (addr % page_size)
28
+ offset = addr - base
29
+ return base, offset
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class MMIORegion:
34
+ """
35
+ Represents a mapped MMIO region.
36
+
37
+ `base_phys` is the physical base address requested (not page-aligned).
38
+ """
39
+
40
+ base_phys: int
41
+ size: int
42
+ _fd: int
43
+ _mmap: mmap.mmap
44
+ _offset: int
45
+
46
+ def close(self) -> None:
47
+ """Unmap and close the underlying /dev/mem file descriptor."""
48
+ try:
49
+ self._mmap.close()
50
+ finally:
51
+ os.close(self._fd)
52
+
53
+ def read32(self, offset: int) -> int:
54
+ """Read a 32-bit little-endian value at `offset` within the region."""
55
+ if offset < 0 or offset + 4 > self.size:
56
+ raise MMIOMapError(f"read32 offset out of range: 0x{offset:X}")
57
+ pos = self._offset + offset
58
+ data = self._mmap[pos : pos + 4]
59
+ return struct.unpack_from("<I", data)[0]
60
+
61
+ def write32(self, offset: int, value: int) -> None:
62
+ """Write a 32-bit little-endian value at `offset` within the region."""
63
+ if offset < 0 or offset + 4 > self.size:
64
+ raise MMIOMapError(f"write32 offset out of range: 0x{offset:X}")
65
+ pos = self._offset + offset
66
+ self._mmap[pos : pos + 4] = struct.pack("<I", value & 0xFFFFFFFF)
67
+
68
+
69
+ class MemoryMapper:
70
+ """
71
+ Maps physical peripheral registers through /dev/mem.
72
+
73
+ Typical usage:
74
+ mapper = MemoryMapper()
75
+ region = mapper.map_region(0xFE200000, 0x1000) # GPIO block
76
+ """
77
+
78
+ def __init__(self, devmem_path: str = "/dev/mem") -> None:
79
+ self._devmem_path = devmem_path
80
+
81
+ def map_region(self, base_phys: int, size: int) -> MMIORegion:
82
+ """
83
+ Map a physical address range.
84
+
85
+ `size` should include all needed registers; it will be rounded up to page size.
86
+ """
87
+ if size <= 0:
88
+ raise ValueError("size must be > 0")
89
+
90
+ page_size = mmap.PAGESIZE
91
+ aligned_base, page_offset = _page_align(base_phys, page_size)
92
+ mapped_size = ((page_offset + size + page_size - 1) // page_size) * page_size
93
+
94
+ try:
95
+ fd = os.open(self._devmem_path, os.O_RDWR | os.O_SYNC)
96
+ except FileNotFoundError as e:
97
+ raise DeviceNotFoundError(f"Device not found: {self._devmem_path}") from e
98
+ except PermissionError as e:
99
+ raise PermissionDeniedError(
100
+ f"Permission denied opening {self._devmem_path}. Try running as root (sudo)."
101
+ ) from e
102
+ except OSError as e:
103
+ raise MMIOMapError(f"Failed opening {self._devmem_path}: {e}") from e
104
+
105
+ try:
106
+ mm = mmap.mmap(
107
+ fd,
108
+ mapped_size,
109
+ flags=mmap.MAP_SHARED,
110
+ prot=mmap.PROT_READ | mmap.PROT_WRITE,
111
+ offset=aligned_base,
112
+ )
113
+ except OSError as e:
114
+ os.close(fd)
115
+ raise MMIOMapError(
116
+ f"mmap failed for phys=0x{base_phys:X} aligned=0x{aligned_base:X} size=0x{mapped_size:X}: {e}"
117
+ ) from e
118
+
119
+ logger.debug(
120
+ "Mapped %s: phys=0x%X aligned=0x%X size=0x%X offset=0x%X",
121
+ self._devmem_path,
122
+ base_phys,
123
+ aligned_base,
124
+ mapped_size,
125
+ page_offset,
126
+ )
127
+ return MMIORegion(base_phys=base_phys, size=size, _fd=fd, _mmap=mm, _offset=page_offset)
128
+
129
+
130
+ class FakeMMIORegion:
131
+ """
132
+ Test helper: in-memory fake MMIO region implementing read32/write32.
133
+
134
+ This is intentionally simple and NOT thread-safe.
135
+ """
136
+
137
+ def __init__(self, size: int) -> None:
138
+ self.size = size
139
+ self._buf = bytearray(size)
140
+
141
+ def close(self) -> None: # pragma: no cover
142
+ return
143
+
144
+ def read32(self, offset: int) -> int:
145
+ if offset < 0 or offset + 4 > self.size:
146
+ raise MMIOMapError(f"read32 offset out of range: 0x{offset:X}")
147
+ return struct.unpack_from("<I", self._buf, offset)[0]
148
+
149
+ def write32(self, offset: int, value: int) -> None:
150
+ if offset < 0 or offset + 4 > self.size:
151
+ raise MMIOMapError(f"write32 offset out of range: 0x{offset:X}")
152
+ struct.pack_into("<I", self._buf, offset, value & 0xFFFFFFFF)
153
+
154
+
155
+ class MemoryBackend:
156
+ """
157
+ Protocol-like base for memory backends.
158
+
159
+ Any backend must implement:
160
+ - read32(offset) -> int
161
+ - write32(offset, value) -> None
162
+ - close() -> None
163
+ """
164
+
165
+ def read32(self, offset: int) -> int: # pragma: no cover
166
+ raise NotImplementedError
167
+
168
+ def write32(self, offset: int, value: int) -> None: # pragma: no cover
169
+ raise NotImplementedError
170
+
171
+ def close(self) -> None: # pragma: no cover
172
+ raise NotImplementedError
173
+
174
+
175
+ class GPIORegion(MemoryBackend):
176
+ """
177
+ A small wrapper to hold the actual MMIORegion (or FakeMMIORegion).
178
+ """
179
+
180
+ def __init__(self, region: MemoryBackend) -> None:
181
+ self._region = region
182
+
183
+ def read32(self, offset: int) -> int:
184
+ return self._region.read32(offset)
185
+
186
+ def write32(self, offset: int, value: int) -> None:
187
+ self._region.write32(offset, value)
188
+
189
+ def close(self) -> None:
190
+ self._region.close()
191
+
192
+
193
+ def open_gpio_region(
194
+ *,
195
+ base_phys: int,
196
+ size: int,
197
+ devmem_path: str = "/dev/mem",
198
+ backend: Optional[MemoryBackend] = None,
199
+ ) -> GPIORegion:
200
+ """
201
+ Factory to open the GPIO MMIO region.
202
+
203
+ If `backend` is provided, it will be used (useful for tests).
204
+ """
205
+ if backend is not None:
206
+ return GPIORegion(backend)
207
+ mapper = MemoryMapper(devmem_path=devmem_path)
208
+ return GPIORegion(mapper.map_region(base_phys=base_phys, size=size))
@@ -0,0 +1,71 @@
1
+ """High-level Pin abstraction built on top of bcmio.gpio.GPIO."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Optional
7
+
8
+ from .gpio import GPIO
9
+ from .utils import validate_gpio_pin
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class Pin:
15
+ """
16
+ High-level representation of a single GPIO pin.
17
+
18
+ This class uses the global `GPIO` mapping under the hood for performance.
19
+ """
20
+
21
+ IN: int = GPIO.IN
22
+ OUT: int = GPIO.OUT
23
+ LOW: int = GPIO.LOW
24
+ HIGH: int = GPIO.HIGH
25
+ PULL_NONE: int = GPIO.PULL_NONE
26
+ PULL_UP: int = GPIO.PULL_UP
27
+ PULL_DOWN: int = GPIO.PULL_DOWN
28
+
29
+ def __init__(
30
+ self,
31
+ pin: int,
32
+ *,
33
+ mode: int = OUT,
34
+ pull: int = PULL_NONE,
35
+ initial: Optional[int] = None,
36
+ auto_open: bool = True,
37
+ ) -> None:
38
+ validate_gpio_pin(pin)
39
+ self.pin = pin
40
+ self.mode = mode
41
+ self.pull = pull
42
+ self.initial = initial
43
+ self.auto_open = auto_open
44
+
45
+ if self.auto_open and not GPIO.is_open():
46
+ GPIO.open()
47
+ GPIO.setup(self.pin, self.mode, pull=self.pull)
48
+ if self.initial is not None:
49
+ GPIO.write(self.pin, self.initial)
50
+
51
+ def read(self) -> int:
52
+ return GPIO.read(self.pin)
53
+
54
+ def write(self, value: int) -> None:
55
+ GPIO.write(self.pin, value)
56
+
57
+ def high(self) -> None:
58
+ GPIO.high(self.pin)
59
+
60
+ def low(self) -> None:
61
+ GPIO.low(self.pin)
62
+
63
+ def toggle(self) -> int:
64
+ return GPIO.toggle(self.pin)
65
+
66
+ def set_pull(self, pull: int) -> None:
67
+ GPIO.set_pull(self.pin, pull)
68
+
69
+ def close(self) -> None:
70
+ """Close is intentionally a no-op for now (shared GPIO mapping)."""
71
+ logger.debug("Pin.close() called for GPIO%d (shared mapping left open)", self.pin)
@@ -0,0 +1,9 @@
1
+ """PWM placeholder (future work)."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class PWM: # pragma: no cover
7
+ def __init__(self, *args: object, **kwargs: object) -> None:
8
+ raise NotImplementedError("PWM not implemented yet. Planned module placeholder.")
9
+
@@ -0,0 +1,35 @@
1
+ """Utility helpers (validation, bit operations)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .constants import GPIO_MAX, GPIO_MIN
6
+ from .exceptions import InvalidPinError
7
+
8
+
9
+ def validate_gpio_pin(pin: int) -> None:
10
+ """Validate that `pin` is within the BCM2711 GPIO range."""
11
+ if not isinstance(pin, int):
12
+ raise InvalidPinError(f"GPIO pin must be int, got {type(pin).__name__}")
13
+ if pin < GPIO_MIN or pin > GPIO_MAX:
14
+ raise InvalidPinError(f"Invalid GPIO pin: {pin} (valid range {GPIO_MIN}..{GPIO_MAX})")
15
+
16
+
17
+ def bit_mask(bit: int) -> int:
18
+ """Return a 32-bit mask with a single bit set."""
19
+ return 1 << bit
20
+
21
+
22
+ def set_bits(value: int, mask: int) -> int:
23
+ return value | mask
24
+
25
+
26
+ def clear_bits(value: int, mask: int) -> int:
27
+ return value & ~mask
28
+
29
+
30
+ def replace_field(value: int, field_mask: int, field_value: int, shift: int) -> int:
31
+ """Replace a bitfield defined by mask+shift with a new value."""
32
+ value &= ~(field_mask << shift)
33
+ value |= (field_value & field_mask) << shift
34
+ return value
35
+
@@ -0,0 +1,14 @@
1
+ """Basic logging setup helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+
8
+ def configure_logging(level: int = logging.INFO) -> None:
9
+ """Configure a basic root logger formatting."""
10
+ logging.basicConfig(
11
+ level=level,
12
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
13
+ )
14
+
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: bcmio
3
+ Version: 0.1.0
4
+ Summary: Low-level GPIO library for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap
5
+ Author: bcmio contributors
6
+ License: MIT
7
+ Keywords: raspberrypi,gpio,bcm2711,mmap,embedded,linux
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: System :: Hardware
15
+ Classifier: Topic :: Software Development :: Embedded Systems
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: ruff>=0.5; extra == "dev"
21
+ Requires-Dist: mypy>=1.10; extra == "dev"
22
+
23
+ # bcmio
24
+
25
+ Biblioteca Python **low-level** para controle de GPIO do **Raspberry Pi 4 Model B (BCM2711)** usando
26
+ **acesso direto a registradores via `mmap`** e **`/dev/mem`** (MMIO).
27
+
28
+ > Aviso: acessar `/dev/mem` normalmente requer **root** (`sudo`). Use com cuidado: MMIO incorreto pode
29
+ > travar o sistema.
30
+
31
+ ## Objetivos
32
+
33
+ - Performance: escrita/leitura direto em registradores (`GPSET/GPCLR/GPLEV`)
34
+ - Arquitetura modular: separar low-level (MMIO/registradores) e high-level (abstrações)
35
+ - API familiar (inspirada em RPi.GPIO/pigpio/gpiozero), mas **sem dependências** dessas bibliotecas
36
+ - Código tipado, com docstrings, pronto para evoluir para PWM/SPI/I2C/UART/interrupts/DMA
37
+
38
+ ## Arquitetura (módulos)
39
+
40
+ - `bcmio/memory.py`: abertura de `/dev/mem`, `mmap`, leitura/escrita 32-bit
41
+ - `bcmio/constants.py`: offsets e constantes (GPIO modes, pulls, endereços base)
42
+ - `bcmio/gpio.py`: acesso aos registradores GPIO (FSEL, SET/CLR, LEV, PULL)
43
+ - `bcmio/pin.py`: classe `Pin` (alto nível) para um pino individual
44
+ - `bcmio/exceptions.py`: exceções customizadas
45
+ - `bcmio/utils.py`: helpers (validação, bit operations)
46
+ - `bcmio/pwm.py`, `bcmio/interrupts.py`: placeholders para evolução
47
+
48
+ ## Como `mmap` funciona (visão geral)
49
+
50
+ 1. Abrimos `/dev/mem` (arquivo especial que expõe memória física do SoC).
51
+ 2. Fazemos `mmap` de uma página (ou mais) a partir do **endereço físico** dos periféricos GPIO.
52
+ 3. A partir do ponteiro mapeado, fazemos leituras/escritas de 32 bits em offsets específicos.
53
+
54
+ No Linux, isso é MMIO (Memory Mapped I/O): escrever em um registrador mapeado altera o hardware.
55
+
56
+ ## Endereços base (BCM2711)
57
+
58
+ O datasheet usa endereços no **barramento** (bus) como `0x7E200000` para o bloco GPIO. No Raspberry Pi 4,
59
+ o endereço **físico** tipicamente é `0xFE200000` (peripheral base `0xFE000000` + GPIO offset `0x200000`).
60
+
61
+ Esta biblioteca:
62
+
63
+ - expõe ambos em `bcmio.constants` (`GPIO_BASE_BUS`, `GPIO_BASE_PHYS_DEFAULT`)
64
+ - por padrão usa o **físico** (`0xFE200000`)
65
+ - permite sobrescrever o endereço base via `GPIO.open(base_phys=...)`
66
+
67
+ ## Registradores GPIO usados (BCM2711)
68
+
69
+ Offsets relativos ao base do GPIO (bloco `GPIO`):
70
+
71
+ - `GPFSEL0..5` (Function Select): 3 bits por pino para definir `IN`, `OUT`, ou função alternativa
72
+ - `GPSET0..1` (Set): escrever `1` no bit seta o pino em nível alto (atômico)
73
+ - `GPCLR0..1` (Clear): escrever `1` no bit seta o pino em nível baixo (atômico)
74
+ - `GPLEV0..1` (Level): lê o nível atual do pino
75
+ - `GPIO_PUP_PDN_CNTRL_REG0..3`: 2 bits por pino para pull-up/pull-down/no-pull (BCM2711)
76
+
77
+ ## Uso
78
+
79
+ ### API estilo “módulo” (`GPIO`)
80
+
81
+ ```python
82
+ from bcmio import GPIO
83
+
84
+ GPIO.open() # inicializa /dev/mem + mmap
85
+ GPIO.setup(17, GPIO.OUT, pull=GPIO.PULL_NONE)
86
+ GPIO.write(17, GPIO.HIGH)
87
+ value = GPIO.read(17)
88
+ GPIO.cleanup()
89
+ ```
90
+
91
+ ### API orientada a objeto (`Pin`)
92
+
93
+ ```python
94
+ from bcmio import Pin
95
+
96
+ led = Pin(17, mode=Pin.OUT)
97
+ led.high()
98
+ led.low()
99
+ led.toggle()
100
+ led.close()
101
+ ```
102
+
103
+ ## Exemplos
104
+
105
+ Veja `exemplos/`:
106
+
107
+ - `exemplos/blink.py`
108
+ - `exemplos/button_read.py`
109
+ - `exemplos/toggle.py`
110
+ - `exemplos/read_digital.py`
111
+
112
+ ## Testes
113
+
114
+ Os testes em `testes/` não acessam `/dev/mem`. Eles usam um backend fake de memória para validar:
115
+
116
+ - cálculo de offsets e bitfields de `GPFSEL`
117
+ - `GPSET/GPCLR` e leitura em `GPLEV`
118
+ - configuração de pull em `GPIO_PUP_PDN_CNTRL_REGx`
119
+
120
+ Rodar:
121
+
122
+ ```bash
123
+ python -m pip install -e ".[dev]"
124
+ pytest -q
125
+ ```
126
+
127
+ ## Segurança e boas práticas
128
+
129
+ - Use `sudo` apenas quando necessário.
130
+ - Prefira isolar e revisar o endereço base antes de usar em produção.
131
+ - Em produção, considere um modo “safe” ou `/dev/gpiomem` (não implementado aqui por requisito).
132
+
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ bcmio/__init__.py
5
+ bcmio/constants.py
6
+ bcmio/exceptions.py
7
+ bcmio/gpio.py
8
+ bcmio/interrupts.py
9
+ bcmio/memory.py
10
+ bcmio/pin.py
11
+ bcmio/pwm.py
12
+ bcmio/utils.py
13
+ bcmio/utils_logging.py
14
+ bcmio.egg-info/PKG-INFO
15
+ bcmio.egg-info/SOURCES.txt
16
+ bcmio.egg-info/dependency_links.txt
17
+ bcmio.egg-info/requires.txt
18
+ bcmio.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+
2
+ [dev]
3
+ pytest>=8
4
+ ruff>=0.5
5
+ mypy>=1.10
@@ -0,0 +1 @@
1
+ bcmio
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bcmio"
7
+ version = "0.1.0"
8
+ description = "Low-level GPIO library for Raspberry Pi 4 (BCM2711) using /dev/mem + mmap"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "bcmio contributors" }]
13
+ keywords = ["raspberrypi", "gpio", "bcm2711", "mmap", "embedded", "linux"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: System :: Hardware",
22
+ "Topic :: Software Development :: Embedded Systems",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["pytest>=8", "ruff>=0.5", "mypy>=1.10"]
27
+
28
+ [tool.setuptools]
29
+ packages = ["bcmio"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ target-version = "py310"
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I", "B", "UP", "SIM"]
37
+
38
+ [tool.mypy]
39
+ python_version = "3.10"
40
+ warn_unused_ignores = true
41
+ warn_redundant_casts = true
42
+ warn_unused_configs = true
43
+ disallow_untyped_defs = true
44
+ check_untyped_defs = true
45
+ no_implicit_optional = true
46
+
bcmio-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
bcmio-0.1.0/setup.py ADDED
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from setuptools import setup
4
+
5
+ setup()
6
+