1. At the time of writing, due to a packaging mess-up, the builds of SDR++ (and many other SDR tools by the looks of it) have suddenly and unintentionally dropped support for the popular RTL-SDR software defined radio dongle. Until this is fixed, users will have to build these applications themselves. The instructions are a little lacking in details, so here’s exactly how to do that, working on Kubuntu:

    # To install:
    sudo aptitude install cmake libfftw3-dev libglfw3-dev libvolk-dev zstd libairspy-dev librtlsdr-dev
    git clone https://github.com/AlexandreRouma/SDRPlusPlus.git
    cd SDRPlusPlus
    mkdir build
    cd build
    cmake ..
    make -j5 # 5 is the number of threads to use for building. adjust to your preference
    sh ../create_root.sh
    sudo make install
    
    # To run:
    sdrpp
    

  2. Reddit recently underwent another new, unpopular redesign, meaning that there are now three different UIs for accessing the website. Out of habit I would look at new-new reddit before remembering to switch to old-new reddit, and quickly noticed that looking at new-new reddit causes my laptop’s fans instantly start spinning.

    Here’s a firefox performance profile for 20-30s of each iteration of the reddit homepage. I simply loaded the page and did absolutely nothing.

    old.reddit.com: constant ~5% CPU usage with occasional spikes up to ~90%

    new.reddit.com: usually 0% CPU usage with occasional bursts up to ~50%

    reddit.com (new-new reddit): constant 30% CPU usage with occasional spikes even higher

    I don’t have the time or knowledge required to dig into the performance profiler but I would be fascinated to know how old-new reddit ended up more efficient than old-old reddit, and exactly what new-new reddit is doing with 30% of my CPU??

  3. When importing photos into DigiKam, I want them to be renamed based on the ISO8601 datetime they were taken, ideally to millisecond precision to avoid conflicts when multiple images were taken in a single second. This has the nice side-effect that photos from multiple camera sources are guaranteed to be displayed in the correct order when browsing files, which is important and practical for me.

    I started out using this custom file renaming template on import:

    [date:"yyyy-MM-ddTHHMMss"]-[file]
    

    as DigiKam’s [date] placeholder didn’t have an option for milliseconds, and I couldn‘t find a counter option which could handle my desired “append -# per-collision if this renaming scheme results in collisions”, so ended up just adding the entire camera-generated filename to the end.

    Unfortunately, this led to my files all being in the wrong order, even from the same camera. I haven’t quite figured out why, but it seems that there are several different internal datetime values stored in the image files: file creation date (which is completely wrong), the values used in DigiKam’s [date] templates (which are mostly wrong), and the DateTimeOriginal values stored in Exif data, which are not only correct but provide sub-second resolution too!

    So here’s the DigiKam import naming template which I will be using going forward:

    [meta:Exif.Photo.DateTimeOriginal]{range:1,10}{replace:":","-"}T[meta:Exif.Photo.DateTimeOriginal]{range:12,}{replace:":",""}s[meta:Exif.Photo.SubSecTimeOriginal]
    

    which produces ISO8601-ish filenames like this:

    2023-06-16T143139s922.JPG
    

    Broken down,

    [meta:Exif.Photo.DateTimeOriginal]
    

    evalulates to a string like

    2023:06:16 14:31:39
    

    but I don’t want colons in filenames for portability, so I take only the date part with {range:1,10}, replace the colons with hyphens {replace:":","-"}, add a literal T separator to avoid spaces in filenames, take the time part of the datetime and remove the colons completely, add a literal s separator to delimit seconds and milliseconds as I wanted to keep them separate but not use the typical . delimiter.

  4. Some tips for using Teensy on linux:

    The teensy board support package is only supported in the Arduino applications downloaded directly from the arduino site, NOT on versions distributed by package managers. I used the AppImage and it worked fine. If you try to install the teensy board package on an unsupported version of Arduino you might see errors like “archive not supported”.

    Even when you have a supported version of Arduino installed, installing the Teensy board package isn’t totally reliable. The first time I downloaded it, compiling would fail with the following error:

    fork/exec /home/barnaby/.arduino15/packages/teensy/tools/teensy-compile/11.3.1/arm/bin/arm-none-eabi-g++: no such file or directory
    
    Compilation error: fork/exec /home/barnaby/.arduino15/packages/teensy/tools/teensy-compile/11.3.1/arm/bin/arm-none-eabi-g++: no such file or directory
    

    The 11.3.1 folder existed, but was empty. Down- then upgrading the board package didn‘t fix it. Removing the board package and reinstalling it did work.

  5. Finally got koreader and plato set up on my Kobo, and calibre set up for managing and converting ebooks. The default {author}/{title} file structure for moving ebooks to the reader doesn’t work at all for me, but fortunately(?) calibre supports no less than three different template languages allowing for very flexible ebook organisation schemes. These can be configured by opening the “Configure this Device” menu and editing the “Save Template” field.

    I ended up using this little script to organise books in a way which makes navigation easy for my collection:

    program:
    # If the book is in a series, make a folder for that series, and ensure books are in order
    # by prepending their series index to their file name.
    if (field('series')) then
      strcat(field('series'), '/', field('series_index'),  ' - ', field('title'))
    # Otherwise, if a book has tags, make a folder for the first tag and put it there.
    elif (list_count(field('tags'), ',') > 0) then
      strcat(sublist(field('tags'), 0, 1, ','), '/', field('title'))
    # Otherwise, simply place the book at the top level for easy access and future sorting.
    else
      field('title')
    fi
    

    A couple of books ended up in the wrong folders, presumably due to some error with tag ordering or metadata syncing, but it was quick to clear up manually in the filesystem.

  6. PHPUnit’s HTML code coverage reports don’t play nicely with GitHub pages “main branch /docs folder” by default, as they store CSS, JS and icon assets in folders prefixed with underscores.

    Here’s a little bash script to run tests with code coverage enabled, then move the assets around:

    rm -rf docs/coverage/
    XDEBUG_MODE=coverage  ./vendor/bin/phpunit tests --coverage-filter src --coverage-html docs/coverage
    mv docs/coverage/_css docs/coverage/phpunit_css
    mv docs/coverage/_icons docs/coverage/phpunit_icons
    mv docs/coverage/_js docs/coverage/phpunit_js
    grep -rl _css docs/coverage | xargs sed -i "" -e 's/_css/phpunit_css/g'
    grep -rl _icons docs/coverage | xargs sed -i "" -e 's/_icons/phpunit_icons/g'
    grep -rl _js docs/coverage | xargs sed -i "" -e 's/_js/phpunit_js/g'
    

    That allows you to use GitHub pages to show code coverage reports as well as docs, as I’m doing for taproot/indieauth.

  7. Here’s a python snippet for analysing an iNaturalist export file and exporting an HTML-formatted list of species which only have observations from a single person (e.g. this list for the CNC Wien 2021)

    # coding: utf-8
    
    import argparse
    import pandas as pd
    
    """
    Find which species in an iNaturalist export only have observations from a single observer.
    
    Get an export from here: https://www.inaturalist.org/observations/export with a query such
    as quality_grade=research&identifications=any&rank=species&projects[]=92926 and at least the
    following columns: taxon_id, scientific_name, common_name, user_login
    
    Download it, extract the CSV, then run this script with the file name as its argument. It will
    output basic stats formatted as HTML.
    
    The only external module required is pandas.
    
    Example usage:
    
    		py uniquely_observed_species.py wien_cnc_2021.csv > wien_cnc_2021_results.html
    
    If you provide the --project-id (-p) argument, the taxa links in the output list will link to 
    a list of observations of that taxa within that project. Otherwise, they default to linking
    to the taxa page.
    
    If a quality_grade column is included, non-research-grade observations will be included in the
    analysis. Uniquely observed species with no research-grade observations will be marked. Species
    which were observed by multiple people, only one of which has research-grade observation(s) will
    also be marked.
    
    By Barnaby Walters waterpigs.co.uk
    """
    
    if __name__ == "__main__":
    	parser = argparse.ArgumentParser(description='Given an iNaturalist observation export, find species which were only observed by a single person.')
    	parser.add_argument('export_file')
    	parser.add_argument('-p', '--project-id', dest='project_id', default=None)
    
    	args = parser.parse_args()
    
    	uniquely_observed_species = {}
    
    	df = pd.read_csv(args.export_file)
    
    	# If quality_grade isn’t given, assume that the export contains only RG observations.
    	if 'quality_grade' not in df.columns:
    		df.loc[:, 'quality_grade'] = 'research'
    
    	# Filter out casual observations.
    	df = df.query('quality_grade != "casual"')
    
    	# Create a local species reference from the dataframe.
    	species = df.loc[:, ('taxon_id', 'scientific_name', 'common_name')].drop_duplicates()
    	species = species.set_index(species.loc[:, 'taxon_id'])
    	
    	for tid in species.index:
    		observers = df.query('taxon_id == @tid').loc[:, 'user_login'].drop_duplicates()
    		research_grade_observers = df.query('taxon_id == @tid and quality_grade == "research"').loc[:, 'user_login'].drop_duplicates()
    
    		if observers.shape[0] == 1:
    			# Only one person made any observations of this species.
    			observer = observers.squeeze()
    			if observer not in uniquely_observed_species:
    				uniquely_observed_species[observer] = []
    
    			uniquely_observed_species[observer].append({
    				'id': tid,
    				'has_research_grade': (not research_grade_observers.empty),
    				'num_other_observers': 0
    			})
    		elif research_grade_observers.shape[0] == 1:
    			# Multiple people observed the species, but only one person has research-grade observation(s).
    			rg_observer = research_grade_observers.squeeze()
    			if rg_observer not in uniquely_observed_species:
    				uniquely_observed_species[rg_observer] = []
    			
    			uniquely_observed_species[rg_observer].append({
    				'id': tid,
    				'has_research_grade': True,
    				'num_other_observers': observers.shape[0] - 1
    			})
    	
    	# Sort observers by number of unique species.
    	sorted_observations = sorted(uniquely_observed_species.items(), key=lambda t: len(t[1]), reverse=True)
    
    	print(f"<p>{sum([len(t) for _, t in sorted_observations])} taxa uniquely observed by {len(sorted_observations)} observers.</p>")
    
    	print('<p>')
    	for observer, _ in sorted_observations:
    		print(f"@{observer} ", end='')
    	print('</p>')
    
    	print('<p><b>bold</b> species are ones for which the given observer has one or more research-grade observations.</p>')
    	print('<p>If only one person has RG observations of a species, but other people have observations which need ID, the number of needs-ID observers are indicated in parentheses.')
    
    	for observer, taxa in sorted_observations:
    		print(f"""\n\n<p><a href="https://www.inaturalist.org/people/{observer}">@{observer}</a> ({len(taxa)} taxa):</p><ul>""")
    		for tobv in sorted(taxa, key=lambda t: species.loc[t['id']]['scientific_name']):
    			tid = tobv['id']
    			t = species.loc[tid]
    
    			if args.project_id:
    				taxa_url = f"https://www.inaturalist.org/observations?taxon_id={tid}&amp;project_id={args.project_id}"
    			else:
    				taxa_url = f'https://www.inaturalist.org/taxa/{tid}'
    			
    			rgb, rge = ('<b>', '</b>') if tobv.get('has_research_grade') else ('', '')
    			others = f" ({tobv.get('num_other_observers', 0)})" if tobv.get('num_other_observers', 0) > 0 else ''
    
    			if not pd.isnull(t['common_name']):
    				print(f"""<li><a href="{taxa_url}">{rgb}<i>{t['scientific_name']}</i> ({t['common_name']}){rge}{others}</a></li>""")
    			else:
    				print(f"""<li><a href="{taxa_url}">{rgb}<i>{t['scientific_name']}</i>{rge}{others}</a></li>""")
    		print("</ul>")
    
  8. I finally got Visual Studio Code’s terminal and tab handling working the way I wanted, with these keybindings:

    [
      {
          "key": "shift+cmd+]",
          "command": "workbench.action.terminal.focusNext",
          "when": "terminalFocus"
      },
      {
          "key": "shift+cmd+[",
          "command": "workbench.action.terminal.focusPrevious",
          "when": "terminalFocus"
      },
      {
          "key": "cmd+w",
          "command": "workbench.action.terminal.kill",
          "when": "terminalFocus"
      },
      {
          "key": "ctrl+`",
          "command": "workbench.action.terminal.focus"
      },
      {
          "key": "ctrl+`",
          "command": "workbench.action.focusActiveEditorGroup",
          "when": "terminalFocus"
      }
    
    ]
    

    This way, cmd+shift+] and cmd+shift+[ not only allow you to switch between editor tabs, but also terminals, when the terminal is focused. cmd+w kills the current terminal tab as expected. ctrl+` switches focus between the editor and the terminal.

    I was considering overriding cmd+t to open a new terminal when the terminal is focused, but decided to just get used to the built in ctrl+shift+` shortcut instead.

  9. A Camomile tip I learned the hard way: audio units built using camomile (and presumably VSTs and lv2s too) will stop working if you rename them, as they rely on the bundle filename, the .txt configuration file and the main .pd patch all sharing the same base name in order to function.

  10. Every time I try to do embedded programming outside the arduino/teensy ecosystem, I’m amazed at how everything else manages to somehow be even worse

  11. Aaron Parecki: Ok that was fun, thanks for all the responses! Lots of great stuff in there. Now take your favorite programming language and tell me the 3 things you most dislike about it. No complaining about languages you don't use!

    For python, the lack of type information for function signatures and return values in the documentation has always annoyed me.

    The lack of naming consistency in the standard library, too — it’s almost as bad as PHP, with nospaces, under_scores and CamelCase at every level: modules, classes, functions, arguments.

    I can’t think of a third major annoyance off the top of my head though, and almost every time I use another programming language, I end up realising just how well designed some aspect of python is, so it’s not doing too badly.

  12. Experimenting with some computational musicology on a vast corpus of traditional music compiled by a friend. Extrapolating from the last few hours, I anticipate the first complete analysis will take 2.5 days to complete on my macbook. Plenty of time to research how to build a raspberry pi parallel computing cluster…

  13. It just took me about 30 mins to figure it out, so here’s how to install python plugins in KiCad 5.0 on a Mac.

    1. Make sure your build of KiCad has scripting enabled. It looks like fresh downloads have it by default, but it doesn’t hurt to check. Go KiCad → About KiCad → Show Version Info and make sure that all of the KICAD_SCRIPTING_ flags are set to ON.
    2. Find pcbnew’s plugin search path list. Open pcbnew, and open Tools → Scripting Console. Run import pcbnew; print pcbnew.PLUGIN_DIRECTORIES_SEARCH and you’ll see a list of folders which pcbnew will search for plugins
    3. Move your plugin files/folders to one of these locations
    4. In pcbnew, Tools → External Plugins… → Refresh Plugins. Your Tools → External Plugins menu should fill up with plugins.
  14. about tcpdump, a very handy little command line utility for looking at network traffic. Not as fully featured as Wireshark, but nice if you want to e.g. pipe network traffic into another process. It’s also not >300MB :/

  15. When using the python2.7 gzip module with StringIO, it’s extremely important to call GzipFile.close() after you’ve finished writing to it. If you don’t, the archive will probably not be readable as the CRC record will not be appended, and you’ll get a bunch of IOErrors despite everything looking fine.