Category: Tech Articles

  • Update to “Why ChatGPT and Bard won’t (yet) take over my job”

    A few months ago I wrote about my simple test to see how both Bard and ChatGPT 3.5 did with a simple programmer interview question (summary: neither one passed) and since Bard has had its innards recently replaced (with Google Gemini LLM) which is supposedly an improvement over its original PaLM LLM AI, I thought I’d rerun the test for Bard to see if it did any better.

    The prompt I submitted back in September was:

    create java 8 code/class that provides a macro replacement capability. specifically a string can contain zero or more macro references where a reference is something like “$name” (the $ char introduces the alphanumeric macro name), and the processing of the string requires a map and each $name is replaced with the corresponding value in the map. one special case is that if the $ char is encountered within a single-quote string, then it’s not taken as a macro

    and so I submitted it exactly as above to the improved Bard. This is what is produced (though I did add the import java.util.* and the main() method):

    import java.util.*;
    
    public class MacroReplacer {
    
        private final Map<String, String> macros;
    
        public MacroReplacer(Map<String, String> macros) {
            this.macros = macros;
        }
    
        public String replaceMacros(String text) {
            StringBuilder builder = new StringBuilder();
            int pointer = 0;
            while (pointer < text.length()) {
                int dollarIndex = text.indexOf('$', pointer);
                if (dollarIndex == -1) {
                    builder.append(text.substring(pointer));
                    break;
                }
    
                // Check if dollar is inside a single-quote string
                int prevQuoteIndex = -1;
                int quoteIndex = text.indexOf('\'', pointer);
                while (quoteIndex != -1 && quoteIndex < dollarIndex) {
                    prevQuoteIndex = quoteIndex;
                    quoteIndex = text.indexOf('\'', quoteIndex + 1);
                }
                if (prevQuoteIndex != -1 && prevQuoteIndex % 2 == 1) {
                    // Dollar inside single-quote string, append as-is
                    builder.append(text.substring(pointer, dollarIndex + 1));
                    pointer = dollarIndex + 1;
                    continue;
                }
    
                // Extract macro name
                int nextSpaceIndex = text.indexOf(' ', dollarIndex + 1);
                int macroNameLength = nextSpaceIndex != -1 ? nextSpaceIndex - dollarIndex - 1 : text.length() - dollarIndex - 1;
                String macroName = text.substring(dollarIndex + 1, dollarIndex + 1 + macroNameLength);
    
                // Replace macro with value from map
                String macroValue = macros.get(macroName);
                if (macroValue != null) {
                    builder.append(macroValue);
                } else {
                    builder.append('$');
                    builder.append(macroName);
                }
    
                pointer = dollarIndex + 1 + macroNameLength;
            }
            return builder.toString();
        }
    
        /**
         * had to add (copied from ChatGPT's code)
         */
        public static void main(String... args)
        {
            Map<String, String> macroMap = Map.of(
                    "name", "John",
                    "age", "30"
            );
            MacroReplacer bard = new MacroReplacer(macroMap);
    
            String input = "Hello $name! You are $age years old. Do not replace '   $age' in single quotes.";
            String output = bard.replaceMacros(input);
    
            System.out.println(output);
        }
    }

    and when I ran it I got

    $name!30$age' in single quotes.

    which, sadly, is much worse than original Bard.

    Maybe GPT4 is better (I don’t want to pay to test that) but, then again, maybe I’m justified in still not worrying about them taking my job.

  • Why ChatGPT and Bard won’t (yet) take over my job

    Late last year (2022) it seemed like the programming world freaked out about the capability of ChatGPT and, a bit later, Google’s Bard–it seemed like there were messages of doom everywhere about how programmers would be out of a job soon. There were many headlines about how people could create apps in just a few minutes.

    After a while the hype settled down and the reality of what those two LLMs could be with respect to programming became more understood–it seemed that maybe after all, people like me wouldn’t be (yet) replaced by AI.

    Testing them out

    When I give interviews to candidate engineers, I have a standard set of questions/tasks that I ask them, somewhat to be able to normalize across engineers and somewhat because the questions I’ve asked seem to do a reasonable job of identifying what I view as strengths and weaknesses.

    One of the questions I routinely ask candidates is a coding task that asks them to write Java code that does macro replacement. (For those readers who don’t know what this means, think of form emails/letters that start with something like “Dear Scott”, where the “Scott” part is taken from some database of people–behind this is something that has some template like “Dear firstname” — that replacement of firstname with “Scott” is basically a macro replacement).

    I happened to interview a candidate recently for one of my contracts and was intrigued by the approach the candidate took to this task–he focused on optimization and ended up using what’s called a stack to try to make it as fast as possible (I don’t know that his approach would have been all that optimized…). That got me to thinking about whether or not my own answer to that interview question was faster (more optimized) than the usual approach that I’ve seen interview candidates respond with. (Yes, prior to asking candidates this question, I created a solution–in fact it was the solution that prompted me to include the problem as an interview question.)

    For whatever reason I decided to test out ChatGPT and Bard to see how they both would do with solving this pretty trivial programming problem, mostly to compare the efficiency of my approach (which is to use a Finite State Machine–see https://en.wikipedia.org/wiki/Finite-state_machine).

    The Prompt

    I know there’s somewhat of an art to submitting the optimal “prompt” to these LLM AIs, but I’m not the sort to try to figure out how to make a supposedly smart AI be really smart, so I submitted the following prompt to both ChatGPT (3.5–the public version) and Bard (still public, but continually updated based on the alerts I see when I use it):

    create java 8 code/class that provides a macro replacement capability. specifically a string can contain zero or more macro references where a reference is something like “$name” (the $ char introduces the alphanumeric macro name), and the processing of the string requires a map and each $name is replaced with the corresponding value in the map. one special case is that if the $ char is encountered within a single-quote string, then it’s not taken as a macro

    From each of them I got Java code that compiled successfully. I didn’t trust the code at all to work so I started reviewing each result and realized that each one had bugs. For those of you who are programmers you might want to review the following code to see if you can spot the errors

    ChatGPT code

    This is what ChatGPT came back with given the above prompt

    import java.util.Map;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class Chatgpt {
        public static String process(String input, Map<String, String> macroMap) {
            String pattern = "\\$(\\w+)";
            Pattern r = Pattern.compile(pattern);
    
            StringBuilder result = new StringBuilder();
            boolean inSingleQuotes = false;
    
            for (int i = 0; i < input.length(); i++) {
                char c = input.charAt(i);
    
                if (c == '\'') {
                    inSingleQuotes = !inSingleQuotes;
                    result.append(c);
                } else if (!inSingleQuotes && c == '$') {
                    Matcher matcher = r.matcher(input.substring(i));
    
                    if (matcher.find()) {
                        String macroName = matcher.group(1);
                        String macroValue = macroMap.getOrDefault(macroName, "");
                        result.append(macroValue);
                        i += matcher.group(1).length() - 1;
                    } else {
                        result.append(c);
                    }
                } else {
                    result.append(c);
                }
            }
    
            return result.toString();
        }
    
        public static void main(String[] args) {
            Map<String, String> macroMap = Map.of(
                    "name", "John",
                    "age", "30"
            );
    
            String input = "Hello $name! You are $age years old. Don't replace '  $age' in single quotes.";
            String output = process(input, macroMap);
    
            System.out.println(output);
        }
    }

    (Note: I modified the test input string based on what I believed would be a problem after reviewing both ChatGPT- and Bard-generated code; the specific modification I did was to add white space in between the ‘ and the $ characters).

    And this is what running it produces:

    Hello Johne! You are 30e years old. Don't replace '  30e' in single quotes.

    which is clearly not what is intended as the last character of the macros (name and age) are incorrectly being included.

    Bard code

    This is what Google’s Bard came back with (with an edit of mine to include the main method with a string that I believed would cause it to fail):

    import java.util.*;
    
    public class Bard {
    
        private final Map<String, String> macros;
    
        public Bard(Map<String, String> macros) {
            this.macros = macros;
        }
    
        public String replaceMacros(String str) {
            StringBuilder sb = new StringBuilder();
            int i = 0;
            while (i < str.length()) {
                char c = str.charAt(i);
                if (c == '$') {
                    // Check if the '$' character is within a single-quote string.
                    if (i > 0 && str.charAt(i - 1) == '\'') {
                        sb.append(c);
                        i++;
                    } else {
                        // Find the end of the macro name.
                        int end = i + 1;
                        while (end < str.length() && Character.isAlphabetic(str.charAt(end))) {
                            end++;
                        }
                        String macroName = str.substring(i + 1, end);
                        // Replace the macro name with the corresponding value in the map.
                        String macroValue = macros.get(macroName);
                        if (macroValue != null) {
                            sb.append(macroValue);
                        } else {
                            sb.append("$");
                            sb.append(macroName);
                        }
                        i = end;
                    }
                } else {
                    sb.append(c);
                    i++;
                }
            }
            return sb.toString();
        }
    
        /**
         * had to add (copied from ChatGPT's code)
         */
        public static void main(String... args)
        {
            Map<String, String> macroMap = Map.of(
                    "name", "John",
                    "age", "30"
            );
            Bard bard = new Bard(macroMap);
    
            String input = "Hello $name! You are $age years old. Do not replace '   $age' in single quotes.";
            String output = bard.replaceMacros(input);
    
            System.out.println(output);
        }
    }
    

    and when it’s run it produces

    Hello John! You are 30 years old. Do not replace '   30' in single quotes.

    and that’s clearly wrong as it still replaces the macro even though it’s “protected” by single quotes.

    Summary

    I don’t want to draw this out by trying to figure out either why they both failed or how to make them produce correct code. I just want to show that a they both fail–miserably in my mind–to create correctly functioning code given a coding prompt. And I say “miserably” because the bugs that each solution has isn’t immediately obvious and shows up only with actual testing

    My personal message on all this is if anyone thinks that the current versions of ChatGPT and Bard can do anything but produce some code that has to be reviewed, tested, and very likely modified, then they need to actually try out these LLMs. They just aren’t going to come close to replacing non-trivial programming at this time (and, my personal opinion is, after being in the industry for almost 45 years) is that it will be a very long time before we programmers have to worry about our jobs.

  • Fun with queues – or how to make a maze

    This is companion piece to “how to solve a maze”.

    I’ve spent far too much time playing with mazes, both when I was young, and now later in life. I’ve wondered if this would fall under a “special interest” and I think there’s some argument that it could.

    While I was playing with code for solving a maze, I had to have some maze for it to solve. Initially I just had the code take out walls at random, knowing that while this wasn’t the best approach by any means, it was probably good enough to test the solver code (assuming enough walls were taken out). I also resorted to hardcoding a set of walls to remove on a small maze.

    But that wasn’t good enough, and I wanted to create good and fun mazes, though those attributes are hard to define.

    Now, if a maze has a single path from start to finish, then a deterministic algorithm as described in the other post will find it. Unfortunately the creation of a good and fun maze isn’t so deterministic as the generation of it must incorporate some amount of randomness that ends up presenting a good number of long-ish wrong paths. Additionally maze creation has a few constraints that make it a bit less straightforward: all cells should be reachable from the start, and there should (ideally) be exactly one path through it.

    False starts

    Sometimes I rush into coding without thinking enough. I think this happens when I think the problem is easier than it really is, and because I’ve coding long enough that I think I don’t need to stop and think. In any case, that’s what I did at first–I didn’t think enough and ended up going down some blind alleys, which is totally apropos solving mazes.

    My first attempt was to use recursion to create a path (I guess recursion was on my brain). My code notes describe the idea:

        /**
         * The real maze maker recursive algorithm.  Not sure if it will work
         * but the idea is that each invocation of this method the current
         * position is a newly entered square and we have to randomly decide
         * which direction to go (if we move at all), subject to the following
         * conditions:
         *
         *  * each cell must have at least 1 wall
         *  * we can't open up a wall to an adjacent cell that's already been visited
         *
         * We will rotate through the 4 directions and each time we have
         * a low probability of opening up a wall in that direction.  Once
         * we open up a wall, we recurse into this method.
         */

    It kept track of the “ends” of the path and if the processing of the current “end” didn’t end up reaching the finish of the maze, it reprocessed it. I graded it as a failure because it left some cells isolated and it produced more than one path through the maze (at least that specific implementation did that–it might have been able to be modified to avoid one or both of those conditions, but I didn’t pursue that).

    When it was clear that that approach didn’t work, I changed the code to choose cells at random, checking if they had at least one wall missing (meaning they were connected to the start cell through some path) and then opening up one of the remaining walls at random. The code would then, using my solver, check if there was a path from start to finish, and if there was, then the first phase of the construction was done. The second phase consisted of finding all the isolated cells (those that had 4 walls still), and opening up one of those walls at random, repeating until there were no isolated cells left.

    Unfortunately that didn’t work all that well either for similar reasons.

    A solution?

    I don’t have enough notes in code left to reconstruct the different attempts between the recursive non-solution and the queue-based one that actually does a pretty good job so I’m just going to skip to describing this workable solution.

    One of the issues noted above is how the recursive algorithm(s) left some cells isolated and one of the reasons for this happening is that there’s nothing that keeps track of what to try next other than the details in the stack of recursive calls and that information is lost bit by bit when a recursive call returns.

    The way around this is to explicitly keep track of the cells that are still open to being extended, where this means specifically that it’s possible to open up another wall in the cell, where opening up a wall is allowed only if there’s a cell on the other side of the wall (i.e., not off the edge of the maze) and that cell has all four walls in place. This idea of keeping a cell “in play” if it’s possible to open up another wall is key to creating a maze that doesn’t leave any isolated cells.

    Now, it’s possible to keep track of these in-play cells by examining each and every cell each time it’s time to try to open up another cell/wall, but there are some downsides to this:

    1. it’s expensive in time to do this as cells will be needlessly evaluated multiple times once it’s not possible to open up any more of its walls
    2. there’s no way to try to create a long path by continuing to try to extend from the just-opened cell. The control of this turns out to be an important parameter of how the final maze looks.

    All queued up

    The idea of the queue-based solution is to retain in the queue only those cells that might be able to have another wall opened up. We first push the start cell onto the queue, as it’s by definition it has all four walls intact, then we go into a loop that does the following:

    1. terminate if the queue is empty
    2. pop the first cell from the queue
    3. check if the cell can have another wall opened up
    4. if the cell can have one or more walls opened, then randomly choose one of the possibilities and open it up, putting the just-attached cell onto the queue, and put the original cell back onto the queue
    5. if the cell doesn’t have any walls that can be opened, just drop the cell by not putting it back onto the queue.

    That will produce a maze with a path from start to finish. And, interestingly, it doesn’t have to check if it’s solvable as it will always be solvable.

    The details of step 4 determine if the maze is interesting or not, so it’s important to look at the possibilities in step 4. Given that there are two new items to push onto the queue (the just-examined cell–call this “current”–and the just-opened cell–call this “target”) and two ends to push them onto (which is possible using Java’s Queue class), the following enumerates the raw combinations:

    1. “current” at the head of the queue, and “target” at head+1
    2. “current” at the head of the queue, and “target” at the tail
    3. “current” at the tail, and “target” at tail-1
    4. “current” at the tail, and “target” at head
    5. “target” at the head, and “current” at head+1
    6. “target” at the head, and “current” at tail
    7. “target” at the tail, and “current” at tail-1
    8. “target” at the tail, and “current” at head

    There are duplicates: (4) and (6), and (2) and (8), so there are six unique ways of pushing these two cells in step 3 (this ignores the strategy of inserting both at random positions in the queue).

    Since the algorithm pulls the next cell to try to extend from the head of the queue, the maze created depends on what gets pushed onto the head (if anything). If we re-push to the head the cell just taken off, then the algorithm basically expands out the cell as much as possible before moving to the next one, which has a different effect than if the just-opened cells are expanded before reprocessing.

    Interesting mazes

    My code allows me to choose the extension strategy so I can evaluate the effects of them on the final maze. Given that each generated maze is different (due to the use of randomness) it’s not always easy to tell what’s good or not, but in general there are some patterns: (2) and (3) produce mazes that have fairly direct and straight paths from start to finish; (7) and (8) produce slightly more visually complicated mazes than (2) and (3) but whose solutions are equally simple; (1) produces mazes with long paths from start to finish that wind up and down, which look like they’d be hard to solve but really aren’t as there aren’t many deep branches along the way; (4) looks like it produces complicated mazes (more on this below); (5) is similar to (1); and (6) is similar to (1).

    Here are a few simple character-based renderings of some of the various strategies (10×10 maze):

    _________________________________________
    |   |   |   |   |   |   |   |   |   |   |
    | →   →   →   →   →   →   ↓     | →   F |
    |_ _|___|___|_ _|_ _|_ _|_ _|___|_ _|___|
    __ ___________ ___ ___ ___ _______ ______
    |   |   |   |   |   |   |   |   |   |   |
    | ↑ |   |   |   |   |   | ↓     | ↑ |   |
    |_ _|_ _|_ _|___|___|___|_ _|___|_ _|_ _|
    __ ___ ___ _______________ _______ ___ __
    |   |   |   |   |   |   |   |   |   |   |
    | ↑   ←   ← |   |     ↓   ←     | ↑   ← |
    |_ _|___|_ _|_ _|___|_ _|_ _|___|___|_ _|
    __ _______ ___ _______ ___ ___________ __
    |   |   |   |   |   |   |   |   |   |   |
    |   |     ↑   ←     | ↓ |   |       | ↑ |
    |_ _|___|___|_ _|___|_ _|___|___|_ _|_ _|
    __ ___________ _______ ___________ ___ __
    |   |   |   |   |   |   |   |   |   |   |
    |       |   | ↑     | ↓     |     →   ↑ |
    |_ _|___|_ _|_ _|___|_ _|___|___|_ _|___|
    __ _______ ___ _______ ___________ ______
    |   |   |   |   |   |   |   |   |   |   |
    |   |     →   ↑     | →   ↓     | ↑     |
    |_ _|___|_ _|___|___|___|_ _|___|_ _|___|
    __ _______ _______________ _______ ______
    |   |   |   |   |   |   |   |   |   |   |
    |   |     ↑ |   |   |   | →   →   ↑     |
    |_ _|___|_ _|_ _|_ _|_ _|___|_ _|_ _|___|
    __ _______ ___ ___ ___ _______ ___ ______
    |   |   |   |   |   |   |   |   |   |   |
    |   |     ↑   ←   ←   ← |   |   |       |
    |_ _|___|_ _|_ _|___|_ _|_ _|___|_ _|_ _|
    __ _______ ___ _______ ___ _______ ___ __
    |   |   |   |   |   |   |   |   |   |   |
    |       |   |   |     ↑   ←     |   |   |
    |_ _|___|___|___|___|___|_ _|___|___|_ _|
    __ _______________________ ___________ __
    |   |   |   |   |   |   |   |   |   |   |
    |                   |     S     |       |
    |___|___|___|___|___|___|___|___|___|___|
    

    and another

    _________________________________________
    |   |   |   |   |   |   |   |   |   |   |
    | F         | ↓   ← |       |           |
    |_ _|___|___|_ _|_ _|___|_ _|_ _|___|_ _|
    __ ___________ ___ _______ ___ _______ __
    |   |   |   |   |   |   |   |   |   |   |
    | ↑   ←   ←   ← | ↑   ← |   |   |       |
    |___|___|___|___|___|_ _|_ _|_ _|___|___|
    ______________________ ___ ___ __________
    |   |   |   |   |   |   |   |   |   |   |
    |                   | ↑ |   |       |   |
    |_ _|___|___|___|_ _|_ _|_ _|___|_ _|_ _|
    __ _______________ ___ ___ _______ ___ __
    |   |   |   |   |   |   |   |   |   |   |
    |   | →   →   ↓ |   | ↑   ← |       |   |
    |_ _|_ _|___|_ _|___|___|_ _|_ _|___|_ _|
    __ ___ _______ ___________ ___ _______ __
    |   |   |   |   |   |   |   |   |   |   |
    |   | ↑ |   | ↓ | →   ↓ | ↑ |   |       |
    |_ _|_ _|_ _|_ _|_ _|_ _|_ _|_ _|_ _|___|
    __ ___ ___ ___ ___ ___ ___ ___ ___ ______
    |   |   |   |   |   |   |   |   |   |   |
    |   | ↑ |   | →   ↑ | →   ↑ |   |       |
    |_ _|_ _|_ _|___|___|_ _|___|_ _|___|_ _|
    __ ___ ___ ___________ _______ _______ __
    |   |   |   |   |   |   |   |   |   |   |
    |   | ↑ |           |       |   |   |   |
    |_ _|_ _|___|___|_ _|___|_ _|_ _|_ _|_ _|
    __ ___ ___________ _______ ___ ___ ___ __
    |   |   |   |   |   |   |   |   |   |   |
    |     ↑   ←   ← |       |   |           |
    |___|___|___|_ _|___|_ _|_ _|___|___|_ _|
    ______________ _______ ___ ___________ __
    |   |   |   |   |   |   |   |   |   |   |
    |       | →   ↑ |   |   |   |       |   |
    |_ _|___|_ _|___|_ _|_ _|___|_ _|_ _|_ _|
    __ _______ _______ ___ _______ ___ ___ __
    |   |   |   |   |   |   |   |   |   |   |
    |         ↑   ←   ←   ←   S     |       |
    |___|___|___|___|___|___|___|___|___|___|
    

    While both of the above mazes have a solution that looks interesting, only the second one is what I’d call a maze that’s not trivial to solve, as the first one doesn’t have many “deep” branches. The second one used strategy (4) while the first one used strategy (1).

    This is where more (objective) analysis of the strategies is useful but I haven’t done much on that. I can say that there appears to be a balance between the branching factor (basically a normalized number of branches for each cell along the solution path), branch depth (the maximum path length of each incorrect branch), and branch volume (how many cells each incorrect branch occupies in total). I can only say that after eyeballing a number of mazes created by each strategy that strategy (4) creates satisfying mazes–ones that provide ample opportunities to take a wrong turn.

  • Fun with recursion–or how to solve a maze

    When I was really young, maybe ten years old, I loved mazes, both solving them and creating them. That was decades ago, prior to personal computers (I’m dating myself with that disclosure) and so the only mazes we could really solve were in small books my friend and I bought (or could get a parent to buy). The mazes in those books were often done in shapes of dinosaurs or dragons or what have you, and the mazes were (sometimes) made more difficult because of those shapes. We learned to draw mazes, in both the original between the lines and in the style of on the lines. Depending on details such as curves, hatch marks, and size, these mazes we made were easy or hard.

    When I got my first “personal computer” (a TRS-80 Model I, with 16K of RAM with a display that showed 16 rows of 64 columns of uppercase-only text, with the ability to switch into graphics mode that allowed 48 rows of 128 columns of blocky pixels), I (somehow) found a BASIC program that I typed in that would create and solve mazes; at least I’m pretty sure I didn’t create the algorithms by myself, but it’s been so long ago (45 years) that maybe I actually did create it myself. If so, I apologize to my smarter younger self.

    Then, for decades I forgot about all that until we purchased the game quoridor at a local bookstore , where the goal of the game is to get a pawn from your side to the opposite side where the path to the other side can be (will be) blocked by walls that you and your opponent put up between the squares. After playing it I thought about writing a program that would play it better than humans could (even though the branching factor on each move is well over 100–more than chess has, if I recall correctly).

    One of the rules of the game–which is what prompted my recall of all the maze-making and -solving days in my past–is that no placement of a wall can totally block a player from reaching the other side. This translates to making sure that there’s some path from the current position to the finish, which is essentially the same as solving a maze.

    There are significant differences between a well-crafted maze (i.e., a single non-trivial path from start to finish) and the path problem presented by quoridor rules (i.e., sparse wall placement, allowing many paths from start to finish), but the general problem is the same.

    So I started a quick prototype of a computer game and got sidetracked on the movement rule that disallows a complete block to the finish, and after a false start on a generalized maze-solving algorithm, I got a simple algorithm fully working, and it’s this algorithm that I’m covering in this post, mostly because it brings back a lot of good memories of just working through a completely abstract problem.

    What is a maze?

    From a conceptual perspective (with a big, big, lean toward computer science concepts), a maze is just a tree with the start position as the root of the tree and some leaf node (or nodes) being the finish. This is packed into a visual 2D form that is intended to make it difficult to see the path from root to leaf. (Note: if the maze is not a tree, then there is at least one node that has two distinct paths to it, which can make it easier to solve the maze.)

    So in essence solving a maze is equivalent to walking a tree structure, and the algorithm presented here does indeed traverse a tree structure in a typical recursive fashion.

    What is recursion?

    I’m not sure who will end up reading this post, so I don’t know what to expect in terms of knowledge of computers and programming, so I’ll cover the concept of recursion very briefly.

    To recurse is for some operation to refer back to itself only with simpler information. The prototypical mathematical function that’s defined in terms of recursion is the fibonacci number:

    fib(n) = fib(n-1) + fib(n-2)   ; for n > 1
    fib(0) = 1
    fib(1) = 1

    In some sense recursion breaks down a problem into a simpler one until the problem size gets to an easily solved (or defined) case.

    Tree walking

    If the goal of solving a maze is equivalent to finding a path from the root of a tree to a specific leaf node(s), then we just need to walk the tree keeping track of just how we got from root to leaf. There are two general ways of tree walking: breadth-first and depth-first. The basic difference between the two is that depth-first walking usually uses recursion and a parallel stack to keep track of the (current) path while breadth-first uses a queue to visit all the tree nodes.

    As this post is implying that recursion is fun, it should be clear that for this problem that recursion was chosen as the way to solve a maze.

    Problem details

    For our purposes, let’s say the maze is on a square grid of size n, meaning that there are n^2 “cells”. Let’s further say that it’s possible–subject to wall placement–to move only in one of four directions from a given cell: up, left, down, right. Walls between adjacent cells disallow a move between the cells, and a move from one side of the grid directly to the opposite side (via “wrapping around”) is never allowed.

    Additionally, let’s say the start position is given as start and can be any cell on the edge, and finish is also any cell on the edge (things get more interesting if a few things are relaxed: if start and finish are not on the edges and the maze is not a tree, then I believe it might not be always solvable as the algorithm could end up going in circles).

    Finally, let’s say that the maze maker did the proper job and the maze is actually solvable, meaning that there really is a path from the root of the tree to the finish leaf–if that’s not the case then there wouldn’t be a single tree that represents the problem.

    With the above, we’re now ready to jump into the recursive algorithm…

    Recursive solution

    As mentioned above, the idea of recursion is to break the problem down to a smaller problem and the apply the same algorithm to the smaller problem. In our case, the “smaller problem” is looking for a path that’s one move shorter than where we currently are in the maze–basically we’ll see if we can take a step in the right direction and then try to solve the maze from that just-stepped-into position in the maze.

    Let’s assume (with good reason) that the current position (cell) in the maze that we’re currently at is a valid position, meaning very specifically that there’s a path from the start position to the current position. This is trivially true if we start at the start position–there’s a path (of zero steps) from the start to there. From that point on, if we only take valid steps then there will still be a path from the start to the current position. So we just have to define what a valid move is, which is pretty easy: a valid move is one that meets the following conditions:

    • it doesn’t go outside the grid
    • it doesn’t go through a wall
    • it doesn’t go into a cell that’s already been visited

    The final point is to check if we’ve actually solved the maze–that we’ve reached the finish. This is equivalent to defining what fib(0) and fib(1) are above. So, if our current cell is the finish, then we’re done–we’ve found a path from start to finish.

    With that in mind, the basic algorithm in sort-of-code for solve() is

    solve(position)
      IF at-finish(position)
        RETURN true
    
      visited[position] = true
    
      IF ((can-move(position, UP) AND solve(move(position, UP)) OR
          (can-move(position, RIGHT) AND solve(move(position, RIGHT)) OR
          (can-move(position, DOWN AND solve(move(position, DOWN)) OR
          (can-move(position, LEFT AND solve(move(position, LEFT)))
        success = true
    
      visited[position] = false
      
      RETURN success   

    The core algorithm really is that simple–and it doesn’t matter what order the UP, RIGHT, DOWN, LEFT are done in, as long as all four directions are tried. I’ve left out the definition of “can-move” but it really just tests the three conditions above.

    Getting the path

    While it’s true that the algorithm to find a path from start to finish is as simple as what’s shown above, the path from start to finish is held only in the recursion info of which particular line of code above that’s doing the can-move(position, direction) AND solve(move(position, direction))— each correct step (can-move line) is encoded by which one of the four lines was chosen.

    In order to be able to extract the path in a more convenient form, it’s necessary to use a stack, where a position is pushed to the top of the stack when it’s visited, and then removed from the top when we’re done visiting it. On success at finding the finish, we just don’t remove anything from the stack, and then the path is the whole set of cells from the stack.

    Finally, some code…

    Here’s some (verbose) Java code that puts all the above together (and that is a bit more generalized in that it can find all solutions):

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Stack;
    
    public class Visitor
    {
        private int[][] visited;
        private int     size;
        private Grid    grid;
    
        /**
         * Non-static class so it has access to visited grid
         */
        private class VisitContext
        {
            private List<List<Cell>> solutions = new ArrayList<>();
            private Stack<Cell>      path      = new Stack<>();
            private int              depth;
            private int              maxDepth;
            private Cell             finish;
            private boolean          stopAtFirst;
    
            VisitContext(boolean stopAtFirst)
            {
                this.stopAtFirst = stopAtFirst;
            }
    
            void enter(Cell pos)
            {
                path.push(pos);
                depth++;
                maxDepth = Math.max(depth, maxDepth);
                visited[pos.getCol()][pos.getRow()]++;
            }
    
            void exit(Cell pos, boolean solved)
            {
                visited[pos.getCol()][pos.getRow()]--;
                if (!solved)
                    pos = path.pop();
                depth--;
            }
    
            boolean addSolution()
            {
                solutions.add(new ArrayList<Cell>(path));
    
                return stopAtFirst;
            }
    
            boolean finished(Cell pos)
            {
                return ((pos.getCol() == finish.getCol()) &&
                        (pos.getRow() == finish.getRow()));
            }
    
            Stack<Cell> getPath()
            {
                return path;
            }
    
            int getMaxDepth()
            {
                return maxDepth;
            }
        }
    
        /**
         * Grid is what has the walls defined to make the maze
         */
        public Visitor(Grid grid)
        {
            this.grid = grid;
    
            size    = grid.getSize();
            visited = new int[size][size];
        }
    
        /**
         * Find a path(s) from start to finish, returning true if a path was found
         */
        public boolean findFinish(Cell start, Cell finish)
        {
            VisitContext context = new VisitContext(true);
            boolean      solved;
    
            context.finish = finish;
    
            solved = solve(start, DirVec.RIGHT, context);
    
            if (!solved)
            {
                System.out.println("No solution found");
            }
            else
            {
                for (List<Cell> soln : context.solutions)
                    System.out.println("solution: " + soln);
    
                grid.setPath(context.solutions.get(0));
            }
    
            return solved;
        }
    
        /**
         * The recursive algorithm
         */
        private boolean solve(Cell pos, DirVec dir, VisitContext ctx)
        {
            boolean solved = false;
    
            if (ctx.finished(pos))
                return ctx.addSolution();
    
            ctx.enter(pos);
    
            if (canMove(pos, dir) && solve(dir.apply(pos), dir, ctx))
                solved = true;
            else if (canMove(pos, (dir = dir.rotateLeft())) &&
                     solve(dir.apply(pos), dir, ctx))
                solved = true;
            else if (canMove(pos, (dir = dir.rotateLeft())) &&
                     solve(dir.apply(pos), dir, ctx))
                solved = true;
            else if (canMove(pos, (dir = dir.rotateLeft())) &&
                     solve(dir.apply(pos), dir, ctx))
                solved = true;
    
            ctx.exit(pos, solved);
    
            return solved;
        }
    
        private boolean canMove(Cell pos, DirVec dir)
        {
            return (grid.canMove(pos.getCol(), pos.getRow(), dir.asBits()) && notVisited(dir.apply(pos)));
        }
    
        private boolean notVisited(Cell pos)
        {
            return visited[pos.getCol()][pos.getRow()] == 0;
        }
    
    }
    

  • First experience with a All-In-One (water) CPU cooler

    I’m finding out how sensitive I am to certain stimuli (and it does seem like I’m getting more sensitive over time) and one of those things I am sensitive to is the fan noise of my computer. This latest computer I built is less than five months old and when I built it I wanted to use the current generation of AMD’s Ryzen as my then-current computer was a couple of generations behind, so I went with a Ryzen 7900X knowing that they ran hot and that I’d need a good CPU cooler.

    I chose the Noctua NH-D15, with two towers and two fans. It has kept the CPU well within a reasonable temperature range, and I was happy with how low the idle temps were (around 39 C) but when compiling code it would make quite a noise even though the computer was across the room (with 35 foot HDMI cables…that was fun getting that that length to work).

    When I moved my office temporarily into a smaller room in the house I ended up putting the computer no more than 4 feet from me and that fan noise started to sound a lot louder. And it didn’t help that running (far too many) Chrome and Firefox processes and tabs ended up with the CPU running higher than normal idle and so there was a more or less constant fan noise.

    I decided to try an All-In-One (AIO) water cooler thinking it would cut down a lot on the noise, so I did my research and ordered an Arctic Liquid Freezer II 360 and a case that would it would fit in (yet another purchase…sigh). I knew it would be relatively straightforward to move all the components over to the new case, though it would take a few hours for me.

    So late one day this last week I started on it all. The new case (Fractal Design R5) is well designed and I did a test fit of the AIO cooler and then started moving the PSU and motherboard over. All was straightforward, relatively speaking as this was the first AIO I’ve worked with; I did have to make sure that it only used the CPU FAN header on the motherboard–I didn’t know a single header could drive the pump and the three fans, but that’s the setup.

    I got everything together and then plugged all the cables back in and turned it on. And, a POST failure on “VGA” LED. Along with a horrible sound from the PC piezo speaker that I’ve carried along from build to build (as no one seems to supply them anymore). Turn it off, unplug, check and reseat things. Back on, still VGA failure and buzzing, so now I’m thinking my RX6600 GPU somehow got fried (how??) so I put in my backup RX6600 and it also fails, so now I’m thinking it’s possibly the motherboard.

    Next step is to remove the GPU and rely on the iGPU in the Ryzen 7900x. Same VGA failure. Seems like it has to be the motherboard. But before I declare final defeat, I thought it wise to reset the CMOS so I do that, then boot up and still VGA failure, but the awful speaker buzz stops, so maybe progress?

    All except the first test had been without any cables (HDMI, USB, RJ45) plugged in (just easier to do) and I decided, for some reason, to plug in the HDMI cable just in case anything showed up on screen. It was then that I noticed that my KVM switch was off–I had forgotten that I had turned that off for some unknown reason before I started this whole side-grade process. I turned that on, made sure the display was on, rebooted the computer and it worked.

    Now, my perhaps wrong understanding from all my decades of building computers is that POST does not (or at least, usually doesn’t) require a display to be connected and turned on–memory and CPU of course, but other components? I’ve not used a Gigabyte motherboard before so maybe it’s specific to these boards. I don’t know, so while I’m quite happy all was working, I was grumbling about the cause of the apparent failure.

    So, word to the wise on this: when testing new/changed configurations at the motherboard level, make sure there’s a display plugged into the computer.


    Now for the results of my AIO side-grade. I’m mostly happy in that it’s definitely quieter in general, though when I do a build the three fans will kick in. The CPU temps are also good. The problem is that I sometimes hear a high pitched ringing that is coming from the AIO unit and that’s almost as annoying as the Noctua cooler was. I’ve tightened the screw and have adjusted the fan speed profile at the UEFI level but it still is occurring.

    Lastly, while I like the Fractal case, the USB header on the motherboard for the front-panel connector is a bit too close to the bend in the case for the pass-through to the cable management area on the backside of the motherboard tray and it was (still is) really hard to get the right angle to connect the cable to the header. So I bent a few of the pins and I can’t seem to get them unbent so I have no USB on the front panel of the case. This is not as bad as it could be from my perspective as the whole computer is squeezed into a modified shelf unit so there’s no room for me to plug anything into those USB slots. But still. I’m going to see if there’s anything I can buy that will help unbend those pins…

  • Setting up LUKS on a new SSD

    I’m not a linux OS expert even though I’ve been using linux for over twenty years (that said, I’d say that most anyone who installs and uses linux has to have a good amount of technical knowledge). It wasn’t until somewhat recently that I realized I really should have Full Disk Encryption (FDE) on the NVMe SSD drives that I’ve been installing on the last few computers builds I’ve done (and, for sure, it’s wise to use some encryption if there’s any private or important information on a drive–a simple delete of files doesn’t actually overwrite the data on the disk of deleted files usually; this advice applies regardless of NVMe or SATA form-factor). So, as I’ve been building newer systems and adding drives, I’ve been setting them up with with FDE using LUKS (Linux Unified Key Setup), and have been quite happy with the simplicity and the security of it all.

    This article goes over the steps needed to set up LUKS on a new, non-boot SSD drive–in this case it’s a new 1TB Crucial MX500 2.5″ SATA drive.

    I’m going to label Step 0 as “install the physical SSD drive in the computer” and not go into any details at all there. For my setup, I just plugged it into a SATA connector and confirmed via UEFI setup screens that it’s detectable at the hardware level.

    Step 1 is to locate the block device in linux itself. While a non-root user can do a few of these commands, you really need to be root, and I tend to just do a simple sudo bash and then do all the commands while in the root shell. So open up a shell:

    [scott] $ sudo bash
    [root] # 

    and list block devices:

    [root] # lsblk
    NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
    sda                                             8:0    0 931.5G  0 disk  
    sr0                                            11:0    1  1024M  0 rom   
    zram0                                         252:0    0     8G  0 disk  [SWAP]
    nvme0n1                                       259:0    0   1.8T  0 disk  
    ├─nvme0n1p1                                   259:1    0   600M  0 part  /boot/efi
    ├─nvme0n1p2                                   259:2    0     1G  0 part  /boot
    └─nvme0n1p3                                   259:3    0   1.8T  0 part  
      └─luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 253:0    0   1.8T  0 crypt /home
                                                                             /
    nvme1n1                                       259:4    0 931.5G  0 disk  
    └─nvme1n1p1                                   259:5    0 931.5G  0 part  
      └─crucialx                                  253:1    0 931.5G  0 crypt /crucial

    You can see the already-setup FDE on crucialx and luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3–both have type crypt. The new MX500 is a SATA drive and it shows up as sda–not even partitioned, so the next step is to partition it.

    Note also that there’s another way to list the new device: fdisk -l, one advantage of which is that it also displays the full /dev path and the drive model info:

    [root] # fdisk -l
    Disk /dev/sda: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
    Disk model: CT1000MX500SSD1 
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 4096 bytes
    I/O size (minimum/optimal): 4096 bytes / 4096 bytes
    ...

    (output has been truncated to show just the new drive).

    Partitioning can be done with fdisk (or, if the device is more than 2TB, the GNU utility parted needs to be used) and the steps for this are to create a new primary partition (assuming all of the drive is to be part of the FDE process), write out the partition table, and then make sure the OS uses this new information:

    [root@titan scott]# fdisk /dev/sda
    
    Welcome to fdisk (util-linux 2.38).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.
    
    Device does not contain a recognized partition table.
    Created a new DOS disklabel with disk identifier 0x0cbc7bb1.
    
    
    Command (m for help): n
    Partition type
       p   primary (0 primary, 0 extended, 4 free)
       e   extended (container for logical partitions)
    Select (default p): p
    Partition number (1-4, default 1): 
    First sector (2048-1953525167, default 2048): 
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-1953525167, default 1953525167): 
    
    Created a new partition 1 of type 'Linux' and of size 931.5 GiB.
    
    Command (m for help): w
    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
    
    
    [root] # partprobe

    Use lsblk to see the new partition:

    [root@titan scott]# lsblk
    NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
    sda                                             8:0    0 931.5G  0 disk  
    └─sda1                                          8:1    0 931.5G  0 part  

    The new partition is /dev/sda1. Now it’s time for LUKS setup.

    LUKS setup really requires that you understand a bit of the overall model of it before jumping in. The high level idea of how it operates here is that LUKS sits between the filesystem (such as btrfs or ext4) and the raw block device (like /dev/sda1) doing the encryption of data written and the decryption of data read. The LUKS command that manages this in-between part is cryptsetup and the basic operations needed for this management are as follows:

    • cryptsetup luksFormat: formats a raw block device so it can be used with the LUKS system
    • cryptsetup luksOpen: opens up a formatted LUKS block device and assigns a logical ID to id
    • cryptsetup luksClose: closes a device opened up with luksOpen
    • cryptsetup luksAddKey: adds a new encryption key to the device
    • cryptsetup luksDump: lists the encryption key info on the device

    As LUKS uses AES256 encryption, the question of how the keys are stored comes up. It turns out that there’s a single AES256 master key and it’s encrypted by secondary keys. LUKS provides 8 “slots” (slot storage is created during the luksFormat operation and are filled via the luksAddKey command) to store the keys used to decrypt the master key. The keys stored in these 8 slots can be created via a passphrase that’s stretched into the required number of bits (LUKS version 2 uses Argon2 for that), or via a good secure random number (bit) generator. Both of these methods will be used so that the FDE is automounted during bootup.

    The usefulness of the 8 slots is that you can specify a passphrase (or two) while having another secure random key of 256 bits that can be used for scripts or during bootup, all of which will allow decryption of the master key that is used for actual encryption and decryption. (And, having multiple slots allows multiple people to unlock, say, a boot drive that has FDE without sharing passwords.)

    Before jumping into the actual operations to use LUKS, it’s important to understand the logical ID that is required for some operations: this logical ID is the “handle” to a block device/partition that’s been opened by LUKS and it shows up in the file system (under /dev/mapper/). It must be unique across mapped/opened block devices and I’ve found it useful to have a convention of appending x to a useful name to indicate that it’s encrypted, like mx500x.

    Step 2 is to put it all together:

    [root]# cryptsetup luksFormat /dev/sda1
    
    WARNING!
    ========
    This will overwrite data on /dev/sda1 irrevocably.
    
    Are you sure? (Type 'yes' in capital letters): YES
    Enter passphrase for /dev/sda1: 
    Verify passphrase: 
    
    [root]# cryptsetup luksOpen /dev/sda1 mx500x
    Enter passphrase for /dev/sda1: 
    [root]# ls -l /dev/mapper/
    total 0
    crw-------. 1 root root 10, 236 Mar  5 06:50 control
    lrwxrwxrwx. 1 root root       7 Mar  5 06:50 crucialx -> ../dm-1
    lrwxrwxrwx. 1 root root       7 Mar  5 06:50 luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 -> ../dm-0
    lrwxrwxrwx. 1 root root       7 Mar  5 07:53 mx500x -> ../dm-2
    [root]# 

    For the passphrase, choose a secure (long) one and do not forget it as you will lose access to all data on the FDE drive (well, not 100% true if you set things up for automount by using the secure random key generated during that process). And to show the effect of closing a logical ID:

    [root]# cryptsetup luksClose mx500x
    [root]# ls -l /dev/mapper/
    total 0
    crw-------. 1 root root 10, 236 Mar  5 06:50 control
    lrwxrwxrwx. 1 root root       7 Mar  5 06:50 crucialx -> ../dm-1
    lrwxrwxrwx. 1 root root       7 Mar  5 06:50 luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 -> ../dm-0
    [root]# 
    

    At this point all the LUKS setup has been done and it’s time for Step 4 where the drive is formatted. The device does need to be opened (via cryptsetup luksOpen) and then it’s easily formatted; I have recently been using btrfs instead of ext4, so the following is what I did:

    [root]# cryptsetup luksOpen /dev/sda1 mx500x
    Enter passphrase for /dev/sda1: 
    [root]# mkfs.btrfs /dev/mapper/mx500x 
    btrfs-progs v6.1.3
    See http://btrfs.wiki.kernel.org for more information.
    
    NOTE: several default settings have changed in version 5.15, please make sure
          this does not affect your deployments:
          - DUP for metadata (-m dup)
          - enabled no-holes (-O no-holes)
          - enabled free-space-tree (-R free-space-tree)
    
    Label:              (null)
    UUID:               8bc72d72-09f9-478a-b21e-dc0a76bf7eda
    Node size:          16384
    Sector size:        4096
    Filesystem size:    931.50GiB
    Block group profiles:
      Data:             single            8.00MiB
      Metadata:         DUP               1.00GiB
      System:           DUP               8.00MiB
    SSD detected:       yes
    Zoned device:       no
    Incompat features:  extref, skinny-metadata, no-holes
    Runtime features:   free-space-tree
    Checksum:           crc32c
    Number of devices:  1
    Devices:
       ID        SIZE  PATH
        1   931.50GiB  /dev/mapper/mx500x

    and then to mount it:

    [root]# mkdir /mx500
    [root]# mount /dev/mapper/mx500x /mx500/
    [root]# df -h
    Filesystem            Size  Used Avail Use% Mounted on
    devtmpfs              4.0M     0  4.0M   0% /dev
    tmpfs                  31G  736M   31G   3% /dev/shm
    tmpfs                  13G  2.4M   13G   1% /run
    /dev/dm-0             1.9T  267G  1.6T  15% /
    tmpfs                  31G   45M   31G   1% /tmp
    /dev/dm-0             1.9T  267G  1.6T  15% /home
    /dev/nvme0n1p2        974M  318M  589M  36% /boot
    /dev/nvme0n1p1        599M   18M  582M   3% /boot/efi
    /dev/mapper/crucialx  932G  367G  565G  40% /crucial
    tmpfs                 6.2G  2.4M  6.2G   1% /run/user/1000
    /dev/mapper/mx500x    932G  3.8M  930G   1% /mx500

    At this point /mx500 is fully usable and all data written to it will be encrypted.

    For convenience I (strongly) suggest setting it up so that the FDE drive is automounted during OS bootup. This can be a bit more complicated than desired if you have more than one NVMe drive, as the assignment of /dev drive is dependent on the scan done during bootup and isn’t 100% predictable (as I found out later). If you do have more than one NVMe drive, read the section lower down also.

    Setting up automount during bootup requires the following:

    • create a secure random key, saved to a protected file
    • edit /etc/crypttab to add a line that will do an automatic open on the device during bootup
    • edit /etc/fstab to add a line that will do an automatic mount of the opened device

    I’ve put the the secure random key file under /root directory as that’s protected and isn’t likely to be mistakenly deleted or modified. It can be created like this:

    [root]# dd if=/dev/random bs=32 count=1 of=/root/lukskey.mx500x
    1+0 records in
    1+0 records out
    32 bytes copied, 5.4303e-05 s, 589 kB/s

    If you haven’t used dd (“data duplicator”) before, what that command is doing is copying 32 bytes (bs=32 count=1: 1 block of size 32) from /dev/random to /root/lukskey.mx500x. It’s a very useful command (but can be dangerous if you don’t know what you’re doing as it can and will write to raw block devices (and totally destroy the data on them).

    Now the new 32-byte key must be added to one of the 8 slots:

    [root] cryptsetup luksAddKey /dev/sda1 /root/lukskey.mx500x
    Enter any existing passphrase: 
    [root]

    Once you’ve created the key file, add a line like the following to /etc/crypttab (which automates the luksOpen done manually above):

    mx500x   /dev/sda1  /root/lukskey.mx500x

    and finally a line like the following can be added to /etc/fstab (which automates the mount done manually above):

    /dev/mapper/mx500x   /mx500   btrfs   defaults  0 0

    And that’s all the setup needed if you don’t have more than one NVMe drive. At this point, double-check paths that were entered into /etc/*tab files as errors here will cause bootup problems. Reboot the computer to test it all out to make sure everything gets automounted.

    If you have more than one NVMe drive, then it’s necessary to use the UUID of the partition as the actual drive number (e.g., the 0 in /dev/nvme0n1) is assigned during a boot-time scan of devices. And there are actually two UUIDs that need to be used: one for use in /etc/crypttab and one for use in /etc/fstab. In order to get the UUIDs, use the following command (the important UUIDs are in bold):

    $ sudo lsblk -o +name,mountpoint,uuid
    [root@titan scott]# lsblk -o +name,uuid
    NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS NAME                  UUID
    sda                                             8:0    0 931.5G  0 disk              sda                   
    └─sda1                                          8:1    0 931.5G  0 part              sda1                  561e1265-0150-437c-b979-4bf4a5a1ff49
      └─mx1000x                                   253:1    0 931.5G  0 crypt /mx1000     mx1000x               8bc72d72-09f9-478a-b21e-dc0a76bf7eda
    sdb                                             8:16   0   3.6T  0 disk              sdb                   
    └─sdb1                                          8:17   0   3.6T  0 part              sdb1                  d8347463-e812-4293-ac9b-c9dae64d2992
      └─mx4000x                                   253:2    0   3.6T  0 crypt /mx4000     mx4000x               331eef2c-4ef9-47d7-a856-8ef28e928a90
    zram0                                         252:0    0     8G  0 disk  [SWAP]      zram0                 
    nvme0n1                                       259:0    0   1.8T  0 disk              nvme0n1               
    ├─nvme0n1p1                                   259:1    0   600M  0 part  /boot/efi   nvme0n1p1             B4CD-57EE
    ├─nvme0n1p2                                   259:2    0     1G  0 part  /boot       nvme0n1p2             c0d6df9c-9664-4174-99a3-4c207a52f318
    └─nvme0n1p3                                   259:3    0   1.8T  0 part              nvme0n1p3             822c7f1b-7d42-48ac-b2f6-3758ceecc5b3
      └─luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3 253:0    0   1.8T  0 crypt /home       luks-822c7f1b-7d42-48ac-b2f6-3758ceecc5b3
                                                                                                               66cead1d-0664-4f46-99a8-0b3ddb8891f5
                                                                             /                                 
    nvme1n1                                       259:4    0 931.5G  0 disk              nvme1n1               
    └─nvme1n1p1                                   259:5    0 931.5G  0 part              nvme1n1p1             40818881-4df8-45a2-8c34-390e1faaca5d
      └─luks-40818881-4df8-45a2-8c34-390e1faaca5d 253:3    0 931.5G  0 crypt /crucial    luks-40818881-4df8-45a2-8c34-390e1faaca5d
                                                                                                               2686e2f5-4a18-4be3-9340-79296349a7af

    The first UUID (that’s associated with the crypt type partition of the device) is used in /etc/crypttab like so:

    luks-40818881-4df8-45a2-8c34-390e1faaca5d UUID=40818881-4df8-45a2-8c34-390e1faaca5d /root/lukskey.crucialx

    and the second UUID (that’s associated with the mounted partition) is used in /etc/fstab like so:

    UUID=2686e2f5-4a18-4be3-9340-79296349a7af /crucial                btrfs   defaults        0 0

    (I haven’t verified the following but I think that it’s necessary to do a luksOpen and mount of the partition to get the second UUID.)

  • Webcams and USB cables on linux

    March 2023

    And here I thought USB was compatible (mostly) with itself. Apparently, I still have a lot to learn about USB even though I’ve been in the computer field for 40+ years.

    Last week I was so looking forward to getting Zoom to work on linux. I had just found out–and I don’t recall how this happened–that Zoom supports linux and given that I do 98% of my work on linux (Fedora, for-ever) with the other 2% being on macOS for all the video calls I’m on, I thought it would be so nice to not have to twist my body to face the macbook on one side of my desk. So I ordered what looked to be a reasonable webcam (Logitech’s Brio 300) and waited for it to arrive. I had to order a USB-C to A adapter as the goal was to plug it into my KVM switch so multiple machines could make use of it.

    When both parts arrived I plugged things in, somewhat naively expecting it all to just work. And, of course it didn’t. So I plug it in (with no C to A adapter) to my mac and all seems well, leaving me disappointed with linux and with my researching things prior to ordering.

    So I start digging into what might be the problem. I quickly find out that the subsystem in linux for video is v4l2 (Video 4 Linux 2, though I’m guessing that the “2” means major changes from “1”, but I didn’t look enough to verify) and I install v4l2 tools and start trying to get those to work. The computer can see the webcam device itself, but all I see is black for any video app.

    Eventually I give up (mostly) and let it sit for a few days. And I think about ordering a known compatible model but after clicking through so many links I’m not liking what I’m seeing (“discontinued model” showed up more than once). I eventually started searching again for possible problems with Brio 300 and linux and I finally hit upon a review written in German (which Google offered to translate) and when I read it (translated well enough, I suppose) there was as a mention of MJPEG and YUYV and native hardware operations not working if a USB 2 cable is used… and, of course that adapter drops it from USB 3 to 2 and so I thought that I should try going directly from the webcam into the motherboard USB 3 connector.

    And it worked and I was a bit (or more) surprised as the bandwidth used by a HD webcam at 30fps should *not* overwhelm USB 2. Okay, whatever. So then I start plugging in the cable+adapter (to type A) into USB 3.2, 3.1, and 2 ports and it becomes clear that it works only with 3.0/3.1 (blue/teal ports)–not 3.2 (red) as far as I can tell.

    So after ordering and using a 6ft USB 3.1 extension cable and plugging it into the 3.1 (A connector) ports with the adapter it all works.

    Thinking that maybe this little webcam is using a large amount of the bandwidth, I found out about usbtop and so I ran it, and it shows that it’s using only around 24Mbps, or a small portion of the USB 2 bandwidth (480Mbps). Sigh.

    Moral of the story is that backwards compatibility isn’t always, and that it’s good to bypass “transformations” of USB data (like those that come with downgrading to USB 2). I suppose related to this is my saga of having a really nice ASUS Republic of Gamers mechanical RGB keyword not work if it’s connected to a USB 2 KVM switch…


    Update (a few days after the above was written)… well, it’s not quite so simple as it turns out it is possible to throw USB 2 in the mix and have it work. Either the combination of adapters (and orientation of the C-to-C connection) or the particular USB 3.2 (?) port used on the motherboard is important (or both), as I was able to get the Brio working using a USB 2 female-to-male extension cable as long as the orientation of connections was correct and the USB type A connector was plugged into the right motherboard USB connector. The working combination requires the manufacturer’s logo on the connectors/cables and the generic USB “network” symbol to be on the same side of the connection. Sigh.