Skip to content

Commit 6f74a48

Browse files
Merge pull request #498 from AndreWohnsland/dev
Add i2c support for PCA9535, MCP23017, PCF8574
2 parents a9ff3dc + 62c33a0 commit 6f74a48

35 files changed

+1330
-424
lines changed

AGENTS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Project Rules
2+
3+
- use the defined toolkit (e.g. uv, ruff, ty, yarn, biome) for dependency management and code quality
4+
- Ensure tests are still succeeding after making a change, if a test exists
5+
- Do not implement unasked extra features or edge cases, if you think they are crucial, you need to ask for permission first
6+
- new dependencies need to work especially under unix (Raspberry Pi) systems
7+
8+
## App Versions
9+
10+
- **v1 (Qt)**: Desktop app using PyQt6, run via `runme.py` or `just qt`
11+
- **v2 (Web)**: React frontend + FastAPI backend, run via `just api` + `just web`
12+
13+
## Project Structure
14+
15+
- `src/` - Main Python source (shared between v1/v2)
16+
- `src/ui/` - Qt UI setup code (v1 only)
17+
- `src/api/` - FastAPI routers & endpoints (v2 backend)
18+
- `web_client/src/` - React components (v2 frontend)
19+
- `tests/` - Pytest tests
20+
- `dashboard/` - Separate analytics dashboard app
21+
22+
## Shared vs Version-Specific Code
23+
24+
Shared code (used by both v1 and v2):
25+
26+
- `src/database_commander.py` - Database operations
27+
- `src/models.py` - Core data models
28+
- `src/machine/` - Hardware control
29+
- `src/config/` - Configuration handling
30+
31+
Version-specific code:
32+
33+
- `src/ui/` - Qt only (v1)
34+
- `src/api/` - FastAPI only (v2)
35+
36+
## Configuration
37+
38+
- `src/config/` - Configuration management module
39+
- `custom_config.yaml` - User customization file
40+
- `web_client/.env.*` - Web client environment settings

docs/payment.md

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -197,35 +197,35 @@ The recommended way for a "basic" hardware setup is:
197197
### Installation Steps
198198

199199
=== "NFC Based"
200-
**Linux Systems**
201200

202-
Linux is the most easy way, since most of the things can be done over a script.
203-
Just run:
201+
Follow the according steps for your OS to set up the payment service and GUI.
204202

205-
```bash
206-
wget -O - https://raw.githubusercontent.com/AndreWohnsland/CocktailBerry-Payment/main/scripts/unix_installer.sh | bash
207-
```
203+
=== "Linux Systems"
208204

209-
Then follow the services installation steps below.
205+
Linux is the most easy way, since most of the things can be done over a script.
206+
Just run:
210207

211-
**Windows Systems**
208+
```bash
209+
wget -O - https://raw.githubusercontent.com/AndreWohnsland/CocktailBerry-Payment/main/scripts/unix_installer.sh | bash
210+
```
212211

213-
Windows can be quite restrictive when it comes to executing scripts and similar tasks.
214-
Make sure the user is able to execute PowerShell as well as Python scripts and can install applications.
215-
If you want to use docker on windows, make sure you [install it](https://docs.docker.com/desktop/setup/install/windows-install/) and set it to auto start with windows.
212+
=== "Windows Systems"
216213

217-
Then just open a PowerShell terminal as Administrator and run the following command to download and execute the installation script:
214+
Windows can be quite restrictive when it comes to executing scripts and similar tasks.
215+
Make sure the user is able to execute PowerShell as well as Python scripts and can install applications.
216+
If you want to use docker on windows, make sure you [install it](https://docs.docker.com/desktop/setup/install/windows-install/) and set it to auto start with windows.
218217

219-
```powershell
220-
powershell -ExecutionPolicy ByPass -c "irm https://github.com/AndreWohnsland/CocktailBerry-Payment/blob/main/scripts/windows_installer.ps1 | iex"
221-
```
218+
Then open a PowerShell terminal as Administrator and run the following command to download and execute the installation script:
219+
220+
```powershell
221+
powershell -ExecutionPolicy ByPass -c "irm https://github.com/AndreWohnsland/CocktailBerry-Payment/blob/main/scripts/windows_installer.ps1 | iex"
222+
```
222223

223-
<!-- TODO: LINK -->
224-
<!-- Alternatively, there is also a pre-built executable available for the GUI, which you can download from the release page. -->
225-
<!-- You might not be able to set all options tough when using this directly, so even when using this, going over this preparation and service installation steps is recommended. -->
224+
<!-- TODO: LINK -->
225+
<!-- Alternatively, there is also a pre-built executable available for the GUI, which you can download from the release page. -->
226+
<!-- You might not be able to set all options tough when using this directly, so even when using this, going over this preparation and service installation steps is recommended. -->
226227

227-
If [uv](https://docs.astral.sh/uv/getting-started/installation) or [git](https://git-scm.com/install/windows) fails to install, you might need to install them manually first.
228-
Then follow the services installation steps below.
228+
If [uv](https://docs.astral.sh/uv/getting-started/installation) or [git](https://git-scm.com/install/windows) fails to install, you might need to install them manually first.
229229

230230
**Service installation**
231231

docs/setup.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ They can be used at own risk of CocktailBerry not working 100% properly.
6464
??? info "List Hardware Config Values"
6565
Hardware config values are used to configure and enable the connected hardware.
6666

67-
| Value Name | Description |
68-
| :--------------------- | :---------------------------------------------------------------------------------------------- |
69-
| `PUMP_CONFIG` | List with config for each pump: pin, volume flow, tube volume to pump up |
70-
| `LED_NORMAL` | List of normal (non-addressable) LED configs: pin, default on, prep state |
71-
| `LED_WSLED` | List of WS281x (addressable) LED configs: pin, brightness, count, rings, default on, prep state |
72-
| `RFID_READER` | Enables connected RFID reader, [more info](troubleshooting.md#set-up-rfid-reader) |
73-
| `MAKER_PINS_INVERTED` | [Inverts](faq.md#what-is-the-inverted-option) pin signal (on=low, off=high) |
74-
| `MAKER_PUMP_REVERSION` | Enables reversion (direction) of pump |
75-
| `MAKER_REVERSION_PIN` | [Pin](#configuring-the-pins-or-used-board) which triggers reversion |
67+
| Value Name | Description |
68+
| :---------------------------- | :---------------------------------------------------------------------------------------------- |
69+
| `MAKER_PINS_INVERTED` | [Inverts](faq.md#what-is-the-inverted-option) pin signal (on=low, off=high) |
70+
| `PUMP_CONFIG` | List with config for each pump: pin, volume flow, tube volume to pump up |
71+
| `I2C_CONFIG` | Config for I2C GPIO expander (16 pins, 0-15) |
72+
| `LED_NORMAL` | List of normal (non-addressable) LED configs: pin, default on, prep state |
73+
| `LED_WSLED` | List of WS281x (addressable) LED configs: pin, brightness, count, rings, default on, prep state |
74+
| `RFID_READER` | Enables connected RFID reader, [more info](troubleshooting.md#set-up-rfid-reader) |
75+
| `MAKER_PUMP_REVERSION_CONFIG` | Enables reversion (direction) of pump during cleaning |
7676

7777
??? info "List Software Config Values"
7878
Software config values are used to configure additional connected software and its behavior.

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ dependencies = [
2222
"mfrc522>=0.0.7 ; sys_platform == 'linux'",
2323
"rpi-ws281x>=5.0.0 ; sys_platform == 'linux'",
2424
"gpiozero>=2.0.1 ; sys_platform == 'linux'",
25+
"rpi-gpio>=0.7.1 ; sys_platform == 'linux'",
2526
"python-periphery>=2.4.1",
2627
"sqlalchemy>=2.0.40",
2728
"alembic>=1.14.1",
2829
"pulp>=3.2.2",
2930
"stamina>=25.2.0",
31+
"adafruit-circuitpython-mcp230xx>=2.5.19",
32+
"adafruit-circuitpython-pcf8574>=1.0.15",
33+
"adafruit-circuitpython-busdevice>=5.2.15",
3034
]
3135

3236
[dependency-groups]

scripts/setup.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ else
113113
uv pip install lgpio
114114
fi
115115
# on none RPi devices, we need to set control to the GPIOs, and set user to sudoers
116-
if ! is_raspberry_pi; then
116+
if is_raspberry_pi; then
117+
sudo raspi-config nonint do_i2c 0
118+
else
117119
bash scripts/setup_non_rpi.sh
118120
fi
119121
fi

src/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
SupportedRfidType = Literal["No", "MFRC522", "USB"]
99
SupportedPaymentOptions = Literal["Disabled", "CocktailBerry", "SumUp"]
1010
SupportedLedStatesType = Literal["Effect", "On", "Off"]
11+
I2CExpanderType = Literal["MCP23017", "PCF8574", "PCA9535"]
12+
SupportedPinControlType = Literal["GPIO", "MCP23017", "PCF8574", "PCA9535"]
1113
NEEDED_PYTHON_VERSION = (3, 11)
1214
FUTURE_PYTHON_VERSION = (3, 13)

src/api/routers/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ async def update_options(options: dict, background_tasks: BackgroundTasks) -> Ap
106106
async def clean_machine(background_tasks: BackgroundTasks) -> ApiMessage:
107107
raise_when_cocktail_is_in_progress()
108108
_logger.log_header("INFO", "Cleaning the Pumps")
109-
revert_pumps = cfg.MAKER_PUMP_REVERSION
109+
revert_pumps = cfg.MAKER_PUMP_REVERSION_CONFIG.use_reversion
110110
mc = MachineController()
111111
background_tasks.add_task(mc.clean_pumps, None, revert_pumps)
112112
return ApiMessage(message=DH.get_translation("cleaning_started"))

src/config/config_manager.py

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,17 @@
2727
ConfigInterface,
2828
DictType,
2929
FloatType,
30+
I2CExpanderConfig,
3031
IntType,
3132
ListType,
3233
NormalLedConfig,
3334
PumpConfig,
35+
ReversionConfig,
3436
StringType,
3537
WS281xLedConfig,
3638
)
3739
from src.config.errors import ConfigError
38-
from src.config.validators import build_number_limiter, validate_max_length
40+
from src.config.validators import build_number_limiter, validate_max_length, valide_no_identical_active_i2c_devices
3941
from src.filepath import CUSTOM_CONFIG_FILE
4042
from src.logger_handler import LoggerHandler
4143
from src.models import CocktailStatus
@@ -81,8 +83,12 @@ class ConfigManager:
8183
UI_PICTURE_SIZE: int = 240
8284
UI_ONLY_MAKER_TAB: bool = False
8385
PUMP_CONFIG: ClassVar[list[PumpConfig]] = [
84-
PumpConfig(pin, flow, 0) for pin, flow in zip(_default_pins, _default_volume_flow)
86+
PumpConfig(pin, flow, 0, "GPIO") for pin, flow in zip(_default_pins, _default_volume_flow)
8587
]
88+
# Inverts the pin signal (on is low, off is high)
89+
MAKER_PINS_INVERTED: bool = True
90+
# MCP23017 I2C GPIO expander configuration (16 pins, address 0x20-0x27)
91+
I2C_CONFIG: ClassVar[list[I2CExpanderConfig]] = []
8692
# Custom name of the Maker
8793
MAKER_NAME: str = f"CocktailBerry (#{random.randint(0, 1000000):07})"
8894
# Number of bottles possible at the machine
@@ -95,16 +101,12 @@ class ConfigManager:
95101
MAKER_CLEAN_TIME: int = 20
96102
# Base multiplier for alcohol in the recipe
97103
MAKER_ALCOHOL_FACTOR: int = 100
98-
# Option to invert pump direction
99-
MAKER_PUMP_REVERSION: bool = False
100-
# Pin used for the pump direction
101-
MAKER_REVERSION_PIN: int = 0
104+
# Reversion for cleaning
105+
MAKER_PUMP_REVERSION_CONFIG = ReversionConfig(use_reversion=False, pin=0, pin_type="GPIO", inverted=False)
102106
# If the maker should check automatically for updates
103107
MAKER_SEARCH_UPDATES: bool = True
104108
# If the maker should check if there is enough in the bottle before making a cocktail
105109
MAKER_CHECK_BOTTLE: bool = True
106-
# Inverts the pin signal (on is low, off is high)
107-
MAKER_PINS_INVERTED: bool = True
108110
# Theme Setting to load according qss file
109111
MAKER_THEME: SupportedThemesType = "default"
110112
# How many ingredients are allowed to be added by hand to be available cocktail
@@ -175,28 +177,49 @@ def __init__(self) -> None:
175177
"UI_HEIGHT": IntType([build_number_limiter(1, 3000)]),
176178
"UI_PICTURE_SIZE": IntType([build_number_limiter(100, 1000)]),
177179
"UI_ONLY_MAKER_TAB": BoolType(check_name="Only Maker Tab Accessible"),
180+
"MAKER_PINS_INVERTED": BoolType(check_name="Inverted"),
178181
"PUMP_CONFIG": ListType(
179182
DictType(
180183
{
181-
"pin": IntType([build_number_limiter(0, 1000)], prefix="Pin:"),
184+
"pin_type": ChooseOptions.pin,
185+
"pin": IntType([build_number_limiter(0)], prefix="Pin:"),
182186
"volume_flow": FloatType([build_number_limiter(0.1, 1000)], suffix="ml/s"),
183187
"tube_volume": IntType([build_number_limiter(0, 100)], suffix="ml"),
184188
},
185189
PumpConfig,
186190
),
187191
lambda: self.choose_bottle_number(ignore_limits=True),
188192
),
193+
"I2C_CONFIG": ListType(
194+
DictType(
195+
{
196+
"device_type": ChooseOptions.i2c,
197+
"enabled": BoolType(check_name="Enabled", default=True),
198+
"address_int": IntType(prefix="0x", default=20),
199+
"inverted": BoolType(check_name="Inverted"),
200+
},
201+
I2CExpanderConfig,
202+
),
203+
0,
204+
[valide_no_identical_active_i2c_devices],
205+
),
189206
"MAKER_NAME": StringType([validate_max_length]),
190207
"MAKER_NUMBER_BOTTLES": IntType([build_number_limiter(1, 999)]),
191208
"MAKER_PREPARE_VOLUME": ListType(IntType([build_number_limiter(25, 1000)], suffix="ml", default=100), 1),
192209
"MAKER_SIMULTANEOUSLY_PUMPS": IntType([build_number_limiter(1, 999)]),
193210
"MAKER_CLEAN_TIME": IntType([build_number_limiter()], suffix="s"),
194211
"MAKER_ALCOHOL_FACTOR": IntType([build_number_limiter(10, 200)], suffix="%"),
195-
"MAKER_PUMP_REVERSION": BoolType(check_name="Pump can be Reversed"),
196-
"MAKER_REVERSION_PIN": IntType([build_number_limiter(0, 1000)]),
212+
"MAKER_PUMP_REVERSION_CONFIG": DictType(
213+
{
214+
"use_reversion": BoolType(check_name="active", default=False),
215+
"pin": IntType([build_number_limiter(0)], default=0, prefix="Pin:"),
216+
"pin_type": ChooseOptions.pin,
217+
"inverted": BoolType(check_name="Inverted", default=False),
218+
},
219+
ReversionConfig,
220+
),
197221
"MAKER_SEARCH_UPDATES": BoolType(check_name="Search for Updates"),
198222
"MAKER_CHECK_BOTTLE": BoolType(check_name="Check Bottle Volume"),
199-
"MAKER_PINS_INVERTED": BoolType(check_name="Inverted"),
200223
"MAKER_THEME": ChooseOptions.theme,
201224
"MAKER_MAX_HAND_INGREDIENTS": IntType([build_number_limiter(0, 10)]),
202225
"MAKER_CHECK_INTERNET": BoolType(check_name="Check Internet"),
@@ -205,7 +228,8 @@ def __init__(self) -> None:
205228
"LED_NORMAL": ListType(
206229
DictType(
207230
{
208-
"pin": IntType([build_number_limiter(0, 200)], prefix="Pin:"),
231+
"pin_type": ChooseOptions.pin,
232+
"pin": IntType([build_number_limiter(0)], prefix="Pin:"),
209233
"default_on": BoolType(check_name="Default On"),
210234
"preparation_state": ChooseOptions.leds,
211235
},
@@ -216,7 +240,7 @@ def __init__(self) -> None:
216240
"LED_WSLED": ListType(
217241
DictType(
218242
{
219-
"pin": IntType([build_number_limiter(0, 200)], prefix="Pin:"),
243+
"pin": IntType([build_number_limiter(0)], prefix="Pin:"),
220244
"brightness": IntType([build_number_limiter(1, 100)], suffix="%", default=100),
221245
"count": IntType([build_number_limiter(1, 500)], suffix="LEDs", default=24),
222246
"number_rings": IntType([build_number_limiter(1, 10)], suffix="X", default=1),
@@ -341,6 +365,8 @@ def set_config(self, configuration: dict, validate: bool) -> None:
341365
self._set_config(no_list_or_dict, validate)
342366
list_or_dict = {k: value for k, value in configuration.items() if isinstance(value, list | dict)}
343367
self._set_config(list_or_dict, validate)
368+
# Cross-config validation for dependencies between configs
369+
self._validate_cross_config(validate)
344370

345371
def _set_config(self, configuration: dict, validate: bool) -> None:
346372
for config_name, config_value in configuration.items():
@@ -358,13 +384,31 @@ def _set_config(self, configuration: dict, validate: bool) -> None:
358384
if validate:
359385
raise e
360386

361-
def _validate_config_type(self, configname: str, configvalue: Any) -> None:
362-
"""Validate the configvalue if its fit the type / conditions."""
363-
config_setting = self.config_type.get(configname)
364-
# old or user added configs will not be validated
365-
if config_setting is None:
366-
return
367-
config_setting.validate(configname, configvalue)
387+
def _validate_cross_config(self, validate: bool) -> None:
388+
"""Validate cross-config dependencies.
389+
390+
Currently implemented:
391+
- Checks that I2C pin types used in PUMP_CONFIG have corresponding enabled devices in I2C_CONFIG.
392+
"""
393+
# Collect all I2C pin types used in PUMP_CONFIG (excluding GPIO)
394+
required_i2c_types: set[str] = set()
395+
for pump in self.PUMP_CONFIG:
396+
if pump.pin_type != "GPIO":
397+
required_i2c_types.add(pump.pin_type)
398+
399+
# Get enabled I2C device types from I2C_CONFIG
400+
enabled_i2c_types = {cfg.device_type for cfg in self.I2C_CONFIG if cfg.enabled}
401+
402+
# Check that all required I2C types have enabled devices
403+
missing_types = required_i2c_types - enabled_i2c_types
404+
if missing_types:
405+
error_msg = (
406+
f"PUMP_CONFIG uses I2C pin types {', '.join(missing_types)} but I2C_CONFIG "
407+
"has no enabled devices of these types. Add enabled entries to I2C_CONFIG."
408+
)
409+
_logger.error(f"Config Error: {error_msg}")
410+
if validate:
411+
raise ConfigError(error_msg)
368412

369413
def choose_bottle_number(self, get_all: bool = False, ignore_limits: bool = False) -> int:
370414
"""Select the number of Bottles, limits by max supported count."""

0 commit comments

Comments
 (0)