Skip to content

Commit 208c2e1

Browse files
authored
Merge pull request #96 from pimoroni/strix-technica-custom
MQTT Example Project
2 parents b9f3595 + 74b4135 commit 208c2e1

File tree

1 file changed

+229
-0
lines changed

1 file changed

+229
-0
lines changed

projects/mqtt/mqtt.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#!/usr/bin/env python
2+
3+
from sys import exit
4+
import argparse
5+
import time
6+
7+
try:
8+
import paho.mqtt.client as mqtt
9+
except ImportError:
10+
raise ImportError("This example requires the paho-mqtt module\nInstall with: sudo pip install paho-mqtt")
11+
12+
import blinkt
13+
14+
15+
MQTT_SERVER = "localhost"
16+
MQTT_PORT = 1883
17+
MQTT_TOPIC = "pimoroni/blinkt"
18+
19+
# Set these to use authorisation
20+
MQTT_USER = None
21+
MQTT_PASS = None
22+
23+
description = """\
24+
MQTT Blinkt! Control
25+
26+
This example uses MQTT messages from {server} on port {port} to control Blinkt!
27+
28+
It will monitor the {topic} topic by default, and understands the following messages:
29+
30+
rgb,<pixel>,<r>,<g>,<b> - Set a single pixel to an RGB colour. Example: rgb,1,255,0,255
31+
clr - Clear Blinkt!
32+
bri,<val> - Set global brightness (for val in range 0.0-1.0)
33+
34+
You can use the online MQTT tester at http://www.hivemq.com/demos/websocket-client/ to send messages.
35+
36+
Use {server} as the host, and port 80 (Eclipse's websocket port). Set the topic to topic: {topic}
37+
""".format(
38+
server=MQTT_SERVER,
39+
port=MQTT_PORT,
40+
topic=MQTT_TOPIC
41+
)
42+
parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter)
43+
parser.add_argument('-H', '--host', default=MQTT_SERVER,
44+
help='MQTT broker to connect to')
45+
parser.add_argument('-P', '--port', default=MQTT_PORT, type=int,
46+
help='port on MQTT broker to connect to')
47+
parser.add_argument('-T', '--topic', action='append',
48+
help='MQTT topic to subscribe to; can be repeated for multiple topics')
49+
parser.add_argument('-u', '--user',
50+
help='MQTT broker user name')
51+
parser.add_argument('-p', '--pass', dest='pw',
52+
help='MQTT broker password')
53+
parser.add_argument('-q', '--quiet', default=False, action='store_true',
54+
help='Minimal output (eg for running as a daemon)')
55+
parser.add_argument('-g', '--green-hack', default=False, action='store_true',
56+
help='Apply hack to green channel to improve colour saturation')
57+
parser.add_argument('--timeout', default='0',
58+
help='Pixel timeout(s). Pixel will blank if last update older than X seconds. May be a single number or comma-separated list. Use 0 for no timeout')
59+
parser.add_argument('-D', '--daemon', metavar='PIDFILE',
60+
help='Run as a daemon (implies -q)')
61+
62+
args = parser.parse_args()
63+
64+
# Get timeout list into expected form
65+
args.timeout = args.timeout.split(',')
66+
67+
if len(args.timeout) == 1:
68+
args.timeout = args.timeout * blinkt.NUM_PIXELS
69+
elif len(args.timeout) != blinkt.NUM_PIXELS:
70+
print("--timeout list must be {} elements long".format(blinkt.NUM_PIXELS))
71+
exit(1)
72+
73+
try:
74+
args.timeout = [int(x) for x in args.timeout]
75+
except ValueError as e:
76+
print("Bad timeout value: {}".format(e))
77+
exit(1)
78+
79+
args.timeout = [x and x or 0 for x in args.timeout]
80+
args.min_timeout = min(args.timeout)
81+
82+
if args.daemon:
83+
import signal
84+
try:
85+
import daemon
86+
except ImportError:
87+
raise ImportError("--daemon requires the daemon module. Install with: sudo pip install python-daemon")
88+
try:
89+
import lockfile.pidlockfile
90+
except ImportError:
91+
raise ImportError("--daemon requires the lockfile module. Install with: sudo pip install lockfile")
92+
93+
if not args.topic:
94+
args.topic = [MQTT_TOPIC]
95+
96+
97+
class PixelClient(mqtt.Client):
98+
def __init__(self, prog_args, *args, **kwargs):
99+
super(PixelClient, self).__init__(*args, **kwargs)
100+
self.args = prog_args
101+
self.update_time = [None] * blinkt.NUM_PIXELS
102+
self.on_connect = self.on_connect
103+
self.on_message = self.on_message
104+
105+
blinkt.set_clear_on_exit()
106+
# Some stuff doesn't get set up until the first time show() is called
107+
blinkt.show()
108+
109+
if self.args.user is not None and self.args.pw is not None:
110+
self.username_pw_set(username=self.args.user, password=self.args.pw)
111+
self.connect(self.args.host, self.args.port, 60)
112+
113+
def cmd_clear(self):
114+
blinkt.clear()
115+
blinkt.show()
116+
117+
def cmd_brightness(self, bri):
118+
try:
119+
bri = float(bri)
120+
except ValueError:
121+
print("Malformed command brightness, expected float, got: {}".format(str(bri)))
122+
return
123+
blinkt.set_brightness(bri)
124+
blinkt.show()
125+
126+
def cmd_rgb(self, pixel, data):
127+
try:
128+
if pixel == "*":
129+
pixel = None
130+
else:
131+
pixel = int(pixel)
132+
if pixel > 7:
133+
print("Pixel out of range: {}".format(str(pixel)))
134+
return
135+
136+
r, g, b = [int(x) & 0xff for x in data]
137+
if self.args.green_hack:
138+
# Green is about twice the luminosity for a given value
139+
# than red or blue, so apply a hackish linear compensation
140+
# here taking care of corner cases at 0 and 255. To do it
141+
# properly, it should really be a curve but this approximation
142+
# is quite a lot better than nothing.
143+
if r not in [0, 255]:
144+
r = r + 1
145+
if g not in [0]:
146+
g = g / 2 + 1
147+
if b not in [0, 255]:
148+
b = b + 1
149+
150+
if not self.args.quiet:
151+
print('rgb', pixel, r, g, b)
152+
153+
except ValueError:
154+
print("Malformed RGB command: {} {}".format(str(pixel), str(data)))
155+
return
156+
157+
if pixel is None:
158+
for x in range(blinkt.NUM_PIXELS):
159+
blinkt.set_pixel(x, r, g, b)
160+
self.update_time[x] = time.time()
161+
else:
162+
blinkt.set_pixel(pixel, r, g, b)
163+
self.update_time[pixel] = time.time()
164+
165+
blinkt.show()
166+
167+
def on_connect(self, client, userdata, flags, rc):
168+
if not self.args.quiet:
169+
print("Connected to {s}:{p} listening for topics {t} with result code {r}.\nSee {c} --help for options.".format(
170+
s=self.args.host,
171+
p=self.args.port,
172+
t=', '.join(self.args.topic),
173+
r=rc,
174+
c=parser.prog
175+
))
176+
177+
for topic in self.args.topic:
178+
client.subscribe(topic)
179+
180+
def on_message(self, client, userdata, msg):
181+
182+
data = msg.payload.decode('utf-8').strip().split(',')
183+
command = data.pop(0)
184+
print(command, data)
185+
186+
if command == "clr" and len(data) == 0:
187+
self.cmd_clear()
188+
return
189+
190+
if command == "bri" and len(data) == 1:
191+
self.cmd_brightness(data[0])
192+
return
193+
194+
if command == "rgb" and len(data) == 4:
195+
self.cmd_rgb(data[0], data[1:])
196+
return
197+
198+
def blank_timed_out_pixels(self):
199+
now = time.time()
200+
to_upt_pairs = zip(self.args.timeout, self.update_time)
201+
for pixel, (to, uptime) in enumerate(to_upt_pairs):
202+
if to is not None and uptime is not None and uptime < now - to:
203+
blinkt.set_pixel(pixel, 0, 0, 0)
204+
self.update_time[pixel] = None
205+
blinkt.show()
206+
207+
def loop(self, timeout=1.0, max_packets=1):
208+
self.blank_timed_out_pixels()
209+
return super(PixelClient, self).loop(timeout, max_packets)
210+
211+
212+
def sigterm(signum, frame):
213+
client._thread_terminate = True
214+
215+
216+
if args.daemon:
217+
# Monkey-patch daemon so's the daemon starts reasonably quickly. FDs don't
218+
# strictly speaking need closing anyway because we haven't opened any (yet).
219+
daemon.daemon.get_maximum_file_descriptors = lambda: 32
220+
args.quiet = True
221+
pidlf = lockfile.pidlockfile.PIDLockFile(args.daemon)
222+
with daemon.DaemonContext(
223+
pidfile=pidlf,
224+
signal_map={signal.SIGTERM: sigterm}):
225+
client = PixelClient(args)
226+
client.loop_forever()
227+
else:
228+
client = PixelClient(args)
229+
client.loop_forever()

0 commit comments

Comments
 (0)