Intel NUC Spreadsheet

Trying to get my head around all the different NUCs and their performance/price.

Price was taken from Amazon (in USD) and performance from PassMark. Neither are very accurate, but should be adequate for an overview.

GenNameModelProcessorSingle/Multi PassMarkTDPPrice USDPrice / PerformanceNotes
1Sandy BridgeDCCP847DYE Celeron 847 17
2Ivy BridgeDC3217IYE Core i3-3217U17
2Ivy BridgeDC3217BY Core i3-3217U 17
2Ivy BridgeDC53427HYE Core i5-3427U 17
3Bay Trail-MDN2820FYKH Celeron N2820 7.5
3Bay Trail-MDN2820FYKH Celeron N2830 7.5
3Bay Trail-IDE3815TYKHE Atom E38155
4HaswellD34010WYKCore i3-4010U9292,44115
4HaswellD34010WYKH Core i3-4010U9292,44115
4HaswellD54250WYKCore i5-4250U 1,4133,37615$3504.09.6
4HaswellD54250WYKHCore i5-4250U 1,4133,37615$5002.86.8
5Broadwell-UNUC5i7RYH i7-5557U 1,8934,95228
5Broadwell-UNUC5i5RYH i5-5250U 1,4243,58815$4403.28.2
5Broadwell-UNUC5i5RYK i5-5250U 1,4243,58815
5Broadwell-UNUC5i3RYH i3-5010U 1,1613,05715$2354.913.0
5Broadwell-UNUC5i3RYK i3-5010U 1,1613,05715
5Broadwell-UNUC5i3MYHE i3-5010U 1,1613,05715
5Broadwell-UNUC5i5MYHE i5-5300U vPro 1,5313,83815$2895.313.3
5BraswellNUC5CPYHCeleron N30504698796$1413.36.2
5BraswellNUC5PPYHPentium N37005481,8366$1902.99.7
5BraswellNUC5PGYHPentium N37005481,8366
6Skylake-UNUC6i3SYKCore i3-6100U1,3023,60715$3803.49.5
6Skylake-UNUC6i3SYHCore i3-6100U1,3023,60715$3803.49.5
6Skylake-UNUC6i5SYKCore i5-6260U1,5924,36015$3904.111.2
6Skylake-UNUC6i5SYHCore i5-6260U1,5924,36015$3504.512.5
6Skylake-UNUC6i7KYKCore i7-6770HQ1,9089,71945$6103.115.9
6Apollo LakeNUC6CAYHCeleron J34557712,11210$2702.97.8
6Apollo LakeNUC6CAYSCeleron J34557712,11210
7Kaby Lake-U (Baby Canyon)NUC7i7BNHi7-7567U2,2616,46428$4954.613.1
7Kaby Lake-U (Baby Canyon)NUC7i7BNHX1i7-7567U2,2616,46428$5803.911.116GB Optane
7Kaby Lake-U (Baby Canyon)NUC7i5BNHi5-7260U1,9395,63815$4204.613.4
7Kaby Lake-U (Baby Canyon)NUC7i5BNHX1i5-7260U1,9395,63815$4804.011.716GB Optane
7Kaby Lake-U (Baby Canyon)NUC7i5BNKi5-7260U1,9395,63815$4304.513.1Short
7Kaby Lake-U (Baby Canyon)NUC7i3BNHi3-7100U1,3593,76615$3404.011.1
7Kaby Lake-U (Baby Canyon)NUC7i3BNHX1i3-7100U1,3593,76615$5002.77.516GB Optane
7Kaby Lake-U (Baby Canyon)NUC7i3BNKi3-7100U1,3593,76615$3503.910.8
7Kaby Lake-U (Dawson Canyon)NUC7i5DNHEi5-7300U1,9575,16115$5003.910.3
7Kaby Lake-U (Dawson Canyon)NUC7i5DNKEi5-7300U1,9575,16115$5003.910.3Short
7Kaby Lake-U (Dawson Canyon)NUC7i3DNHEi3-7100U1,3593,76615$3503.910.8
7Kaby Lake-U (Dawson Canyon)NUC7i3DNKEi3-7100U1,3593,76615$3503.910.8Short
7Kaby Lake-R (Dawson Canyon)NUC7i7DNHEi7-8650U2,2248,70615$6703.313.0
7Kaby Lake-R (Dawson Canyon)NUC7i7DNKEi7-8650U2,2248,70615
7Gemini LakeNUC7PJYHPentium J50051,1932,90110$2155.513.5
7Gemini LakeNUC7CJYHCeleron J40051,1501,54410$1457.910.6
8Kaby Lake-GNUC8i7HNKCore i7‑8705G2,2199,92865$8802.511.3
8Kaby Lake-GNUC8i7HVKCore i7‑8809G2,33510,996100$1,0502.210.5
8Coffee Lake-UNUC8i7BEHi7-8559U2,56812,22428$5604.621.8
8Coffee Lake-UNUC8i5BEHi5-8259U2,22010,99428$5504.020.0
8Coffee Lake-UNUC8i5BEKi5-8259U2,22010,99428$5793.819.0
8Coffee Lake-UNUC8i3BEHi3-8109U2,1486,15428$3027.120.4
8Coffee Lake-UNUC8i3BEKi3-8109U2,1486,15428$3446.217.9
8Cannon Lake-UNUC8i3CYSMi3-8121U1,6544,80215$6002.88.08GB + 1TB
8Cannon Lake-UNUC8i3CYSNi3-8121U1,6544,80215$5503.08.7
8Whiskey Lake-UNUC8i7INHi7-8565U2,3448,92115
8Whiskey Lake-UNUC8i5INHi5-8265U2,1468,02515
8Apollo LakeNUC8CCHKRCeleron N33507631,1116

Posted on

Wallabag + CapRover in 5m

Wallabag “is a self hostable application for saving web pages”

CapRover “is an extremely easy to use app/database deployment & web server manager”

This is a quick and dirty setup for testing Wallabag (without full database or redis, or email, etc).

5m setup

  1. Setup new app with persistent data enabled in CapRover
  2. In App Configs add the environmental variable SYMFONY__ENV__DOMAIN_NAME and set it to the full URL of where you want Wallabag. e.g.
  3. Add two persistent directories ofr /var/www/wallabag/data/ and /var/www/wallabag/web/assets/images/
  4. In the deployment tab scroll down to method 4 and enter: FROM wallabag/wallabag and deploy
  5. In 30s Wallabag should be available at
  6. Default login is wallabag / wallabag so you probably want to go change that

Example App Configs


  • No CSS or images: Need to make sure SYMFONY__ENV__DOMAIN_NAME points to the full URL including protocol. e.g.
  • 502 error: Log into to server and check Docket logs. e.g. `
    • docker service logs srv-captain--wallabag --follow or
    • docker service ps srv-captain--wallabag --no-trunc

Posted on

Download steam images

Little Python program to download your images from your Steam gallery.

For each file it just downloads the images by its id and attempts to include some meta data about the image in a separate json file. For example:

{"Id": 1474149818, "Game": "Crypt of the NecroDancer", "Desc": "Bat out of hell", "Url": ""}

Code uses requests and has been tested with Python 3.7 on Windows.

import requests
import re
from pathlib import Path
import json
from typing import List

# Because I'm lazy and probably the only person to ever run this
STEAM_ID = 'Ardren'
DOWNLOAD_DIRECTORY = r'c:\Users\Matthewd\Dropbox\Screenshots-Steam'

def scan_gallery(username):
    file_ids = []
    page = 1

    while True:
        url = f"{username}/screenshots/?p={page}&sort=newestfirst&browsefilter=myfiles&view=grid&privacy=14"
        r = requests.get(url)

        matches = list(re.finditer(r'<a href="https://steamcommunity\.com/sharedfiles/filedetails/\?id=(\d+)"', r.text))

        if len(matches) == 0:
            print(f"No more files found on page {page}")

        print(f"Found {len(matches)} images on page {page}")

        for match in matches:

        page += 1

    return file_ids

def download_file_ids(base_directory: Path, file_ids: List[int]):
    for file_id in file_ids:

        download_filename = base_directory / f"{file_id}.jpeg"
        metadata_filename = base_directory / f"{file_id}.json"

        if download_filename.exists():
            print(f"Already downloaded {download_filename}")

        url = f"{file_id}"
        r = requests.get(url)

        match ='<a href="([^/]+/[^/]+/)" target=', r.text)
        if match is None:
            print(f"Unable to find image_src for file_id {file_id}")

        create_metadata(metadata_filename, file_id, r.text)

        with'wb+') as f:
            r = requests.get(
            bytes_written = f.write(r.content)
            print(f"Downloaded {bytes_written} bytes to {download_filename}")

def create_metadata(metadata_filename: Path, file_id: int, html_detail_contents: str):
    game_name_match ='<h3 class="ellipsis apphub_responsive_menu_title">([^<]+)</h3>', html_detail_contents)
    game_desc_match ='<textarea class="descField" name="description" id="description" class="dynInput" maxlength="140">([^<]*)</textarea>', html_detail_contents)
    image_url_match ='<a href="([^/]+/[^/]+/)" target="_blank">', html_detail_contents)

    data = {
        'Id': file_id,

    with'w+') as f:
        json.dump(data, f)

if __name__ == '__main__':
    download_file_ids(Path(DOWNLOAD_DIRECTORY), scan_gallery(STEAM_ID))

Posted on

Website update

Hey look, HTTPS and colors

Edit: and block-quotes, and tables, and responsive images, and page titles

Posted on

Website back up

Website back up today after long period of down time.

I think I would like to go back to recording things I do.

Even if they are small and useless, I think it would be heathy for me.

Posted on

TP-Link TD-W8961N

  • Product Link
  • Don’t buy one
  • If you’re too lazy to replace it, and you want to monitor your ADSL stats when it rains…

import telnetlib
import re
import time

class TPW8961N(object):

    def __init__(self, host): = host

    def __enter__(self):
        self.con = telnetlib.Telnet(, 23, 5)
        self.con.read_until('Password: ')
        self.con.read_until('TP-LINK> ')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):

    def send_cmd(self, cmd):
        self.con.write(cmd.get_cmd() + '\n')
        response = self.con.read_until('TP-LINK> ')
        return cmd.parse(response)

class Command(object):
    def __init__(self):

    def get_cmd(self):

    def parse(self, string):

class StatusCommand(Command):
    def get_cmd(self):
        return 'wan adsl status'

    def parse(self, response):
        match ='current modem status: (.*?)\r\n', response)
        if match is not None and == 'up':
            return 'up'
            return 'down'

class RateCommand(Command):
    def get_cmd(self):
        return 'wan adsl c'

    def parse(self, response):
        dl_match ='near-end interleaved channel bit rate: (.*?) kbps\r\n', response)
        ul_match ='far-end interleaved channel bit rate: (.*?) kbps\r\n', response)

        if dl_match is None or ul_match is None:
            return {'dl_rate': 0, 'ul_rate': 0}

        return {'dl_rate': int(, 'ul_rate': int(}

class QualityCommand(Command):
    def __init__(self, direction):
        if direction not in ['downstream', 'upstream']:
            raise Exception('Unknown direction, must be "downstream" or "upstream"')
        self.direction = direction
        super(Command, self).__init__()

    def get_cmd(self):
        if self.direction == 'downstream':
            return 'wan adsl l n'
        return 'wan adsl l f'

    def parse(self, response):
        noise_match ='noise margin ' + self.direction + ': (.*?) db\r\n', response)
        attenuation_match ='attenuation ' + self.direction + ': (.*?) db\r\n', response)

        if noise_match is None or attenuation_match is None:
            return {'noise': 0.0, 'attenuation': 0.0}

        return {'noise': float(, 'attenuation': float(}

if __name__ == '__main__':

    while True:
            print time.strftime('%Y-%m-%d %H:%M-%S')
            with TPW8961N('') as modem:
                print modem.send_cmd(StatusCommand())
                print modem.send_cmd(RateCommand())
                print modem.send_cmd(QualityCommand('downstream'))
                print modem.send_cmd(QualityCommand('upstream'))
        except KeyboardInterrupt:
        except e:
            print e

Posted on

Picking a Practice Lock

I bought a transparent practice lock (5 pin with spools) from Massdrop a while ago, and thanks to some procrastinating had a proper go at it today. While it is just a practice lock, I struggled judging the correct tension when dealing with false sets, and the whole process felt more delicate than a basic lock. I feel like I might have given up on a standard lock before understanding how the lock behaved and why. Still, I need much, much more practice.

(Massdrop is running another drop for it now)

Posted on

KBT Pure Pro 60% Keyboard Layout

There’s not that much information about the (Vortex) KBT Pure Pro keyboard on the internet so I thought I’d add my notes here.

  1.5 1x12 1.5
  1.75 1x11 2.25
  2 1x13
  1.25 1x2 1.25 4.5 1x6

(1 unit = 0.75" or about 19mm)
Notable weird key sizes:
  \          1
  Backspace  1
  Del        1.5
  L Shift    2
  R Shift    1
  R Ctrl     1
  System     1
  Space      4.5
  R Alt      1

I’ve been playing with using Lua to generate parts of my wiki pages, below is the code to generate a simple SVG of the Pure Pro’s layout.



local tag = doku.xml_tag

local unit = 19*2  -- Unit size, mapped straight to pixels
local space = 4    -- Spacing between keys (as pixels again)

local halfspace = space/2
local doublespace = space*2

local fontsize = 10

local layout = {
  {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
  {1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5},
  {1.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.25},
  {2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
  {1.25, 1, 1, 1.25, 4.5, 1, 1, 1, 1, 1, 1}

local keys = {
  {'esc', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\\', 'BS'},
  {'tab', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 'del'},
  {'caps', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k',' l', ';', '\'', 'enter'},
  {'shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'shift', 'up', 'rctrl'},
  {'lctrl', 'fn', 'sys', 'lalt', 'space', 'ralt', 'fn', 'pn', 'left', 'down', 'right'},

local scene = {}

local background = tag('rect', {width="100%", height="100%", fill="#ddd", stroke="black"})
table.insert(scene, background)

for row=1, #layout do
  x = unit
  for col=1, #layout[row] do

    local rect = tag('rect', {x=x+halfspace,

    table.insert(scene, rect)

    local sizetext = tag('text', {x=x+doublespace,
                                  style='font-size: ' .. fontsize .. 'px'},

    table.insert(scene, sizetext)

    if keys[row] and keys[row][col] then
      local keytext = tag('text', {x=x+doublespace,
                                  style='font-size: ' .. fontsize .. 'px'},

      table.insert(scene, keytext)

    x = x + (layout[row][col]*unit)

-- Keyboard is 15 units by 5 units (+1 each side for padding)
print(tag('svg', {width=(unit*17), height=(unit*7)}, scene))

SVG Output

1esc111213141516171819101-1=1\1BS1.5tab1q1w1e1r1t1y1u1i1o1p1[1]1.5del1.75caps1a1s1d1f1g1h1j1k1 l1;12.25enter2shift1z1x1c1v1b1n1m1,1.1/1shift1up1rctrl1.25lctrl1fn1sys1.25lalt4.5space1ralt1fn1pn1left1down1right

Posted on

LuaJIT Benchmarks

I’ve been playing with Lua a little bit recently and was interested in getting an idea of the performance difference between Lua 5.3 and LuaJIT. (The LuaJIT website has very good page covering benchmarks. I just wanted to play around myself). Like the LuaJIT page I picked a few example programs from The Computer Language Benchmarks Game. Programs were run on a AMD-6300FX running Windows 8.

nbody.lua-4.lua 50000000

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 469.36 0.05 467.05 2,356
luajit203 26.28 0.00 26.20 2,504
luajit21 26.31 0.00 26.25 2,512

fannkuchredux.lua 12

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 1717.53 0.14 1710.78 2,284
luajit203 105.33 0.00 105.16 2,528
luajit21 106.06 0.00 105.91 2,536

spectralnorm.lua 5500

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 199.65 0.03 199.38 2,864
luajit203 4.89 0.00 4.88 2,724
luajit21 4.89 0.00 4.88 2,728

binarytrees.lua-2.lua 2

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 439.98 13.95 423.66 1,072,400
luajit203 78.34 2.22 75.86 928,076
luajit21 77.92 1.75 75.91 811,984

Summary Relative increase in speed over Lua 5.3.

Benchmark lua53 luajit203 luajit2
nbody.lua-4.lua 50000000 1.00 17.83 17.79
fannkuchredux.lua 12 1.00 16.27 16.15
spectralnorm.lua 5500 1.00 40.86 40.86
binarytrees.lua-2.lua 20 1.00 5.58 5.58

Posted on

Needless Abuse of Lua

So, this is pretty awful. Cool too. Bust mostly awful.


local function setoptions(f)
    local options = {}
    local currentname = nil
    local env = setmetatable({},
        __index = function(self, method)
            if method == 'set' then
                return function(name)
                    if currentname ~= nil then
                        error('need to call "to" first')
                    currentname = name
            elseif method == 'to' then
                return function(value)
                    if currentname == nil then
                        error('need to call "set" before "to"')
                    options[currentname] = value
                    currentname = nil
            elseif method == 'enable' then
                return function(name)
                    options[name] = true

    -- close enough to fsetenv
    load(string.dump(f), nil, nil, env)()

    return options

local function pptable(t)
    for k,v in pairs(t) do
        print(k, '=', v)

-- Cool way
local options = setoptions(function()
    set "port" to "8080"
    set "listen" to ""
    enable "debug"


-- Boring way
options = {port='8080', listen='', debug=true}



debug	=	true
port	=	8080
listen	=

debug	=	true
port	=	8080
listen	=

Posted on