Skip to content

Commit 6494122

Browse files
committed
Initial import
0 parents  commit 6494122

File tree

10 files changed

+307
-0
lines changed

10 files changed

+307
-0
lines changed

.github/workflows/build.yml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: build
2+
3+
on: [push, workflow_dispatch]
4+
5+
env:
6+
DOCKER_REGISTRY: ghcr.io
7+
DOCKER_REPO: ${{ github.repository_owner }}
8+
DOCKER_IMAGE: rc-server
9+
DOCKER_TAG: ${{ github.ref_name }}
10+
DOCKER_PLATFORM: linux/arm64
11+
12+
jobs:
13+
build-docker:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
- name: Set up QEMU
19+
uses: docker/setup-qemu-action@v3
20+
with:
21+
platforms: arm64
22+
- name: Set up Docker Buildx
23+
uses: docker/setup-buildx-action@v3
24+
- name: Login to Docker registry
25+
uses: docker/login-action@v3
26+
with:
27+
registry: ${{ env.DOCKER_REGISTRY }}
28+
username: ${{ github.actor }}
29+
password: ${{ secrets.GITHUB_TOKEN }}
30+
- name: Build and publish image
31+
run: docker buildx build --platform ${DOCKER_PLATFORM} --tag "${DOCKER_REGISTRY}/${DOCKER_REPO}/${DOCKER_IMAGE}:${DOCKER_TAG}" --push .

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/

Dockerfile

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Ubuntu 22.04 base image
2+
FROM ubuntu:22.04
3+
4+
# Install Python
5+
RUN apt-get -q update && DEBIAN_FRONTEND="noninteractive" apt-get -q install -y -o Dpkg::Options::="--force-confnew" --no-install-recommends build-essential python3-dev python3-pip && rm -rf /var/lib/apt/lists/*
6+
7+
# Create app directory
8+
WORKDIR /app
9+
10+
# Install requirements
11+
COPY requirements.txt ./
12+
RUN pip install -r requirements.txt
13+
14+
# Copy app
15+
COPY app/ ./
16+
17+
# Run app
18+
ENTRYPOINT ["/usr/bin/python3", "server.py"]
19+
CMD ["-a", "0.0.0.0"]
20+
EXPOSE 8001

README.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Remote Control REST API
2+
3+
REST API to use an RF 433 MHz transmitter as a remote control. It currently only supports the Chacon DIO 1.0 protocol.
4+
5+
It has only been tested on an ODROID-C4 board and uses an ODROID-specific WiringPi version. It should be easy to adapt it for other boards (e.g. Raspberry Pi).
6+
7+
## Installation
8+
9+
Execute the following commands on a Debian or Ubuntu system to install the required dependencies:
10+
```
11+
apt-get update
12+
apt-get install -y build-essential python3-dev python3-pip
13+
pip install -r requirements.txt
14+
```
15+
16+
## Usage
17+
18+
First, connect an RF 433 MHz transmitter to the GPIO pin of your choice. Take note of the corresponding WiringPi pin number (see [pinout.xyz](https://pinout.xyz/pinout/wiringpi)).
19+
20+
### Server
21+
22+
Execute the following command to run the server locally:
23+
24+
```
25+
./app/server.py
26+
```
27+
28+
You may then go to http://127.0.0.1:8001 to browse the documentation and test the API.
29+
30+
The following arguments are available:
31+
32+
```
33+
./app/server.py [-h] [-a ADDRESS] [-p PORT] [-g GPIO] [-l LOG_LEVEL]
34+
35+
Optional arguments:
36+
-h, --help Show help message and exit
37+
-a ADDRESS, --address ADDRESS Address to bind to (default: 127.0.0.1)
38+
-p PORT, --port PORT Port to listen on (default: 8001)
39+
-g GPIO, --gpio GPIO GPIO WiringPi pin number (default: 0)
40+
-l LOG_LEVEL, --log-level LOG_LEVEL Log level: CRITICAL, ERROR, WARNING, INFO, DEBUG (default: INFO)
41+
```
42+
43+
A Docker image is also available for the arm64 architecture:
44+
45+
```
46+
docker run -it --rm --privileged -p 8001:8001 ghcr.io/fcrespel/rc-server:master [-h] [-a ADDRESS] [-p PORT] [-g GPIO] [-l LOG_LEVEL]
47+
```
48+
49+
You may want to run it in the background using commands such as the following:
50+
51+
```
52+
# Create and start container
53+
docker run -d --name rc-server --privileged -p 127.0.0.1:8001:8001 ghcr.io/fcrespel/rc-server:master
54+
55+
# Stop server
56+
docker stop rc-server
57+
58+
# Start server
59+
docker start rc-server
60+
61+
# Show live logs
62+
docker logs -f rc-server
63+
```
64+
65+
NOTE: the API port is not secured, make sure to only expose it locally or to trusted clients.
66+
67+
### Client
68+
69+
You may call the API with any HTTP client such as curl:
70+
71+
```
72+
# Replace 12345678 with the actual Chacon DIO 1.0 sender code (arbitrary 26-bit integer)
73+
74+
# Get button 1 status:
75+
curl -sSf -XGET http://127.0.0.1:8001/chacondio10/12345678/1
76+
77+
# Set button 1 to ON:
78+
curl -sSf -XPUT http://127.0.0.1:8001/chacondio10/12345678/1 -d 1
79+
80+
# Set button 1 to OFF:
81+
curl -sSf -XPUT http://127.0.0.1:8001/chacondio10/12345678/1 -d 0
82+
```

app/chacondio10/__init__.py

Whitespace-only changes.

app/chacondio10/cli.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import argparse
2+
3+
import odroid_wiringpi as wiringpi
4+
5+
from .protocol import transmit
6+
7+
8+
def parse_args():
9+
parser = argparse.ArgumentParser(description="Chacon DIO 1.0 remote control")
10+
parser.add_argument("-g", "--gpio", help="GPIO WiringPi pin number (default: 0)", type=int, choices=range(0, 30), metavar="[0-29]", default=0)
11+
parser.add_argument("-s", "--sender", help="Sender code 26-bit number", type=int, required=True)
12+
parser.add_argument("-b", "--button", help="Button number between 0 and 15, -1 for all (group function)", type=int, choices=range(-1, 16), metavar="[0-15]", required=True)
13+
parser.add_argument("-o", "--onoff", help="0 (OFF) or 1 (ON)", type=int, choices=range(0, 2), metavar="[0-1]", required=True)
14+
parser.add_argument("-r", "--repeat", help="Number of times to repeat the message (default: 5)", type=int, default=5)
15+
return parser.parse_args()
16+
17+
def main():
18+
args = parse_args()
19+
group = True if args.button < 0 else False
20+
button = 0 if args.button < 0 else args.button
21+
onoff = True if args.onoff > 0 else False
22+
23+
if wiringpi.wiringPiSetup() == -1:
24+
raise Exception("Failed to initialize WiringPi")
25+
wiringpi.pinMode(args.gpio, wiringpi.OUTPUT)
26+
27+
for i in range(args.repeat):
28+
transmit(args.gpio, args.sender, group, button, onoff)
29+
30+
if __name__ == "__main__":
31+
main()

app/chacondio10/protocol.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import odroid_wiringpi as wiringpi
2+
3+
TIME_HIGH_LOCK = 275
4+
TIME_LOW_LOCK1 = 9900
5+
TIME_LOW_LOCK2 = 2675
6+
7+
TIME_HIGH_DATA = 275 # 310 or 275 or 220
8+
TIME_LOW_DATA_LONG = 1225 # 1340 or 1225 or 1400
9+
TIME_LOW_DATA_SHORT = 275 # 310 or 275 or 350
10+
11+
def sendBit(pin: int, b: bool):
12+
if b:
13+
wiringpi.digitalWrite(pin, wiringpi.HIGH)
14+
wiringpi.delayMicroseconds(TIME_HIGH_DATA)
15+
wiringpi.digitalWrite(pin, wiringpi.LOW)
16+
wiringpi.delayMicroseconds(TIME_LOW_DATA_LONG)
17+
else:
18+
wiringpi.digitalWrite(pin, wiringpi.HIGH)
19+
wiringpi.delayMicroseconds(TIME_HIGH_DATA)
20+
wiringpi.digitalWrite(pin, wiringpi.LOW)
21+
wiringpi.delayMicroseconds(TIME_LOW_DATA_SHORT)
22+
23+
def sendPair(pin: int, b: bool):
24+
sendBit(pin, b)
25+
sendBit(pin, not b)
26+
27+
def sendWord(pin: int, word: int, bits: int):
28+
for bit in reversed(range(bits)):
29+
if word & (1 << bit):
30+
sendPair(pin, True)
31+
else:
32+
sendPair(pin, False)
33+
34+
def transmit(pin: int, sender: int, group: bool, button: int, onoff: bool):
35+
# Start lock
36+
wiringpi.digitalWrite(pin, wiringpi.HIGH);
37+
wiringpi.delayMicroseconds(TIME_HIGH_LOCK);
38+
wiringpi.digitalWrite(pin, wiringpi.LOW);
39+
wiringpi.delayMicroseconds(TIME_LOW_LOCK1);
40+
wiringpi.digitalWrite(pin, wiringpi.HIGH);
41+
wiringpi.delayMicroseconds(TIME_HIGH_LOCK);
42+
wiringpi.digitalWrite(pin, wiringpi.LOW);
43+
wiringpi.delayMicroseconds(TIME_LOW_LOCK2);
44+
wiringpi.digitalWrite(pin, wiringpi.HIGH);
45+
46+
# Sender code (26 bits)
47+
sendWord(pin, sender, 26);
48+
49+
# Group bit
50+
sendPair(pin, group);
51+
52+
# On/off bit
53+
sendPair(pin, onoff);
54+
55+
# Button number (4 bits)
56+
sendWord(pin, button, 4);
57+
58+
# End lock
59+
wiringpi.digitalWrite(pin, wiringpi.HIGH);
60+
wiringpi.delayMicroseconds(TIME_HIGH_LOCK);
61+
wiringpi.digitalWrite(pin, wiringpi.LOW);
62+
63+
# Delay before next transmission
64+
wiringpi.delay(10)

app/chacondio10/routes.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from fastapi import APIRouter, Body, Path, Request
2+
3+
from .protocol import transmit
4+
5+
router = APIRouter(prefix="/chacondio10", tags=["chacondio10"])
6+
7+
@router.get("/{sender}/{button}")
8+
async def get_button(request: Request, sender: int = Path(ge=0, le=67108863), button: int = Path(ge=0, le=15)):
9+
if sender in request.app.state.chacondio10 and button in request.app.state.chacondio10[sender]:
10+
return request.app.state.chacondio10[sender][button]
11+
else:
12+
return 0
13+
14+
@router.put("/{sender}/{button}")
15+
async def put_button(request: Request, sender: int = Path(ge=0, le=67108863), button: int = Path(ge=0, le=15), onoff: int = Body(ge=0, le=1), repeat: int = 5):
16+
if not sender in request.app.state.chacondio10:
17+
request.app.state.chacondio10[sender] = {}
18+
if onoff > 0:
19+
request.app.state.chacondio10[sender][button] = 1
20+
for i in range(repeat):
21+
transmit(request.app.state.gpio, sender, False, button, True)
22+
else:
23+
request.app.state.chacondio10[sender][button] = 0
24+
for i in range(repeat):
25+
transmit(request.app.state.gpio, sender, False, button, False)

app/server.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/python3
2+
3+
import argparse
4+
import logging
5+
from contextlib import asynccontextmanager
6+
7+
import odroid_wiringpi as wiringpi
8+
import uvicorn
9+
from chacondio10.routes import router as chacondio10_router
10+
from fastapi import FastAPI
11+
from fastapi.responses import RedirectResponse
12+
13+
logger = logging.getLogger("uvicorn.error")
14+
15+
@asynccontextmanager
16+
async def lifespan(app: FastAPI):
17+
logger.info(f"Setting up WiringPi for GPIO {app.state.gpio}")
18+
if wiringpi.wiringPiSetup() == -1:
19+
raise Exception("Failed to initialize WiringPi")
20+
wiringpi.pinMode(app.state.gpio, wiringpi.OUTPUT)
21+
yield
22+
23+
app = FastAPI(title="Remote Control REST API", description="REST API to use an RF 433 MHz transmitter as a remote control", version="1.0", lifespan=lifespan)
24+
app.include_router(chacondio10_router)
25+
app.state.gpio = 0
26+
app.state.chacondio10 = {}
27+
28+
@app.get("/", include_in_schema=False)
29+
async def home_page():
30+
return RedirectResponse("/docs")
31+
32+
@app.get("/health", tags=["health"])
33+
async def health():
34+
return {"status": "UP"}
35+
36+
def parse_args():
37+
parser = argparse.ArgumentParser(description=app.title)
38+
parser.add_argument("-a", "--address", help="Address to bind to (default: 127.0.0.1)", type=str, default="127.0.0.1")
39+
parser.add_argument("-p", "--port", help="Port to listen on (default: 8001)", type=int, default=8001)
40+
parser.add_argument("-g", "--gpio", help="GPIO WiringPi pin number (default: 0)", type=int, choices=range(0, 30), metavar="GPIO", default=0)
41+
parser.add_argument("-l", "--log-level", help="Log level (default: INFO)", type=str, default="INFO", choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"])
42+
return parser.parse_args()
43+
44+
def main():
45+
args = parse_args()
46+
app.state.gpio = args.gpio
47+
uvicorn.run(app, host=args.address, port=args.port, log_level=logging.getLevelName(args.log_level))
48+
49+
if __name__ == "__main__":
50+
main()

requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fastapi~=0.111.0
2+
uvicorn[standard]~=0.29.0
3+
odroid-wiringpi~=3.16.2

0 commit comments

Comments
 (0)