Complete README, Blocked queries graph, Lightsoff mode
This commit is contained in:
Nikopol 2024-12-11 22:52:36 +04:00
parent 841e320fe8
commit 47824edc51
8 changed files with 244 additions and 26 deletions

140
README.md
View File

@ -1,15 +1,145 @@
# BlockyGrapher
Query grapher for Blocky DNS Server
# BlockyGrapher v0.9b
![Example setup using this software](./images/displaydemo.jpg)
Physical display query grapher for Blocky DNS Server.
I2C SSD1306 Displays with resolution 128x64 and 128x32 are supported.
Note that this software in beta, and akward bugs may occur.
## Preparation
### Prometheus Metrics endpoint
Open your Blocky DNS Server configuration. Include HTTP server port and enable Prometheus metrics endpoint.
```
ports:
dns: 53
http: 4500
prometheus:
enable: true
path: /metrics
```
Restart server. Open your HTTP port in browser and head into metrics. You shoud see something like this:
![Prometheus Metrics endpoint](./images/endpoint.png)
You should not open HTTP port to the public (Unless you know what you are doing). Open your config again and edit the ports:
```
ports:
dns: 53
http: 127.0.0.1:4500
```
Restart server again.
### Display Installation
Both supported SSD1306 displays has 4 I2C pins that we need to connect - VCC, GND, SCL/SCK, SDA.
![SSD1306 128x64/128x32 I2C Displays](./images/ssd1306.jpg)
In this example we will look at Orange PI Zero 3.
![Orange PI Zero 3 pinout](./images/zero3pinout.png)
We need I2C pins here. We also need 5V (Which is VCC) pin and GND. Connect these to the display.
![Connection](./images/zero3pinoutcut.png)
### Enabling I2C support in SoC Board settings
On most SoC Boards you need to enable I2C devices support.
In this example, we have Orange PI4 LTS with Armbian OS.
We need to open armbian configuration utility `sudo armbian-config` and proceed to System > Hardware.
Note that Orange PI4 LTS has RK3399 SoC. We need to select RK3399 I2C entries. Apply the changes and reboot.
![Armbian Config GUI](./images/rk3399i2c.png)
In some cases GUI Hardware configuration tool may not be available (Like Orange PI Zero 3 with DietPI OS). In that case you need to edit startup environment file in `/boot` directory.
```bash
~$ nano /boot/dietpiEnv.txt
rootdev=UUID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
rootfstype=ext4
consoleargs=console=ttyS0,115200 console=tty1
usbstoragequirks=
extraargs=net.ifnames=0
docker_optimizations=off
overlay_path=allwinner
overlay_prefix=sun50i-h616
# We need to insert hardware I2C name in overlays. In case of Orange PI Zero 3 this is i2c3.
overlays=i2c3
user_overlays=
```
Save and reboot.
## Install python packages
Debian-based:
```bash
# install system packages
apt update
apt install i2c-tools python3-dev python3-pip python3-numpy libfreetype6-dev libjpeg-dev build-essential
# Give user permission to use i2c interface (Replace user with your name). Remember that you need re-login to user after this (You can reboot too if it didn't work for some reason).
# Give user permission to use i2c interface (Replace user with your username). Remember that you need re-login to user after this (You can reboot too if it didn't work for some reason).
usermod -a -G i2c user
# install python packages
pip install -r requirements.txt --break-system-packages
```
## Finding right port
## Finding right port/address and starting the program
*In most cases pysical I2C port can differ from system I2C port binding!* It's better to double-check the ports before proceeding.
`i2cdetect -l` can help to find port you need to use.
Schematics for your adapter/board can also help.
```
~$ sudo i2cdetect -l
i2c-0 i2c mv64xxx_i2c adapter I2C adapter
i2c-1 i2c DesignWare HDMI I2C adapter
i2c-2 i2c mv64xxx_i2c adapter I2C adapter
```
In this example we can see that ether port 0 or port 2 is needed. Let's pick port 2.
SSD1306 128x64 and 128x32 displays usually have 0x3C/0x30 address.
```
~$ sudo i2cdetect -y 2
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
```
We found our display! Now we can start the grapher.
```
python3 dnsmonitor.py --i2c-port 2 --i2c-address 0x3C
```
Note that during first successful startup, you will need to edit newly created `config.ini` configuration file.
```
[F] [16:54:12] config.ini does not exist. Please edit newly created file and start the program.
```
You can see all available startup options using `python3 dnsmonitor.py -h`.
## Cofiguration
Unedited `config.ini` will look like this:
```
[source]
url = http://127.0.0.1:4500/metrics
[appearance]
points = 14
pointsDistance = 10
dots = True
[lightsoff]
enabled = False
start = 23
stop = 06
```
### `[source]`
`url` - Contains Prometeus endpoint URL.
### `[appearence]`
`points` - Amount of points on the graph. Each point pepresents each hour passed. Note that points can can render out of display bounds.
`pointsDistance` - Distance between the points.
`dots` - Renders small dots on each hour. Improves graph readability.
### `[lightsoff]`
`enabled` - Enables Lights OFF mode. During set up period in `start` and `stop` values, turns display off completely. May be useful during the nighttime, where *you are sleeping and not paying attention at all* (OLED displays are VERY bright during night as well).
`start` - Hour, when Lights OFF mode starts.
`stop` - Hour, when Lights OFF mode ends.
## Install as service (Autorun)
### TODO
## Credits
[Terminus Font](https://files.ax86.net/terminus-ttf/) - Font used by numbers in graph.
[Font Awesome V4](https://fontawesome.com/v4/) - Icons
[luma.oled](https://github.com/rm-hull/luma.oled) - SSD1306 Display Driver

View File

@ -71,12 +71,13 @@ def get_device(actual_args=None):
def sigterm_handler(signum, frame):
sys.exit(0)
def scalepoints(points):
def scalepoints(points, blpoints):
if device.height == 32:
h = 31
else:
h = 46
sclpoint = [0] * len(points)
blsclpoint = [0] * len(blpoints)
scl = 1
maxpnt = 0
i = 0
@ -90,10 +91,14 @@ def scalepoints(points):
while i <= len(points) -1:
sclpoint[i] = int(points[i] / scl)
i += 1
i = 0
while i <= len(blpoints) -1:
blsclpoint[i] = int(blpoints[i] / scl)
i += 1
maxcanv = h * scl
return sclpoint, maxcanv
return sclpoint, blsclpoint, maxcanv
def movepoints(points, insertq, restarts, insertr):
def movepoints(points, insertq, blpoints, insertbl, restarts, insertr):
i = 1
tmp = points[0]
tmp2 = 0
@ -118,7 +123,21 @@ def movepoints(points, insertq, restarts, insertr):
restarts[i] = tmp3
tmp3 = tmp4
i += 1
return points, restarts
i = 1
tmp = blpoints[0]
tmp2 = 0
blpoints[0] = insertbl
while i < len(blpoints):
if i == len(blpoints) - 1:
blpoints[i] = tmp
else:
tmp2 = blpoints[i]
blpoints[i] = tmp
tmp = tmp2
i += 1
i = 1
return points, blpoints, restarts
def extractor(prev, flaunch, url):
try:
@ -141,7 +160,7 @@ def extractor(prev, flaunch, url):
else:
return 0, -1
except:
cliout("Extraction failed.", 2)
cliout("Extraction (Q) failed.", 2)
if flaunch == True:
return -1
else:
@ -151,7 +170,38 @@ def extractor(prev, flaunch, url):
else:
return count, count - prev
def drawout(sclpo, maxcanv, failture, restart, distance, dots):
def blextractor(prev, flaunch, url):
try:
data = str(urllib.request.urlopen(url).read()).split("\\n")
count = 0
i = 0
while data[i] != "# TYPE blocky_response_total counter":
i += 1
i += 1
while True:
if "blocky_response_total{reason=\"CACHED\",response_code=\"NOERROR\",response_type=\"CACHED\"}" in data[i]:
break
else:
count += int(re.sub('blocky_response_total{.*?} ', '', data[i]))
i += 1
except urllib.error.URLError as e:
cliout(f"Connection to server failed: {e.reason}", 2)
if flaunch == True:
return -1
else:
return 0, -1
except:
cliout("Extraction (BL) failed.", 2)
if flaunch == True:
return -1
else:
return 0, -1
if flaunch == True:
return count
else:
return count, count - prev
def drawout(sclpo, sclbl, maxcanv, failture, restart, distance, dots):
match device.height:
case 32:
# todo: backport
@ -173,7 +223,23 @@ def drawout(sclpo, maxcanv, failture, restart, distance, dots):
draw.text((device.width - 30, 0), "\uf021", font=icons, fill="white")
case 64:
with canvas(device) as draw:
# Graph
# Blocked (solid) Graph
currpoint = 1
targetpx = 128 - distance
while currpoint < len(sclbl):
if (sclbl[currpoint - 1] < sclbl[currpoint]):
draw.polygon([(targetpx + distance, 47 - sclbl[currpoint - 1]), (targetpx, 47 - sclbl[currpoint]), (targetpx, 47 - sclbl[currpoint - 1])], fill="white")
#cliout(f"RectDBG (less): X1: {targetpx}, Y1: {47 - sclbl[currpoint - 1]}, X2:{targetpx + distance}, Y2: 47",4)
draw.rectangle((targetpx, 47 - sclbl[currpoint - 1], targetpx + distance, 47), fill="white")
else:
draw.polygon([(targetpx + distance, 47 - sclbl[currpoint - 1]), (targetpx + distance, 47 - sclbl[currpoint]), (targetpx, 47 - sclbl[currpoint])], fill="white")
#cliout(f"RectDBG (more): X1: {targetpx}, Y1: {47 - sclbl[currpoint]}, X1: {targetpx + distance}, Y2: 47", 4)
draw.rectangle((targetpx, 47 - sclbl[currpoint], targetpx + distance, 47), fill="white")
currpoint += 1
targetpx -= distance
# Main Graph
currpoint = 1
targetpx = 128 - distance
while currpoint < len(sclpo):
@ -201,16 +267,17 @@ def drawout(sclpo, maxcanv, failture, restart, distance, dots):
with canvas(device) as draw:
draw.text((0,0), "Display not supported", font=smafont, fill="white")
def getdata(oldtotal, url):
def getdata(oldtotal, oldbltotal, url):
total, query = extractor(oldtotal, False, url)
bltotal, blquery = blextractor(oldbltotal, False, url)
# Server lifetime counter | Difference | Err? | Restarted?
if query == -1:
cliout("Extractor finished with error.", 1)
return oldtotal, 0, True, False
return oldtotal, 0, bltotal, 0, True, False
if total < oldtotal:
cliout("Server restart was detected.", 1)
return total, 0, False, True
return total, query, False, False
return total, 0, bltotal, 0, False, True
return total, query, bltotal, blquery, False, False
def cliout(data, code):
time = datetime.datetime.now().strftime("%H:%M:%S")
@ -223,11 +290,13 @@ def cliout(data, code):
codes = "E"
case 3:
codes = "F"
case 4:
codes = "V"
print(f"[{codes}] [{time}] {data}")
def main():
cliout("Blocky Graph Monitor for OLED v0.8b", 0)
cliout("(C) Nikopol 2024", 0)
cliout("Blocky Graph Monitor for OLED v0.9b", 0)
cliout("Nikopol 2024", 0)
cliout("Init...", 0)
# vars spagetti
@ -241,7 +310,10 @@ def main():
restart = False
rstate = False
dots = False
device.contrast(0)
lightsoff = False
start = 22
stop = 6
device.contrast(1)
# Load config
@ -249,12 +321,12 @@ def main():
filedata = open("config.ini").readlines()
except FileNotFoundError:
f = open("config.ini", "x")
f.write("[source]\nurl = http://127.0.0.1:4500/metrics\n[appearance]\npoints = 14\npointsDistance = 10")
f.write("[source]\nurl = http://127.0.0.1:4500/metrics\n[appearance]\npoints = 14\npointsDistance = 10\ndots = True\n[lightsoff]\nenabled = False\nstart = 23\nstop = 06")
f.close()
cliout("config.ini does not exist. Please edit newly created file and start the program.", 3)
exit()
except:
cliout("Failed to read config file. Are permissions correct?", 3)
cliout("Failed to read/write config file. Are permissions correct?", 3)
exit()
while i < len(filedata):
if "url = " in filedata[i]:
@ -265,13 +337,21 @@ def main():
distance = int(filedata[i][17:])
if "dots = True" in filedata[i]:
dots = True
if "enabled = True" in filedata[i]:
lightsoff = True
if "start = " in filedata[i]:
start = int(filedata[i][8:])
if "stop = " in filedata[i]:
stop = int(filedata[i][7:])
i += 1
i = 0
cliout("Config load ok", 0)
pointsmap = [0] * pointscount
blockedmap = [0] * len(pointsmap)
restartmap = [True] * len(pointsmap)
total = extractor(0, True, url)
bltotal = blextractor(0, True, url)
# Show fail on boot time if occurs.
if total == -1:
@ -281,21 +361,24 @@ def main():
now = datetime.datetime.now()
today_time = now.strftime("%H:%M")
hrs = now.strftime("%H")
#cliout(f"start is {start}", 1)
if today_time != today_last_time:
#if 1:
today_last_time = today_time
if hrs != last_hrs:
#if 1:
if (int(hrs) < stop and int(hrs) >= start and lightsoff == True):
cliout(f"Lightsoff is enabled and within time range. Screen will be black this hour.", 0)
last_hrs = hrs
# Grab fresh data
cliout("Extracting data...", 0)
total, query, failture, rstate = getdata(total, url)
total, query, bltotal, blquery, failture, rstate = getdata(total, bltotal, url)
#print(total)
#print(query)
# Now write it to (raw) array and move older data
pointsmap, restartmap = movepoints(pointsmap, query, restartmap, rstate)
pointsmap, blockedmap, restartmap = movepoints(pointsmap, query, blockedmap, blquery, restartmap, rstate)
# ...and scale it to screen size
sclpo, maxcanv = scalepoints(pointsmap)
sclpo, sclbl, maxcanv = scalepoints(pointsmap, blockedmap)
# also write restart map
restart = False
while i < len(restartmap):
@ -303,8 +386,13 @@ def main():
restart = True
i += 1
i = 0
cliout(f"Done. Current: {query}", 0)
drawout(sclpo, maxcanv, failture, restart, distance, dots)
cliout(f"Done. Current: {query}, Blocked: {blquery}", 0)
if (int(hrs) < stop and int(hrs) >= start and lightsoff == True):
with canvas(device) as draw:
# :troll:
hrs = hrs
else:
drawout(sclpo, sclbl, maxcanv, failture, restart, distance, dots)
time.sleep(1)

BIN
images/displaydemo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
images/endpoint.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
images/rk3399i2c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
images/ssd1306.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

BIN
images/zero3pinout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
images/zero3pinoutcut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB