DMA-problem

De senaste dagarna (OK, veckorna) har jag pysslat med att bygga om belysningen i vår akvarievägg till LED-lister istället för den ursprungliga, aldrig färdigställda, varianten med lösa lysdioder. Jag sprang på ett mycket knepigt problem med den Raspberry Pi som jag använde för att styra det hela då den fick en massa larm på SD-kortet, så till den milda grad att den efter ett tag satte det som read-only.

Trasigt kort tänkte jag och provade med ett annat – samma sak. Trasig paj tänkte jag och provade med en annan – samma sak. Något trasigt i installationen tänkte jag och installerade om från grunden – samma sak. Dags att googla. Under ominstallationen märkte jag att larmen kom först när jag drog igång min process som skickar ut signalerna till LED-listerna. Så klantigt kan väl inte ens jag skriva Python-kod så att den knäcker MMC-hanteringen? Nej, det visade sig vara rpi-ws2812-server som bråkade med mig. Efter ett tag hittade jag en sida som beskrev en DMA-krock i kod som även rpi-ws2812-server använder.

Man ska kunna ändra DMA-kanal genom att skicka med rätt siffror i init-kommandot men jag föredrog att ändra rad 281 i main.c från

1
int frequency=WS2811_TARGET_FREQ, dma=5;

till

1
int frequency=WS2811_TARGET_FREQ, dma=10;

för att byta DMA-kanal från 5 till 10 enligt förslag i tråden jag hittade. Efter en omkompilering och en omstart fungerade det mycket bättre!

Tråden är från 2017 och handlar om en äldre version av Raspbian än den jag kör. Jag har två andra pajer igång utan detta problem, en av exakt samma modell som den som krånglar. Alla kör modernare Raspbian än den som rapporterades ge detta problem. Verkar vara ett fel som kommer och går med olika uppdateringar av systemet…

Share

Lessons learned

Efter några veckors drift har jag fått göra två modifieringar:

  1. LED-listerna är hyfsat självhäftande men inte helt. Man behöver komplettera med lim på en del ställen efter ett tag. Använd inte superlim, det smälter och förstör ledningsbanan.
  2. Kopplingarna jag köpte fungerar bra i den ända där jag inte matar ström utan bara kopplar mellan längderna. De tål däremot inte strömmen i den ända där den är ansluten utan blir brända och börjar glappa. Jag har därför lött på ledningar i den ändan i stället.
Share

The whole shebang

Rampen är klar och i drift! Precis som tidigare är det Byggstål i Karlstad som gjort själva rampen. Hårdvaran i övrigt består av en massa olika saker men huvudkomponenterna i det som lyser är:

LED-strip med 60 dioder per meter, WS2812B och IP65-klassning
LED-drivare, 5V/40A/200W
LED-strip connector cable

Till det kommer förstås en Raspberry Pi och ett reläkort. De finns i olika varianter men jag hade ett gäng med åtta reläer liggande typ:

5V 10A 8 Channel Relay Board Module for Arduino Raspberry Pi ARM AVR DSP PIC

IKEA fick släppa till apparatlåda och monteringsplatta:

Samla förvaringslåda
Aptitlig skärbräda

Som vanligt går det förstås åt lite sladd, kopplingar och annat vilket finns hos Kjell & Co.

Det är rätt bra spänningsfall på LED-listerna så jag drog en sladd från varje utgång på drivaren (tre stycken) och matade varje rad fördelat på dem. Kopplingar av typ Wago 221 gör det enkelt att koppla kablarna och med en bit dubbelhäftande tejp bakom sitter de snyggt(?) i rampen.

Tiden får utvisa vad växterna tycker om ljuset men LUX-mätaren i min (vattentäta) telefon säger i alla fall att det är ca 30-40% mer ljus på botten än med min tidigare version av LED-ramp.

Innan någon kommer och föreslår att man kan rita stödlinjer att klistra LED-listerna efter så att de sitter rakt vill jag informera om att det har jag gjort…

Share

Ljusstyrning

Själva styrningen av ljuset består av två saker. Dels en webbaserad frontend där man anger villkoren (vilka skrivs till databasen) och dels en backend som läser databasen och skickar rätt kommandon till servern som styr själva LED-stripen. I front-enden väljer man mellan automatiskt läge där man skapar ett schema för hur ljuset ska variera över dygnet och ett manuellt läge där man kan ange värdena för varje färg och intensitet. Jag har även lagt med en liten Kelvin till RGB-omvandlare för att enkelt kunna ange olika färgtemperaturer. Känsliga tittare varnas, det finns ingen avancerad layout på sidorna utan vi pratar ren HTML, tänk sent 90-tal. Men vill man slänga på en CSS som gör det lite snyggare är det förstås valfritt.

Först kommer front-enden. Den ska ligga i /var/www/cgi-bin och har man följt överkursen i Repetition av temperatur fungerar det direkt, annars får man gå tillbaka dit och fixa så att Apache pratar Python.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/usr/bin/python
 
# Import needed libraries
import mysql.connector
import cgi, cgitb
import sys
import math
 
# Initalize values
konvKelvin = 0
konvR = 0
konvG = 0
konvB = 0
 
# Open the connection to the database
mydb = mysql.connector.connect(
  host="localhost",
  user="username",
  passwd="password",
  database="aqua-light"
)
 
# Start output of HTML
print("Content-Type: text/html")
print()
print("<html><head><title>Aqua-Light för kar 3</title></head><body>")
 
# Check if something is passed to the script
form = cgi.FieldStorage()
 
# Check mode switch and shortcuts
if (form.getvalue('mode_select') == 'Automatiskt'):
    sql = "UPDATE config SET Value = 1 WHERE Name = 'automatic'"
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
if (form.getvalue('mode_select') == 'Manuellt'):
    sql = "UPDATE config SET Value = 0 WHERE Name = 'automatic'"
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
if (form.getvalue('max_select') == 'Max'):
    sql = "UPDATE config SET Value = 0 WHERE Name = 'automatic'"
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
    sql = "UPDATE manual SET R = 255, G = 255, B = 255, I = 255 WHERE ID = " + form.getvalue("ID")
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
if (form.getvalue('min_select') == 'Min'):
        sql = "UPDATE config SET Value = 0 WHERE Name = 'automatic'"
        mycursor = mydb.cursor()
        mycursor.execute(sql)
        mydb.commit()
        sql = "UPDATE manual SET R = 0, G = 0, B = 0, I = 0 WHERE ID = " + form.getvalue("ID")
        mycursor = mydb.cursor()
        mycursor.execute(sql)
        mydb.commit()
 
# Save new manual values
if (form.getvalue('save_manual') == 'Spara'):
    sql = "UPDATE manual SET R = " + form.getvalue('R')
    sql = sql + ", G = " + form.getvalue('G')
    sql = sql + ", B = " + form.getvalue('B')
    sql = sql + ", I = " + form.getvalue('I')
    sql = sql + " WHERE ID = " + form.getvalue('ID')
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
 
# Add new row for automatic values
if (form.getvalue('auto_addnew') == 'Skapa'):
    sql = "INSERT INTO basedata (H, M, S, R, G, B, I) VALUES ("
    sql = sql + form.getvalue('autoH')
    sql = sql + ", "+ form.getvalue('autoM')
    sql = sql + ", "+ form.getvalue('autoS')
    sql = sql + ", "+ form.getvalue('autoR')
    sql = sql + ", "+ form.getvalue('autoG')
    sql = sql + ", "+ form.getvalue('autoB')
    sql = sql + ", "+ form.getvalue('autoI')
    sql = sql + ")"
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
 
# Delete row for automatic values
if (form.getvalue('auto_del') == 'Ta bort'):
    sql = "DELETE FROM basedata WHERE ID=" + form.getvalue('ID')
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
 
# Change row for automatic values
if (form.getvalue('auto_change') == 'Modifiera'):
    sql = "UPDATE basedata SET"
    sql = sql + " H=" + form.getvalue('autoH')
    sql = sql + ", M="+ form.getvalue('autoM')
    sql = sql + ", S="+ form.getvalue('autoS')
    sql = sql + ", R="+ form.getvalue('autoR')
    sql = sql + ", G="+ form.getvalue('autoG')
    sql = sql + ", B="+ form.getvalue('autoB')
    sql = sql + ", I="+ form.getvalue('autoI')
    sql = sql + " WHERE ID=" + form.getvalue('ID')
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
 
# Fill the database
if (form.getvalue('fill_db') == 'Fyll databasen'):
    sql = "DELETE FROM automatic"
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    mydb.commit()
    Sec = 0
    R = 0
    G = 0
    B = 0
    I = 0
    sql = "SELECT * FROM basedata ORDER BY H, M, S"
    mycursor = mydb.cursor()
    mycursor.execute(sql)
    myresult = mycursor.fetchall()
    for result in myresult:
        targetSec = result[1] * 3600 + result[2] * 60 + result[3]
        deltaR = (result[4] - R) / (targetSec - Sec)
        deltaG = (result[5] - G) / (targetSec - Sec)
        deltaB = (result[6] - B) / (targetSec - Sec)
        deltaI = (result[7] - I) / (targetSec - Sec)
        for n in range(Sec, targetSec):
            sql = "INSERT INTO automatic (Sec, R, G, B, I) VALUES ("
            sql = sql + str(n)
            sql = sql + ", " + str(int(R))
            sql = sql + ", " + str(int(G))
            sql = sql + ", " + str(int(B))
            sql = sql + ", " + str(int(I)) + ")"
            mycursor = mydb.cursor()
            mycursor.execute(sql)
            mydb.commit()
            R = R + deltaR
            G = G + deltaG
            B = B + deltaB
            I = I + deltaI
        R = result[4]
        G = result[5]
        B = result[6]
        I = result[7]
        Sec = targetSec
    targetSec = 86400
    deltaR = (0 - R) / (targetSec - Sec)
    deltaG = (0 - G) / (targetSec - Sec)
    deltaB = (0 - B) / (targetSec - Sec)
    deltaI = (0 - I) / (targetSec - Sec)
    for n in range(Sec, 86400):
        sql = "INSERT INTO automatic (Sec, R, G, B, I) VALUES ("
        sql = sql + str(n)
        sql = sql + ", " + str(int(R))
        sql = sql + ", " + str(int(G))
        sql = sql + ", " + str(int(B))
        sql = sql + ", " + str(int(I)) + ")"
        mycursor = mydb.cursor()
        mycursor.execute(sql)
        mydb.commit()
        R = R + deltaR
        G = G + deltaG
        B = B + deltaB
        I = I + deltaI
 
# Konvert K to RGB
if (form.getvalue('save_konvert') == 'Konvertera'):
    konvKelvin = float(form.getvalue('konvKelvin'))
    if konvKelvin < 1000:
        konvKelvin = 1000
    if konvKelvin > 40000:
        konvKelvin = 40000
    tmpKelvin = konvKelvin / 100
    if tmpKelvin <= 66:
        konvR = 255
    else:
        tmpCalc = tmpKelvin - 60
        tmpCalc = 329.698727446 * (tmpCalc ** -0.1332047592)
        konvR = int(tmpCalc)
        if konvR < 0: konvR = 0
        elif konvR > 255: konvR = 255
    if tmpKelvin <= 66:
        tmpCalc = tmpKelvin
        tmpCalc = 99.4708025861 * math.log(tmpCalc) - 161.1195681661
        konvG = int(tmpCalc)
    else:
        tmpCalc = tmpKelvin - 60
        tmpCalc = 288.1221695283 * (tmpCalc ** -0.0755148492)
        konvG = int(tmpCalc)
    if konvG < 0: konvG = 0
    elif konvG > 255: konvG = 255
    if tmpKelvin >= 66:
        konvB = 255
    elif tmpKelvin <= 19:
        konvB = 0
    else:
        tmpCalc = tmpKelvin - 10
        tmpCalc = 138.5177312231 * math.log(tmpCalc) - 305.0447927307
        konvB = int(tmpCalc)
        if konvB < 0: konvB = 0
        elif konvB > 255: konvB = 255
 
# Show mode of operation
mycursor = mydb.cursor()
mycursor.execute("SELECT * FROM config WHERE Name='automatic'")
myresult = mycursor.fetchone()
print("<h1>Aktuellt läge: ")
if (int(myresult[2]) == 0):
    print("Manuell styrning")
    mode = 0
else:
    print("Automatiskt")
    mode = 1
print("</h1>")
 
# Fetch manual values
mycursor = mydb.cursor()
mycursor.execute("SELECT * FROM manual")
myresult = mycursor.fetchone()
manID = str(myresult[0])
manR = str(myresult[1])
manG = str(myresult[2])
manB = str(myresult[3])
manI = str(myresult[4])
 
# Selection of mode and shortcuts
print("<table><tr>")
strHTML = "<td><form action='light.py' method='post'><input type='submit' name='mode_select' value='"
if (mode == 0): strHTML = strHTML + "Automatiskt"
else: strHTML = strHTML + "Manuellt"
strHTML = strHTML + "'></form></td>"
print(strHTML)
print("<td><form action='light.py' method='post'><input type='hidden' name='ID' value='"+ manID + "'><input type='submit' name='max_select' value='Max'></form></td>")
print("<td><form action='light.py' method='post'><input type='hidden' name='ID' value='"+ manID + "'><input type='submit' name='min_select' value='Min'></form></td>")
print("</tr></table>")
 
# Manual values
print("<h2>Inställningar för manuellt läge:</h2>")
print("<form action='light.py' method='post'><input type='hidden' name='ID' value='{}'><table>".format(manID))
print("<tr><th>R</th><th>G</th><th>B</th><th>I</th><th></th></tr><tr>")
print("<td><input type='text' maxlength=3 size=3 name='R' value='{}'></td>".format(manR))
print("<td><input type='text' maxlength=3 size=3 name='G' value='{}'></td>".format(manG))
print("<td><input type='text' maxlength=3 size=3 name='B' value='{}'></td>".format(manB))
print("<td><input type='text' maxlength=3 size=3 name='I' value='{}'></td>".format(manI))
print("<td><input type='submit' name='save_manual' value='Spara'></td>")
print("</tr></table></form>")
 
# Scheme for automatic mode
print("<h2>Schema för automatiskt läge</h2>")
print("<table>")
print("<tr><th>H</th><th>M</th><th>S</th><th>R</th><th>G</th><th>B</th><th>I</th><th></th></tr>")
 
sql = "SELECT * FROM basedata ORDER BY H, M, S"
mycursor = mydb.cursor()
mycursor.execute(sql)
myresult = mycursor.fetchall()
for result in myresult:
    print("<form action='light.py' method='post'><tr><td><input type='hidden' name='ID' value='{}'><input type='text' size=2 maxlength=2 name='autoH' value='{}'></td><td><input type='text' size=2 maxlength=2 name='autoM' value='{}'></td><td><input type='text' size=2 maxlength=2 name='autoS' value='{}'></td><td><input type='text' size=3 maxlength=3 name='autoR' value='{}'></td><td><input type='text' size=3 maxlength=3 name='autoG' value='{}'></td><td><input type='text' size=3 maxlength=3 name='autoB' value='{}'></td><td><input type='text' size=3 maxlength=3 name='autoI' value='{}'></td><td><input type='submit' name='auto_change' value='Modifiera'><input type='submit' name='auto_del' value='Ta bort'></td></tr></form>".format(result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7]))
 
print("<form action='light.py' method='post'><tr><td><input type='text' size=2 maxlength=2 name='autoH'></td><td><input type='text' size=2 maxlength=2 name='autoM'></td><td><input type='text' size=2 maxlength=2 name='autoS'></td><td><input type='text' size=3 maxlength=3 name='autoR'></td><td><input type='text' size=3 maxlength=3 name='autoG'></td><td><input type='text' size=3 maxlength=3 name='autoB'></td><td><input type='text' size=3 maxlength=3 name='autoI'></td><td><input type='submit' name='auto_addnew' value='Skapa'></td></tr></form>")
print("</table>")
 
 
print("<form action='light.py' method='post'><input type='submit' name='fill_db' value='Fyll databasen'></form>")
 
# Kelvin to RGB converter
print("<h2>Kelvin till RGB</h2>")
print("<form action='light.py' method='post'><table>")
print("<tr><th>Kelvin</th><th>R</th><th>G</th><th>B</th><th></th></tr>")
print("<tr><td><input type='text' name='konvKelvin' value='{}' maxlength=7 size=7></td><td>{}</td><td>{}</td><td>{}</td><td><input type='submit' name='save_konvert' value='Konvertera'></td></tr>".format(konvKelvin, konvR, konvG, konvB))
print("</table></form>")
 
print("</body></html>")
mydb.close

Sedan kommer back-end, betydligt kortare. Den kan man placera var man vill, jag har min i /usr/local/bin/aquacontrol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/python
 
# Import needed libraries
import mysql.connector
import socket
import time
import datetime
 
# Initalize values
NUM_LEDS = 588;
LED_TYPE = 3;
 
# Open the connection to the database
mydb = mysql.connector.connect(
  host="localhost",
  user="username",
  passwd="password",
  database="aqua-light"
)
 
# Initialize the LED server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 9999))
message = "setup 1," + str(NUM_LEDS) + "," + str(LED_TYPE) + ";init;"
s.send(message.encode())
 
# Main loop
while True:
    # Check running mode in database
    mycursor = mydb.cursor()
    mycursor.execute("SELECT * FROM config WHERE Name='automatic'")
    myresult = mycursor.fetchone()
    mydb.commit()
    if (int(myresult[2]) == 0):
        # manual mode
        mycursor = mydb.cursor()
        mycursor.execute("SELECT * FROM manual")
        myresult = mycursor.fetchone()
        mydb.commit()
        message = "fill 1,"+format(myresult[1], "02x")+format(myresult[2], "02x")+format(myresult[3], "02x")+";brightness 1,"+str(myresult[4])+";render;"
        s.send(message.encode())
    else:
        # automatic mode
        now = datetime.datetime.now()
        midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
        seconds = (now - midnight).seconds 
        mycursor = mydb.cursor()
        mycursor.execute("SELECT * FROM automatic WHERE Sec=" + str(seconds))
        myresult = mycursor.fetchone()
        mydb.commit()  
        message = "fill 1,"+format(myresult[2], "02x")+format(myresult[3], "02x")+format(myresult[4], "02x")+";brightness 1,"+str(myresult[5])+";render;"
        s.send(message.encode())
 
    time.sleep(1)

För att back-end ska starta automatiskt behöver man skapa filen aqua-light.service i /lib/systemd/system med följande innehåll:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Aqua-Light
After=multi-user.target
  
[Service]
Type=idle
ExecStart=/usr/local/bin/aquacontrol/aqua-light.py
  
[Install]
WantedBy=multi-user.target

Kör sedan

1
2
3
4
sudo chmod +x /usr/local/bin/aquacontrol/aqua-light.py
sudo chmod 644 /lib/systemd/system/aqua-light.service
sudo systemctl daemon-reload
sudo systemctl enable aqua-light.service

så ska det starta automatiskt vid omstart. Att fylla tabellen med de 86400 raderna för den automatiska styrningen tar närmare en halvtimma på min Raspberry Pi. För att inte Apache ska tröttna på att vänta har jag lagt till

1
TimeOut 2400

i /etc/apache2/sites-available/000-default.conf. När man surfar till http://adress_till_din_paj/cgi-bin/light.py ser det ut ungefär som följer:

I mitt fall ovan börjar det hända saker 11:00:00 då det fram till 11:10:00 tänds ett månljus som är tänt till 12:00:00. Då byter det till en soluppgång fram till 12:10:00 som sedan tonar upp till full styrka fram till 13:00:00. Det är tänt fram till 21:00:00 då en solnedgång börjar och håller på till 21:50:00. Då byter den till ett månljus under tio minuter fram till 22:00:00. Detta är tänt till 22:50:00 då det börjar tona ut till helt släckt 23:00:00.

Share

Grundläggande ljusstyrning

För att enkelt kunna styra LED-listerna från olika miljöer har jag valt att använda en server som man kan skicka kommandon till. Den finns på https://github.com/tom-2015/rpi-ws2812-server och installeras enligt följande.

Först måste vi slå av ljudfunktionerna i vår paj då det är den PWM-utgången (normalt GPIO 18) som används. Lägg till

1
blacklist snd_bcm2835

i /etc/modprobe.d/snd-blacklist.conf (skapa filen om den inte finns) och kommentera bort

1
2
# Enable audio (loads snd_bcm2835)
# dtparam=audio=on

i /boot/config.txt. Starta sedan om pajen.

Nu kan man installera servern med

1
2
3
4
5
6
7
sudo apt-get update
sudo apt-get install git
cd /usr/local/bin
cd rpi-ws2812-server
sudo make NO_JPEG=1 NO_PNG=1
sudo chmod +x ws2812svr

För att servern ska starta automatiskt får man skapa filen ws2812svr.service i /lib/systemd/system med följande innehåll:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=ws2812svr
After=multi-user.target
 
[Service]
Type=idle
ExecStart=/usr/local/bin/rpi-ws2812-server/ws2812svr -tcp 9999
 
[Install]
WantedBy=multi-user.target

Kör sedan

1
2
3
sudo chmod 644 /lib/systemd/system/ws2812svr.service
sudo systemctl daemon-reload
sudo systemctl enable ws2812svr.service

så ska den starta automatiskt vid omstart. För att testa skriver man

1
nc 127.0.0.1 9999

och kan sedan skicka kommandon till servern.

Share

Databas

Den nya ljusstyrningen blir lite mer avancerad (hoppas jag) än den gamla och jag tänker ha en databas i botten. Jag vill kunna styra ljuset manuellt via en webbsida och då är det behändigt att låta skriptet som skickar ut styrsignalerna hämta sina instruktioner från en databas som styrsidan uppdaterar. Då krävs det lite mer saker som ska installeras:

1
2
3
sudo apt-get install mysql-server phpmyadmin python3-pip
sudo python -m pip install mysql-connector
sudo python -m pip install mysql-connector-python-rf

När installationerna är klara kör man

1
2
3
4
5
6
sudo mysql --user=root mysql
 
CREATE USER 'username'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'username'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
exit

för att skapa ditt eget admin-konto i MySQL (byt ut username och password). Sedan kan man surfa till din_pajs_ip_adress/phpmyadmin för att hantera databasen. Jag börjar med att skapa en ny användare som heter aqua-light och anger att det ska skapas en databas med samma namn där man har fulla behörigheter.

I databasen skapar jag fyra tabeller:

  1. config
    ID av typ INT, autoincrement, primary index
    Name av typ VARCHAR(255)
    Value av typ VARCHAR(255)
  2. manual
    ID av typ INT, autoincrement, primary index
    R av typ INT
    G av typ INT
    B av typ INT
    I av typ INT
  3. automatic
    ID av typ INT, autoincrement, primary index
    Sec av typ INT
    R av typ INT
    G av typ INT
    B av typ INT
    I av typ INT
  4. basedata
    ID av typ INT, autoincrement, primary index
    H av typ INT
    M av typ INT
    S av typ INT
    R av typ INT
    G av typ INT
    B av typ INT
    I av typ INT

När jag ändå är inne i phpMyAdmin skapar jag en post i config med Name automatic och Value 0. Jag lägger även till en rad i manual med bara nollor.

Tanken är att värdet automatic i config-tabellen ska ange om ljusrampen ska köras i manuellt eller automatiskt läge. I manuellt läge läser den värdena för rött, grönt och blått samt ljusstyrkan från tabellen manual. I automatiskt läge slår den upp vilka värden som ska sättas beroende på hur många sekunder det har gått sedan midnatt. Tabellen manual kommer alltså att ha en rad och tabellen automatic kommer att ha 86400 rader. Med lite webbsidor är det tänkt att man ska kunna växla mellan manuellt och automatiskt läge samt fylla tabellen automatic beroende på hur man vill att ljuset ska växla under dagen. Tabellen basedata innehåller reglerna man har satt upp för det automatiska ljuset.

Share

Repetition av temperatur

Hur man mäter temperatur och hur jag använder det för att styra ett relä som slår av och på värmaren är beskrivet på andra ställen i bloggen men här kommer en sammanställning och uppdatering.

Jag aktiverade 1-wire i raspi-config under uppsättningen av min paj och har anslutit min DS18B20 enligt anvisningarna från Cambridge University. För att den ska hittas och användas behöver två rader läggas till i /etc/modules:

1
2
w1-gpio
w1-therm

För att kunna presentera data och statistik installerade jag webbservern Apache tillsammans med MRTG som kan presentera historiken grafiskt:

1
sudo apt-get install apache2 mrtg

För att MRTG ska få data från temperatur-sensorn krävs det en liten fil som jag kallar mrtg.py. Man får byta adressen till filen w1_slave till aktuell adress, varje sensor har sin egen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python
from time import strftime
 
tfile = open("/sys/bus/w1/devices/28-011553897dff/w1_slave", 'r')
text = tfile.read()
tfile.close()
 
secondline = text.split("\n")[1]
temperaturedata = secondline.split(" ")[9]
temperature = float(temperaturedata[2:])
temperature = int(temperature / 10)
 
print(str(temperature))
print("0")
print(strftime("%Y-%m-%d %H:%M:%S"))
print("Temperatur i kar 3")

Man behöver även byta ut /etc/mrtg.cfg mot en som hämtar data på rätt ställe och visar det som man vill:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
######################################################################
# Multi Router Traffic Grapher -- Sample Configuration File
######################################################################
# This file is for use with mrtg-2.5.4c
 
# Global configuration
WorkDir: /var/www/html/mrtg
WriteExpires: Yes
RunAsDaemon: Yes
Interval: 5
 
Title[^]: Temperaturstatistik for
 
Title[kar3]: kar 3
PageTop[kar3]: &lt;h1>Temperaturen i kar 3&lt;/h1>
Target[kar3]: `/usr/local/bin/aquacontrol/mrtg.py`
MaxBytes[kar3]: 4000
Options[kar3]: growright,gauge,expscale
Factor[kar3]: 0.01
YLegend[kar3]: Temperatur
YTicsFactor[kar3]: 0.01
ShortLegend[kar3]: C
Legend1[kar3]: Temperatur i grader celcius
Legend2[kar3]:
Legend3[kar3]: Maximal 5 Minute Incoming Traffic
Legend4[kar3]: Maximal 5 Minute Outgoing Traffic
LegendI[kar3]:  Temp:
LegendO[kar3]:

För att MRTG ska starta automatiskt behövs ett startscript, skapa filen mrtg i  /etc/init.d med följande innehåll:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#! /bin/sh
### BEGIN INIT INFO
# Provides:          mrtg
# Required-Start:   
# Required-Stop:    
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: mrtg init script
# Description:       This file is used to start, stop, restart,
#                    and determined status of the mrtg daemon.
# Author:            iceflatline &lt;iceflatline@gmail.com>
### END INIT INFO
  
### START OF SCRIPT
set -e
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="mrtg"
NAME=mrtg
DAEMON=/usr/bin/$NAME
DAEMON_ARGS="/etc/mrtg.cfg"
PIDFILE=/etc/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
  
# Exit if the mrtg package is not installed
[ -x "$DAEMON" ] || exit 0
  
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
  
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
  
# Function that starts the mrtg daemon
start()
{
    env LANG=C start-stop-daemon --start --quiet \
    --exec $DAEMON -- $DAEMON_ARGS
}
  
# Function that stops the mrtg daemon
stop()
{
    start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \
    --pidfile $PIDFILE
}
  
case "$1" in
  start)
    log_daemon_msg "Starting $DESC"
    start
    case "$?" in
        0) log_end_msg 0 ;;
        1) log_end_msg 1 ;;
    esac
    ;;
  stop)
    log_daemon_msg "Stopping $DESC"
    stop
    case "$?" in
        0) log_end_msg 0 ;;
        1) log_end_msg 1 ;;
    esac
    ;;
  restart|force-reload)
    log_daemon_msg "Restarting $DESC"
    stop
    case "$?" in
      0|1)
        start
        case "$?" in
            0) log_end_msg 0 ;;
            1) log_end_msg 1 ;;
        esac
        ;;
    esac
    ;;
    status)
    status_of_proc "$DAEMON" "$NAME" 
    ;;
  *)
    echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}"
    ;;
esac
exit 0
### END OF SCRIPT

Kör sedan

1
2
3
4
sudo chmod +x /usr/local/bin/aquacontrol/mrtg.py
sudo chmod +x /etc/init.d/mrtg
sudo systemctl daemon-reload
sudo update-rc.d mrtg defaults

så ska MRTG-biten vara klar. Dags för lite styrning! Följande Python-skript läser av temperaturen varje minut, kollar mot gränsvärden, slår av och på värmaren och larmar via epost om det avviker för mycket. Ange dina egna epostuppgifter och förstås rätt adress till w1_slave-filen som vanligt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import datetime
import syslog
import smtplib
from time import strftime
 
syslog.openlog('aqua-temp')
syslog.syslog('Aqua-temp started')
 
# Definiera temperaturgranser
alarmLow = 24
alarmHigh = 27
tempLow = 24.9
tempHigh = 25.1
 
# Klasser
         
# Variabler
heatOn = 0
heatOff = 1
heater = 22
tempLarm = False
heatStatus = False
 
# Funktioner
def sendMail(subject, msg):
    fromaddr = 'me self <me@self.com>'
    toaddr  = 'me self <me@self.com>'
    message = ('From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s' % (fromaddr, toaddr, subject, msg))
    username = 'username'
    password = 'password'
 
    server = smtplib.SMTP('smtp.gmail.com:587')
    server.starttls()
    server.login(username,password)
    server.sendmail(fromaddr, toaddr, message)
    server.quit()
    syslog.syslog('Mail sent')
 
def writeTemp(temp):
    tfile = open('/var/www/html/temp.html', 'w')
    tfile.write('<html><head>')
    tfile.write('<title>Info för kar 3</title>')
    tfile.write('<META HTTP-EQUIV="refresh" CONTENT="60">')
    tfile.write('</head><body>')
    tfile.write(strftime("%Y-%m-%d %H:%M:%S"))
    tfile.write('<h1>Temperaturen i kar 3 är '+str(temp)+'°C</h1>')
    tfile.write('<h2>Värmaren är ')
    if heatStatus: tfile.write('på</h2>')
    else: tfile.write('av</h2>')
    tfile.write('<p><a href="/mrtg/kar3.html">Historik</a>')
    tfile.write('</body></html>')
    tfile.close()
     
def checkTemp():
    global tempLarm
    global heatStatus
     
    tfile = open("/sys/bus/w1/devices/28-011553897dff/w1_slave", 'r')
    text = tfile.read()
    tfile.close()
 
    secondline = text.split("\n")[1]
    temperaturedata = secondline.split(" ")[9]
    temperature = float(temperaturedata[2:])
    temperature = float(int((temperature/100)+0.5))/10
 
    if (temperature < alarmLow or temperature > alarmHigh):
        if not tempLarm:
            syslog.syslog('Temp is wrong')
            sendMail("Temperaturen ar felaktig", "Temperaturen i kar 3 ar nu "+str(temperature))
            tempLarm = True
    else:
        if tempLarm:
            syslog.syslog('Temp is normal again')
            sendMail("Temperaturen ar normal", "Temperaturen i kar 3 ar nu "+str(temperature))
            tempLarm = False
 
    if (temperature <= tempLow):
        if not heatStatus:
            syslog.syslog('Temp is '+str(temperature)+'. Heater switched on')
            GPIO.output(heater, heatOn)
            heatStatus = True
    if (temperature >= tempHigh):
        if heatStatus:
            syslog.syslog('Temp is '+str(temperature)+'. Heater switched off')
            GPIO.output(heater, heatOff)
            heatStatus = False
     
    writeTemp(temperature)
     
# Initiera GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(heater, GPIO.OUT)
GPIO.output(heater, heatOff)
syslog.syslog('GPIO initialized, heater switched off')
 
# Huvudloop
while True:
    checkTemp()
    time.sleep(60)

För att skriptet ska starta automatiskt behöver man skapa filen aqua-temp.service i /lib/systemd/system med följande innehåll:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Aqua-Temp
After=multi-user.target
 
[Service]
Type=idle
ExecStart=/usr/local/bin/aquacontrol/aqua-temp.py
 
[Install]
WantedBy=multi-user.target

Kör sedan

1
2
3
4
sudo chmod +x /usr/local/bin/aquacontrol/aqua-temp.py
sudo chmod 644 /lib/systemd/system/aqua-temp.service
sudo systemctl daemon-reload
sudo systemctl enable aqua-temp.service

så ska det starta automatiskt vid omstart. Det är praktiskt att få loggen som skapas till en egen fil. Skapa filen /etc/rsyslog.d/aqua-temp.conf med innehållet

1
if $programname == 'aqua-temp' then /var/log/aqua-temp.log

Överkurs

Ibland kan det vara fiffigt att kunna hämta ut data i JSON-format. Jag har därför följande lilla fil, vid namn temp-xml.py, placerad i /var/www/cgi-bin. Ja, du behöver justera sökvägen till w1_slave…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python
 
tfile = open("/sys/bus/w1/devices/28-011553897dff/w1_slave", 'r')
text = tfile.read()
tfile.close()
 
secondline = text.split("\n")[1]
temperaturedata = secondline.split(" ")[9]
temperature = float(temperaturedata[2:])
temperature = float(int((temperature/100)+0.5))/10
 
print ("Content-type: text/html\n")
print ('<?xml version="1.0" encoding="UTF-8"?>')
print ('<rss version="2.0">')
print ('  <item>')
print ('    <temp3>'+str(temperature)+'</temp3>')
print ('  </item>')
print ('</rss>')

För att Apache ska gå med på att köra python-script, och begripa vad cgi-bin är, behöver dessa rader läggas till i /etc/apache2/sites-available/000-default.conf efter DocumentRoot-raden

1
2
3
4
5
6
7
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
&lt;Directory "/var/www/cgi-bin">
        AllowOverride None
        Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
        Order allow,deny
        Allow from all
&lt;/Directory>

Kör sedan

1
2
3
sudo chmod +x /var/www/cgi-bin/temp-xml.py
sudo a2enmod cgi
sudo service apache2 restart

så ska det hela rulla.

Share

Python 3

I Raspbian Stretch installeras både Python 2 och Python 3 men Python 2 är default. Jag vill köra trean som standard vilket kräver lite pill.

1
2
sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.5 2

Verifiera att rätt version används med

1
python --version

Om det inte är trean som körs kan man tvinga vilken version man vill använda med

1
sudo update-alternatives --config python

Allt är inte förinstallerat för python3 så man får lägga till lite.

1
sudo apt-get install python3-rpi.gpio
Share

Nattlig omstart

Om man programmerar så dåligt som jag gör blir det lätt minnesläckor och annat som med tiden får pajen att stanna. Jag brukar lösa det genom att schemalägga en nattlig omstart. Kör

1
sudo crontab -e

och lägg in

1
0 4 * * * /sbin/shutdown -r now

sist så startar den om klockan fyra varje natt.

Share

LED-ramp TNG

För den icke sci-fi-bevandrade står TNG för The Next Generation och det är precis det jag dragit igång. Vi stuvar om lite bland karen hemma och kastar in ett nytt 540-liters vilket kräver nya ramper och ny teknik. Jag behåller Raspberry Pi som styrning men istället för reläkontrollerade lysdioder använder jag adresserbara LED-strips. Ett par beställningar är gjorda på eBay och i väntan på leverans kan man sätta upp den Raspberry Pi som ska styra det hela.

Raspbian Stretch Lite är hemtaget och skrivet till SD-kortet med hjälp av Etcher. Om man monterar SD-kortet igen på datorn efter att ha skrivit till det kommer man åt en partition som heter boot. Där kan man skapa en tom fil som heter ssh (ska fungera med ssh.txt också men det är oprövat) och en fil som heter wpa_supplicant.conf med följande innehåll:

1
2
3
4
5
6
7
8
9
10
country=SE
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
  
network={
ssid="your_real_wifi_ssid"
scan_ssid=1
psk="your_real_password"
key_mgmt=WPA-PSK
}

Du får förstås skriva in dina egna SSID och PSK-uppgifter men sedan är det bara att sätta SD-kortet i pajen, slå på strömmen och logga in via SSH. Inget behov av tangentbord och skärm med andra ord! Filerna som man skapade gör att SSH är aktiverat och att den automatiskt ansluter till WiFi.

Som vanligt börjar jag med att köra

1
sudo raspi-config

för att ställa in diverse parametrar. Eftersom jag planerar att köra min vanliga temperaturstyrning slår jag bland annat på 1-wire under Interfacing options. Filsystemet utökas som vanligt under Advanced options och sedan är det dags för en omstart innan jag uppdaterar systemet med

1
2
sudo apt-get update
sudo apt-get upgrade

Sedan är det bara resten kvar!

Share