Skip to content

Commit e552c81

Browse files
authored
Merge pull request #183 from Solmath/feature/play-video
Improve Python project setup and add CLI and logging to DDP
2 parents f0e2d78 + cd08aa1 commit e552c81

File tree

7 files changed

+458
-40
lines changed

7 files changed

+458
-40
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@ dist-ssr
3030
*.njsproj
3131
*.sln
3232
*.sw?
33+
34+
# Python
35+
# Byte-compiled / optimized / DLL files
36+
__pycache__/

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ repos:
33
rev: v6.0.0
44
hooks:
55
- id: check-yaml
6+
- id: check-toml
67
- id: end-of-file-fixer
78
- id: trailing-whitespace
89
- repo: https://github.com/pre-commit/mirrors-clang-format
@@ -11,3 +12,12 @@ repos:
1112
- id: clang-format
1213
types_or: [c++, c]
1314
exclude: lib/
15+
- repo: https://github.com/astral-sh/ruff-pre-commit
16+
# Ruff version.
17+
rev: v0.14.10
18+
hooks:
19+
# Run the linter.
20+
- id: ruff-check
21+
args: [ --fix ]
22+
# Run the formatter.
23+
- id: ruff-format

README.md

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<!-- markdownlint-disable MD024 -->
2+
13
# IKEA OBEGRÄNSAD Hack/Mod
24

35
Turn your OBEGRÄNSAD LED Wall Lamp into a live drawing canvas
@@ -501,6 +503,22 @@ curl http://your-server/api/clearstorage
501503

502504
DDP enables real-time LED matrix control via UDP packets. External applications can send pixel data directly over the network.
503505

506+
### Python project setup with Poetry
507+
508+
Please [install Poetry](https://python-poetry.org/docs/#installation) for an easy way to setup the Python environment.
509+
510+
To install all required dependencies and create a virtual environment run:
511+
512+
```bash
513+
poetry install
514+
```
515+
516+
VS Code should detect the venv automatically and prompt you to activate it. Once that's done you can use the `python` command as usual. Alternatively use the `poetry run` command to make sure the python from the virtual environment is executed, e.g.:
517+
518+
```bash
519+
poetry run python ddp.py clear
520+
```
521+
504522
### Quick Start
505523

506524
1. **Enable DDP Plugin**
@@ -510,36 +528,48 @@ DDP enables real-time LED matrix control via UDP packets. External applications
510528
```
511529

512530
2. **Send Pixels**
531+
513532
```bash
514533
python3 ddp.py --ip 192.168.178.50 --fill 128
515534
```
516535

517536
### Using ddp.py
518537

519-
The included Python script (`ddp.py`) simplifies DDP packet creation.
538+
The included Python script (`ddp.py`) simplifies DDP packet creation. It offers a rudimentary command line interface to control display content.
520539

521540
**Clear all pixels:**
522541

523542
```bash
524-
python3 ddp.py --ip 192.168.178.50 --clear
543+
python3 ddp.py clear --ip 192.168.178.50
525544
```
526545

527546
**Fill display with brightness value:**
528547

529548
```bash
530-
python3 ddp.py --ip 192.168.178.50 --fill 128
549+
python3 ddp.py fill 128 --ip 192.168.178.50
531550
```
532551

533552
**Set individual pixels (X, Y, brightness):**
534553

535554
```bash
536-
python3 ddp.py --ip 192.168.178.50 --pixel 0 0 255 --pixel 15 15 128
555+
python3 ddp.py pixels --ip 192.168.178.50 -p 0 0 255 -p 15 15 128
537556
```
538557

539-
**Options:**
558+
**Subcommands:**
559+
560+
- `clear`: Clear all pixels
561+
- `fill BRIGHTNESS`: Fill with brightness (0-255)
562+
- `pixels -p X Y BRIGHTNESS`: Set pixel (-p can be used multiple times)
563+
564+
**Options (for all subcommands):**
540565

541566
- `--ip`: Display IP address (default: 192.168.178.50)
542567
- `--port`: UDP port (default: 4048)
568+
- `-v, --verbose`: Write more information to output
569+
- `-d, --debug`: Write even more information to output
570+
571+
**Legacy options (still available for compatibility):**
572+
543573
- `--clear`: Clear all pixels
544574
- `--fill BRIGHTNESS`: Fill with brightness (0-255)
545575
- `--pixel X Y BRIGHTNESS`: Set pixel (can be used multiple times)
@@ -555,7 +585,7 @@ python3 ddp.py --ip 192.168.178.50 --pixel 0 0 255 --pixel 15 15 128
555585

556586
**Packet Structure:**
557587

558-
```
588+
```text
559589
[Header: 10 bytes][RGB Data: 768 bytes for 16×16]
560590
```
561591

@@ -733,7 +763,7 @@ pre-commit install
733763

734764
### Plugin Development
735765

736-
**1. Create Plugin Files**
766+
#### **1. Create Plugin Files**
737767

738768
**plugins/MyPlugin.h:**
739769

@@ -786,7 +816,7 @@ void MyPlugin::websocketHook(JsonDocument &request) {
786816
}
787817
```
788818

789-
**2. Register Plugin in main.cpp**
819+
#### **2. Register Plugin in main.cpp**
790820

791821
```cpp
792822
#include "plugins/MyPlugin.h"

ddp.py

Lines changed: 139 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
#!/usr/bin/env python3
2+
3+
import logging
24
import socket
35
import argparse
46

5-
def create_packet(pixels=None):
7+
logger: logging.Logger = logging.getLogger(__name__)
8+
9+
10+
def create_packet(pixels: list[tuple[int, int, int]] | None = None) -> bytearray:
611
"""Create a DDP packet with specified pixels or all off"""
712
# Header: 10 bytes
8-
packet = bytearray([
9-
0x41, # Version 1
10-
0x00, # Flags
11-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 # Reserved
12-
])
13+
packet = bytearray(
14+
[
15+
0x41, # Version 1
16+
0x00, # Flags
17+
0x00,
18+
0x00,
19+
0x00,
20+
0x00,
21+
0x00,
22+
0x00,
23+
0x00,
24+
0x00, # Reserved
25+
]
26+
)
1327

1428
# Initialize all pixels to 0
1529
data = bytearray([0] * (16 * 16 * 3))
@@ -18,57 +32,150 @@ def create_packet(pixels=None):
1832
if pixels:
1933
for x, y, brightness in pixels:
2034
if 0 <= x < 16 and 0 <= y < 16 and 0 <= brightness <= 255:
21-
index = (y * 16 + x) * 3
22-
data[index:index + 3] = [brightness] * 3
35+
index: int = (y * 16 + x) * 3
36+
data[index : index + 3] = [brightness] * 3
2337

2438
packet.extend(data)
2539
return packet
2640

27-
def send_ddp_packet(ip, port, packet):
41+
42+
def send_ddp_packet(ip: str, port: int, packet: bytearray) -> None:
2843
"""Send a DDP packet to the specified IP and port"""
2944
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
3045
try:
3146
sock.sendto(packet, (ip, port))
32-
print(f"Sent DDP packet to {ip}:{port}")
33-
print(f"Packet size: {len(packet)} bytes")
47+
logger.info(f"Sent DDP packet to {ip}:{port}")
48+
logger.info(f"Packet size: {len(packet)} bytes")
3449
except Exception as e:
35-
print(f"Error: {e}")
50+
logger.error(f"Error: {e}")
3651
finally:
3752
sock.close()
3853

39-
def main():
40-
parser = argparse.ArgumentParser(description='Send DDP packets to control LED matrix')
41-
parser.add_argument('--ip', default='192.168.178.50', help='IP address of the display')
42-
parser.add_argument('--port', type=int, default=4048, help='UDP port')
43-
parser.add_argument('--clear', action='store_true', help='Clear all pixels')
44-
parser.add_argument('--fill', type=int, metavar='BRIGHTNESS',
45-
help='Fill all pixels with specified brightness (0-255)')
46-
parser.add_argument('--pixel', nargs=3, type=int, action='append',
47-
metavar=('X', 'Y', 'BRIGHTNESS'),
48-
help='Set pixel at X,Y to brightness (can be used multiple times)')
4954

50-
args = parser.parse_args()
55+
def create_arg_parser() -> argparse.ArgumentParser:
56+
# Use parent parser for common arguments
57+
parent_parser = argparse.ArgumentParser(
58+
description="The parent parser", add_help=False
59+
)
60+
61+
parent_parser.add_argument(
62+
"--ip", type=str, default="192.168.178.50", help="IP address of the display"
63+
)
64+
parent_parser.add_argument("--port", type=int, default=4048, help="UDP port")
65+
parent_parser.add_argument(
66+
"-d", "--debug", action="store_true", help="Enable debug logging"
67+
)
68+
parent_parser.add_argument(
69+
"-v", "--verbose", action="store_true", help="Enable verbose logging"
70+
)
71+
72+
# Main parser with subcommands
73+
parser = argparse.ArgumentParser(
74+
description="Send DDP packets to control LED matrix", parents=[parent_parser]
75+
)
76+
77+
subparsers = parser.add_subparsers(help="help for subcommand", dest="subcommand")
78+
79+
subparsers.add_parser("clear", help="Clear all pixels", parents=[parent_parser])
80+
81+
fill_parser: argparse.ArgumentParser = subparsers.add_parser(
82+
"fill",
83+
help="Fill all pixels with specified brightness",
84+
parents=[parent_parser],
85+
)
86+
fill_parser.add_argument(
87+
"brightness",
88+
type=int,
89+
metavar="BRIGHTNESS",
90+
choices=range(0, 256),
91+
help="Brightness level (0-255)",
92+
)
93+
94+
pixels_parser: argparse.ArgumentParser = subparsers.add_parser(
95+
"pixels", help="Set individual pixel brightness", parents=[parent_parser]
96+
)
97+
pixels_parser.add_argument(
98+
"-p",
99+
"--pixel",
100+
nargs=3,
101+
type=int,
102+
action="append",
103+
metavar=("X", "Y", "BRIGHTNESS"),
104+
help="Set pixel at X,Y to brightness (can be used multiple times)",
105+
)
106+
107+
# Deprecated arguments for backward compatibility
108+
parser.add_argument(
109+
"--clear", action="store_true", help="Clear all pixels", deprecated=True
110+
)
111+
parser.add_argument(
112+
"--fill",
113+
type=int,
114+
metavar="BRIGHTNESS",
115+
help="Fill all pixels with specified brightness (0-255)",
116+
deprecated=True,
117+
)
118+
parser.add_argument(
119+
"-p",
120+
"--pixel",
121+
nargs=3,
122+
type=int,
123+
action="append",
124+
metavar=("X", "Y", "BRIGHTNESS"),
125+
help="Set pixel at X,Y to brightness (can be used multiple times)",
126+
deprecated=True,
127+
)
128+
129+
return parser
130+
131+
132+
def main() -> None:
133+
parser: argparse.ArgumentParser = create_arg_parser()
134+
args: argparse.Namespace = parser.parse_args()
135+
136+
logging.basicConfig(
137+
level=logging.DEBUG
138+
if args.debug
139+
else logging.INFO
140+
if args.verbose
141+
else logging.WARNING
142+
)
143+
144+
pixels: list[tuple[int, int, int]] = []
51145

52146
# Validate fill brightness
53-
if args.fill is not None and not 0 <= args.fill <= 255:
54-
parser.error("Fill brightness must be between 0 and 255")
147+
if args.subcommand == "fill" or args.fill is not None:
148+
if args.subcommand == "fill":
149+
fill_brightness: int = args.brightness
150+
else:
151+
fill_brightness: int = args.fill
152+
153+
if not 0 <= fill_brightness <= 255:
154+
parser.error("Fill brightness must be between 0 and 255")
155+
156+
logger.info(f"Filling all pixels with brightness {fill_brightness}")
157+
pixels = [(x, y, fill_brightness) for x in range(16) for y in range(16)]
55158

56159
# Validate pixel coordinates and brightness
57-
pixels = []
58-
if args.pixel:
160+
if args.subcommand == "pixels" or args.pixel is not None:
59161
for x, y, brightness in args.pixel:
60162
if not (0 <= x < 16 and 0 <= y < 16):
61163
parser.error(f"Invalid coordinates: {x},{y} (must be 0-15)")
164+
62165
if not (0 <= brightness <= 255):
63166
parser.error(f"Invalid brightness: {brightness} (must be 0-255)")
167+
168+
logger.info(f"Setting pixel ({x},{y}) to brightness {brightness}")
64169
pixels.append((x, y, brightness))
65170

66-
# Create appropriate packet
67-
if args.fill is not None:
68-
pixels = [(x, y, args.fill) for x in range(16) for y in range(16)]
171+
if args.subcommand == "clear" or args.clear:
172+
logger.info("Clearing all pixels")
69173

70-
packet = create_packet(pixels)
174+
packet: bytearray = create_packet(pixels)
71175
send_ddp_packet(args.ip, args.port, packet)
72176

177+
return
178+
179+
73180
if __name__ == "__main__":
74-
main()
181+
main()

0 commit comments

Comments
 (0)