Tuesday, July 16, 2024

EPUB format thoughts

 How hard can EPUB files be?

"EPUB is just HTML"! Hah!

I've got a fun EPUB ebook reader in the store; it's got two nifty features that, IMHO, all ebook readers should have (it can do an offline search of Project Gutenberg, and it's got a two-screen mode, so you can see both a critical image and the text that talks about the image in one spot).

Over the years, I've had to work around a lot of EPUB failures. Today's failure is thanks to the newer EPUB books that Project Gutenberg publishes.

Notably, each book seems to include a (pointless) <pre/> tag. The problem with that tag is that HTML does not support self-closing (void) elements. The Mozilla pages are super clear about that.

So my HTML renderer (which is just a WebView element) takes the <pre/> tag and reads it HTML style, like a <pre> tag that isn't closed. The entire rest of the book, which is most of it, is then displayed with pre-formatted lines. Because pre-formatted lines don't wrap (that's the point of the <pre> tag, the rest of the reading experience is mostly ruined.

SIGH. 


Wednesday, May 8, 2024

First thoughts on std::expected

 std::expected -- what can you do with it?

There's  a fancy new thing in C++: std::expected. There's a lot of pots about it's awesomeness, but what is it, really?


Answer: if you've got a std::expected, you can do three important things. Let's give a name to our std::expected: it's called result. And as a reminder: std::expected holds one of two values, which in this post I'll cleverly call "first" or "second".

1. Just result by itself is  a bool. When first is set, it returns true; when second is set, false.

2. *result is just first.

3. result.error() returns second.

You can also chain them together, assuming you have code where you want to do a bunch of stuff in order and will never have to refactor the code to be multi-threaded, or handle error conditions weirdly, or log something and then fail, or a lot of things. Oh, and using functions means lots of global variables, and the function must return a std::expected, not an int.

1. result.and_then(function) will call function with *result. The function can take exactly one variable, and the function will only be called if result was set to first.

2. result.or_else(function) is like the opposite of and_then. It's also a function that takes a single parameter (love them globals!) but take in the type of second, not first, and is only called if the second was set (the 'unexpected' branch).

Want to make a std::expected?

If you need to return a std::expected, and you're setting the first value, just return it and it's automatically cast into an object as needed. If you need to return the second value (unexpected), return a std::unexpected(). Be sure to not new it; just return std::unexpected("second value").


And some useless crap

What if you you've got a std::expected where first is an int, but you really need a double? Can you cast it? Nope, you can't. Casting is now called "transform".

There's a tranform_error(), too.

First thoughts: what about exceptions?

Lord knows. I guess exceptions aren't a thing any more?

What happens if I need to set a breakpoint? Answer: good luck with that. 

The source code the std::expected iat about 1500 lines of 

Sunday, April 14, 2024

XAML Style Dictionaries

Step by step: adding consistent styles to a XAML app

XAML has a useful feature where you can set up a single file with a bunh of "style" configuration for your XAML (UWP) app, and then use those styles everywhere in your app. The plus side is having a centralized style configuration: all your titles can be one size, and all your technical text has the correct font, and all. The downside is that getting it set up is buried in a blizzard of docs. There's a lot of ways to set up your XAML styles, and Microsoft wants you to fully understand each of them in isolation.

In this simple blog post, I walk through the steps of one way to add centralized styles to your app and then be able to use them everywhere.

Create an AppDictionary.xaml file. In your project, Add > New and add a XAML ResourceDictionary. Call is AppDictionary.xaml.

Add your first style. I tend to prefix all my centralized styles with a small "s" like "sTitle".

    <Style x:Key="sTitle" TargetType="TextBlock">
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="FontSize" Value="24" />
    </Style>    


Update every page and control to include the AppDictionary

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="AppDictionary.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>


Use the new style.  In a textblock, set the Style to be {StaticResource sTitle}.

        <TextBlock Style="{StaticResource sTitle}">Thingy Data</TextBlock>

Now you're styling with style, and it just took one short blog post to replaces multiple lengthy pages of explanation from the learn.microsoft.com site!

Thursday, December 28, 2023

Overview: Installers for .NET Core

 Installer types for .NET Core applications

I've got a new application, and I want a nice installer for it. Visual Studio has a metric ton of different installer projects, variously called "publisher" and "setup" and "install" and "deploy" -- so many, many different adjectives.

TL/DR: Make a setup project

My goal is an installer that is a single file, probably .EXE but I'll do a .MSI or .MSIX or whatever. Additional points are awarded for easy instructions, instructions that are less than 5 years old, a simple build process, output that can be placed somewhere other than the "bin" directory, and, of course, an installer that actually works.

My project, FWIW, is a .NET "Core" (now it's just called ".NET") -- that's the new thing that replaces the "Framework" version of .NET. It's using some of the WinRT APIs, so the output type is "WinExe" and the target framework is "net7.0-windows10.0.22000.0".

Publish (Folder) from main project (23 MB)

Creates a folder with your real .EXE plus a .DLL plus some more DLLs plus a .JSON file plus more random files. They are pretty much all needed, and there's no actual "installer"; it's just random files that you can write an installer for.
Points deducted: not a single file, no installer

Publish (Click Once) from main project (24 MB)

Creates a folder with an MSI for installing, plus also a .EXE for installing, plus also a random set of "Application files" which is full of random files that have all been renamed. The output folder must be in the bin folder.
Points deducted: terrible multi-install experience, wizard is broken, very old docs

The multi-install experience happens when you do an install to test the installer, and then install from literally any other place. Instead of upgrading, or removing the old app, or installing side-by-side, the installer just fails with no suggested course of action.

The publish "wizard" is broken: you can select stuff to change in the installer. But when you select, for example, the "Sign Manifest" "tab", you don't get brought to that experience; the dialog box is unchanged. Instead you have to click the "next" button to get from one "tab" to the next. 

The documentation for "What's new" is from 2012. 

Setup Project from new project (7 MB)

Creates an .MSI for installing, plus a pointless SETUP.EXE that only works with the .MSI. This means I had to waste time figuring out which file I really needed. 

Points deducted for: bizarre and undocumented "editor". To use it, right-click the "Application Folder" and select "Add" and then "Project Output" and give the original project. 

The created installer is a clunky, old-fashioned installer, so the user has to click "next", then look at stuff they won't want to change, then click next again, click next on the UAC prompt, and then click close. And it gives a little warning to be sure to check with Windows Update for updates to .NET Framework, which this app, AFAICT, doesn't use at all (it's .NET Core). 

I really want to deduct points for the "Transitive" property, the description of which is "Determines whether the installer will reevaluate the Condition property for the selected item when reinstalling on a target computer". The words condition, property, select, and target computer really just raise new questions. 

Sunday, September 24, 2023

Project: Govee E-Ink display

 The Govee H5074 E-Ink display project!

All complete! This project uses an Adafruit nrf52840 board running CircuitPython and their 2.13 inch e-ink display (the tri-color one) to pull in data via Bluetooth LE from a Govee H5074 temperate and humidity sensor.

Watch it on YouTube. Also, the code is up on Github; take a look!

Some of the challenging / fun parts: on reset, the device will look around for a Bluetooth "Current Time Service" so that the clock is set automatically (no need to every manually set it!). Reading in Bluetooth advertisements for the Govee H5074 was not as obvious as it should have been (and I've got a blog post about it), and the e-ink display was much more challenging than I though it would be.



Using E-Ink displays with CircuitPython

 Using E-Ink displays with CircuitPython


Adafruit has some very nifty e-ink displays. It's something I've always wanted to try out: I've got an e-book reader that uses e-ink, and of course I've seen retail proce tags that use them. What I found is that although the demo code worked great, there were some major gotcha moments when using them in a project.

Firstly, the update process for the display is both very slow and very obvious. The display will go into a reverse-video mode and clear to white and to black several times before settling in and showing the new display contents. I've got a YouTube video that shows the refresh cycle.

Secondly, the display can only be updated every 3 minutes. This is locked by the code: if you try to refresh the screen more often, the CircuitPython library will just throw an exception.

Lastly, the sample code, as written, doesn't actually let you refresh the display. This may seem weird: the library code (displayio) certainly includes the concept of a display refresh, and when used with OLED displays you can do a refresh. But as it turns out, there's some kind of bug in the CircuitPython library (as of 2023-09-24), and when I do a refresh, my labels simply turn into black rectangles.

All my project code is up on Github. Take a look at the code.py file for the complete code; also look at the GoveeDisplay.py file; that's the file that makes all of the text labels for the device and handles text updates (e.g., it knows how to take the GoveeData object and update the right labels using either degrees Fahrenheit or Celcius).

The ideal way to make a display is to 

  • Set up the display
  • Create  all the text labels, background images, and layout groups
  • Call display.show() (and maybe display.refresh()) to update the display

After that, we can update the label.text fields, and the display will refresh (or, for e-ink displays, we have to call the display.refresh() method to force a refresh). This is simple and clean: updating the display is done just with the labels.

But for the e-ink display, you have to do something else. You can reuse the text labels and whatnot, but you have to rebuild the display all over again. In code.py, this is done in the while True: loop at about line 229. Looking at the code, on every refresh (which happens every 5 minutes), I have to redo pretty much everything display related: release the old display, make a new display_bus, make a new display, call display.show() (with the existing groups, etc.), and then finally I can do a refresh() and have the display update.


    displayio.release_displays()
    display_bus = displayio.FourWire(
        spi, command=epd_dc, chip_select=epd_cs, reset=epd_reset, baudrate=1000000
    )
    time.sleep(1)

    # For issues with display not updating top/bottom rows correctly set colstart to 8
    display = adafruit_ssd1680.SSD1680(
        display_bus,
        colstart=8,
        width=DISPLAY_WIDTH,
        height=DISPLAY_HEIGHT,
        busy_pin=epd_busy,
        highlight_color=0xFF0000,
        rotation=270,
    )
    # We can use the existing group with the new display.
    display.show(mainDisplayGroup)

    #
    # Wait for a time that's evenly divisible by 5. That means that sometimes
    # the scan results will be a little late.
    #
    wait = TimeToWaitForEvenClock(clock.datetime)
    if wait < 4 * 60:  # If it's e.g., 3:44:59
        time.sleep(wait)

    voltage = get_battery_voltage()
    gd.ShowGovee(userprefs, scanResult, clock.datetime, voltage)
    display.refresh()


One more thing -- it's not super reliable

Regretfully, my experience with the e-ink display is that it's also not super reliable. Every now and then, I'd refresh my code, and then the display just wouldn't update. Or maybe it didn't initialize, or it wasn't connected somehow. There's no exception thrown, or useful trace, or debug message.

In at least one case, I was trying to use a different physical microcontroller board of the same type (an NRF52840). And for whatever reason, the display just didn't want to work that the other micro, but it wouldn't say why. Is there a hardware fault? Did I mess up some connection? I have no idea, and no useful path forward.

In the end, I just used the original microcontroller, and eventually everything worked, mostly. And when it doesn't, pressing "reset" will eventually make everything work again.

Links to devices:






Reading Bluetooth LE (BLE) sensor data advertisements in CircuitPython

 Bluetooth Sensors -- reading the Govee H5074 advertisement data




Reading Bluetooth LE (BLE) sensor data from CircuitPython isn't always obvious. In this example, I show how I pull data from the Govee H5074 Bluetooth sensor. The sensor is a small, battery-powered sensor that reads the temperate and humidity and transmits it inside of Bluetooth advertisements.

The code is all up on Github. You'll want to look in the code directory at the GoveeScanner.py file. There's also a Govee5074.py file which can parse an advertisement once it's been seen, and a GoveeDisplay.py file that can display the data to an E-Ink display.


The key to the code is a pair of methods: the Scan() method and the ScanOnce() method. ScanOnce will start a scan ble.start_scan(timeout=...) . In practice, I use very short timeouts (.1 second) but then do a bunch of them in a loop in the Scan method. The start_scan returns a  generator, which is like a list but the contents will flow in over time. The contents will be both advertisements and advertisement scan responses. A scan response is like additional data in the advertisement, but it only comes in when it's requested. The request will be automatic; we don't have to do anything to request a scan_response.

The Govee sensor data is send in the scan_response, in what's called the "Manufacturer Data" section.

The critical thing to know is that the two values -- the advertisements and scan_responses -- aren't linked together by CircuitPython. We have to do that ourselves, and we do it by saving the advertisements in a dictionary that's indexed by the Bluetooth address.
  

The advertisements will have the name of the device. We're looking just for Govee H5074. In the code at about line 88 we look for the complete_name :

            name = advert.complete_name
            if (name is not None) and ("Govee_H5074" in name):
                # print("TRACE: 30: Found main govee advert", advert.address, name)
                goveeAdverts[advert.address] = advert


If the device is one we want, then we save away the advert in the goveeeAdverts dictionary, indexed by the Bluetooth address. This is critical because when we see an advertisement scan_response, we actually don't get the name but we do get the address.

The other part of the ScanOnce method is reading the scan responses. We filter first based on whether it's a response we want (we'll get lots of responses from lots of devices, but we only care about the Govee H5074 devices here).

            if advert.scan_response:
                # Check to make sure that this response address is in the list
                # of adverts that have the right name ("Govee_H5074...")
                # You can't tell from just the response advert; you need the
                # original advert, too.
                if advert.address not in goveeAdverts:
                    # very frequent -- set is only govee adverts, but the
                    # scan_response can be for anything
                    # print("TRACE: 15: not in set", advert.address)
                    continue

Once we know it's a scan_response we want, we pull out the manufacturer data and parse it. If it parses OK, then we prepare to return it.

                MANUFACTURER_SECTION = 0xFF  # Per Bluetooth SIG
                if (MANUFACTURER_SECTION in advert.data_dict):
                    annunciator.Found()
                    buffer = advert.data_dict[MANUFACTURER_SECTION]

                    g5074 = Govee5074(buffer)
                    if (g5074.IsOk):
                        annunciator.Read()
                        if retval is None:  # only print the first one
                            print("TRACE: 28: GOT DATA", g5074)
                        retval = g5074


The annunciator is the code that lets the user know what's going on. "Read" is an indication that we've read the data OK.


TL/DR: when reading Bluetooth advertisements, be sure to check the scan_responses too, because that's where the data might be.