diff --git a/README.md b/README.md index b238ac6..d69d022 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dnsmonitor.py b/dnsmonitor.py index d2fc0d2..076a100 100644 --- a/dnsmonitor.py +++ b/dnsmonitor.py @@ -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) diff --git a/images/displaydemo.jpg b/images/displaydemo.jpg new file mode 100644 index 0000000..07917be Binary files /dev/null and b/images/displaydemo.jpg differ diff --git a/images/endpoint.png b/images/endpoint.png new file mode 100644 index 0000000..6aedb1b Binary files /dev/null and b/images/endpoint.png differ diff --git a/images/rk3399i2c.png b/images/rk3399i2c.png new file mode 100644 index 0000000..72aed4e Binary files /dev/null and b/images/rk3399i2c.png differ diff --git a/images/ssd1306.jpg b/images/ssd1306.jpg new file mode 100644 index 0000000..ca8ce20 Binary files /dev/null and b/images/ssd1306.jpg differ diff --git a/images/zero3pinout.png b/images/zero3pinout.png new file mode 100644 index 0000000..3aba9a0 Binary files /dev/null and b/images/zero3pinout.png differ diff --git a/images/zero3pinoutcut.png b/images/zero3pinoutcut.png new file mode 100644 index 0000000..823558a Binary files /dev/null and b/images/zero3pinoutcut.png differ