{"found":57841,"hits":[{"document":{"authors":[{"contributor_roles":[],"family":"Kaczmarek","given":"Ursula","url":"https://orcid.org/0000-0002-8244-1373"},{"affiliation":[{"id":"https://ror.org/03wc8by49","name":"Posit Software, PBC (United States)"}],"contributor_roles":[],"family":"Clawson","given":"James","url":"https://orcid.org/0009-0008-6756-0607"}],"blog":{"authors":null,"community_id":"3446712e-ab03-4bb7-8641-571dcda8d8cd","created":1787702400,"current_feed_url":null,"description":"Tracking disruptions to U.S. federal science funding, grant by grant.","doi":"https://doi.org/10.59350/grantwitness","favicon":"https://rogue-scholar.org/api/communities/3446712e-ab03-4bb7-8641-571dcda8d8cd/logo","feed_format":"application/atom+xml","feed_url":"https://grantwitness.org/updates/rogue-scholar.xml","filter":null,"generator":"Other","home_page_url":"https://grantwitness.org/updates?type=updates","issn":null,"language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"grantwitness","status":"active","subfield":"3321","title":"Grant Witness","updated":1792540800,"use_api":null},"blog_name":"Grant Witness","blog_slug":"grantwitness","content_html":"<p>With our latest update, <a href=\"https://www.lostfunds.org\">Lost Funds</a> tallies a current disruption of $170 billion in funds Congress committed to states and communities.    A familiar end-of-fiscal year pattern has yet again emerged in the most recent federal spending data: upticks in spending as agencies move to deplete their appropriated funds before they revert back to the Treasury. The latest data reflects spending numbers for a few agencies that fully cover the 2026 Fiscal Year. Other agencies like the Department of Education and EPA are still lagging a bit in their end of year reporting.</p>\n<h2>Reporting updates</h2>\n<p>While our overall disruption figure has lowered, 21 local governments, 1 school district, 30 businesses, and 211 other recipients have joined the ranks of affected recipients. These recipient counts change as we ascertain new areas of funding disruption. A new impact will often introduce new recipients. Likewise, when new spending comes in to remove an impact from a state, we remove those recipients from the counts.</p>\n<p>The biggest change in reported spending arises in the <strong>residents_snap_benefits</strong> impact area, which had more money coming in, leading to smaller losses in that impact.</p>\n<h2>New impact areas</h2>\n<p>With this update we're tracking seven new areas of funding disruption:</p>\n<ol>\n<li><strong>international_education:</strong> Department of Education funding supporting international studies and foreign language programs, along with Fulbright-Hays awards</li>\n<li><strong>full_service_community_schools:</strong> Department of Education funding related to the Full-Service Community Schools Program, which supports coordinated academic, health, social, and family services in schools in underserved communities</li>\n<li><strong>state_personnel_development:</strong> Department of Education funding that sustains the State Personnel Development Grants Program providing career prep and professional development opportunities to  special education personnel</li>\n<li><strong>libraries_services_children:</strong> Institute of Museum and Library Services funding supporting the Grants to States program, the largest source of federal funding for library services</li>\n<li><strong>state_grid_resilience:</strong> Department of Energy funding relating to the state portion of the Grid Resilience State and Tribal Formula Grants Program, which funds improvements to strengthen electric-grid resilience (note this new impact area expands upon and absorbs the impact previously called <strong>pr_energy_resilience</strong>)</li>\n<li><strong>industrial_demonstrations:</strong> Department of Energy funding that backs the Industrial Demonstrations Program at the Office of Clean Energy Demonstrations</li>\n<li><strong>maine_conservation_funding:</strong> A National Oceanic and Atmospheric Administration grant providing for salt-marsh restoration and coastal resilience in Maine</li>\n</ol>\n<p>We found Education Week's <a href=\"https://www.edweek.org/policy-politics/trump-slashed-billions-for-education-in-2025-see-our-list-of-affected-grants/2026/01\">coverage</a> of Department of Education grant <a href=\"https://www.edweek.org/policy-politics/in-trumps-first-year-at-least-12-billion-in-school-funding-disruptions/2026/01\">terminations</a> to be a particularly valuable signal source as we continue to expand our coverage of lost funds. We also continue to track relevant litigation alongside impact areas as states fight back against rescission of the sizable commitments Congress made to them and have noted no new plaintiff states with this update.</p>","doi":"https://doi.org/10.59350/hz0sq-93s53","guid":"https://doi.org/10.59350/hz0sq-93s53","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792540800,"rid":"qn5vd-ry598","summary":"With our latest update, Lost Funds tallies a current disruption of $170 billion in funds Congress committed to states and communities. A familiar end-of-fiscal year pattern has yet again emerged in the most recent federal spending data: upticks in spending as agencies move to deplete their appropriated funds before they revert back to the Treasury.","title":"Lost Funds: Data Update for October 15","updated_at":1792615505,"url":"https://grantwitness.org/updates/lost-funds-data-update-for-october-15","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Kun","given":"Jeremy"}],"blog":{"authors":[{"name":"Jeremy Kun"}],"community_id":"a9da26d0-1f88-4154-a730-28bb864b1c11","created":1780876800,"current_feed_url":null,"description":"Recent content on Math \u2229 Programming","doi":"https://doi.org/10.59350/jeremykun","favicon":"https://rogue-scholar.org/api/communities/a9da26d0-1f88-4154-a730-28bb864b1c11/logo","feed_format":"application/rss+xml","feed_url":"https://www.jeremykun.com/index.xml","filter":null,"generator":"Hugo","home_page_url":"https://www.jeremykun.com/","issn":null,"language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":null,"slug":"jeremykun","status":"active","subfield":"2605","title":"Math \u2229 Programming","updated":1792607261,"use_api":null},"blog_name":"Math \u2229 Programming","blog_slug":"jeremykun","content_html":"Table of Contents\nThis is part of a tutorial series on the CKKS homomorphic encryption scheme.\nIn the last article, we described the ciphertext-ciphertext addition and multiplication operations. We ended with two challenges, one to restore the squared scaling factor $\\Delta^2$ back to $\\Delta$, and the other to convert a three-tuple ciphertext (decrypting under $(1, s, s^2)$) back to a two-tuple ciphertext (decrypting under $(1, s)$).\nThis article covers the latter challenge, which is called relinearization.","doi":"https://doi.org/10.59350/ge6c9-9p826","guid":"https://www.jeremykun.com/2026/10/21/ckks-key-switching-and-relinearization/","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792540800,"rid":"35rkb-jtj64","summary":"Table of Contents This is part of a tutorial series on the CKKS homomorphic encryption scheme. In the last article, we described the ciphertext-ciphertext addition and multiplication operations.","title":"CKKS \u2014 Key Switching and Relinearization","updated_at":1792607704,"url":"https://www.jeremykun.com/2026/10/21/ckks-key-switching-and-relinearization/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Bearman","given":"Nick","url":"https://orcid.org/0000-0002-8396-4061"}],"blog":{"authors":null,"community_id":"890db2a2-185e-4579-9f86-c8d46c887729","created":1779235200,"current_feed_url":null,"description":"A blog on my teaching, open source GIS, travel and life","doi":"https://doi.org/10.59350/nickbearman","favicon":"https://rogue-scholar.org/api/communities/890db2a2-185e-4579-9f86-c8d46c887729/logo","feed_format":"application/rss+xml","feed_url":"https://nickbearman.com/blog.xml","filter":null,"generator":"Quarto","home_page_url":"https://nickbearman.github.io/blog.html","issn":null,"language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"nickbearman","status":"active","subfield":"2306","title":"Nick Bearman","updated":1790722800,"use_api":null},"blog_name":"Nick Bearman","blog_slug":"nickbearman","content_html":"<p>I was fortunate enough to be able to attend FOSS4G in Hiroshima, Japan, my third international FOSS4G conference. Japan is an amazing country - I have included some extra photos at the bottom if you are interested, and also a practical tip about booking trains!</p>\n<p>Hiroshima is a fantastic setting, and we were based in the International Conference Centre, right next to the Peace Memorial Park commemorating lives lost when the nuclear weapon was dropped on Hiroshima during World War II, and their continuing campaign for nuclear disarmament and wider peace. The museum is well worth a visit if you ever have the opportunity, a thought provoking and humbling experience.</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/hiroshima_dome.jpg\"/></p>\n<figcaption>The Hiroshima Bomb Dome, the only structure left standing in the area where the first atomic bomb exploded on 6 August 1945.</figcaption>\n</figure>\n</div>\n<section class=\"level3\" id=\"foss4g-2026\">\n<h3 class=\"anchored\" data-anchor-id=\"foss4g-2026\">FOSS4G 2026</h3>\n<p>The FOSS4G conference was excellent and an amazing opportunity to meet people, make new friends and refresh existing friendships. This year 828 people met together to talk about open source geospatial software, share their work and get comments and feedback.</p>\n<p>One big topic (but not the only topic!) was AI, or more specifically Generative AI and Large Language Models. Many people are using them - and there were some really interesting presentations about the benefits of them, and new ideas of how we might work with them. One great presentation was <a href=\"https://talks.osgeo.org/foss4g-2026/talk/RT8SMH/\">Teaching AI to Contribute to Open Source: Lessons from MapLibre Agent Skills</a> where Stephanie May shared the experience of MapLibre working with AI agents. There was also a significant amount of discussion about not using AI, for a number of different reasons.</p>\n<p>There is a larger discussion going on in the OSGeo and QGIS communities about the impact of these tools. A number of PRs have been submitted, particularly for QGIS, which are very hard to review and result in the core contributors wasting a lot of time trying to understand these changes. As a result, there is discussion about banning AI use with the QGIS codebase. This is still under active discussion, and as QGIS is an open project, you can see (and take part in if you want to) the discussions - for example via the <a href=\"https://lists.osgeo.org/pipermail/qgis-developer/\">QGIS Developer list</a> and QEPs on the QGIS GitHub page <a href=\"https://github.com/qgis/QGIS-Enhancement-Proposals/pull/388\">here</a> and <a href=\"https://github.com/qgis/QGIS-Enhancement-Proposals/pull/387\">here</a>.</p>\n<p>OSGeo made a statement that they are not taking a view on LLM usage and that it is for the individual projects to decide. This was mentioned in the opening ceremony and in the <a href=\"https://wiki.osgeo.org/wiki/Board_Meeting_2026-07-28#AI_policy_discussion\">OSGeo Board meeting minutes</a>. Since this has been adopted, there has been a motion put forward to change this. It is a rapidly moving field, and sometimes I find it quite hard to keep up with developments.</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/osgeo-ai.jpg\"/></p>\n<figcaption>The OSGeo statement on AI, on 1st Sept 2026.</figcaption>\n</figure>\n</div>\n<p>There was also a great <a href=\"https://talks.osgeo.org/foss4g-2026/talk/P9XGVU/\">AMA (Ask Me Anything)</a> session on QGIS run by Marco Bernasocchi where people could ask anything they wanted to. I discovered the difference between a CRS (Coordinate Reference System) and a SRS (Spatial Reference System) - CRS is usual in QGIS, but SRS appears occasionally. A SRS includes a geoid, a CRS does not. Also one of the many limitations of the shapefile format is that you cannot reorder columns in it - in other formats, like geopackage, you can.</p>\n<p>During the main conference, I also presented <a href=\"https://talks.osgeo.org/foss4g-2026/talk/UYL89P/\">gitRmap</a> as a lightning talk - it was great to share this tool with the world and get some useful feedback. One of the highlights for me was one of the keynote presentations <a href=\"https://talks.osgeo.org/foss4g-2026/talk/S9RDGB/\">Sustaining Open Source: What We Can Learn from the Cooperative Model</a> where Ela Kagel discussed elements from the Cooperative Model that might be useful for the Open Source community, and I also learnt about <a href=\"https://coopcycle.org/en/\">Coop Cycle</a>, a group of cooperative based cycle couriers.</p>\n</section>\n<section class=\"level3\" id=\"workshops\">\n<h3 class=\"anchored\" data-anchor-id=\"workshops\">Workshops</h3>\n<p>This year I had the opportunity to present a workshop at the conference. It was a great opportunity to share some R and GIS code with enthusiastic folks, and I also managed to sell a copy of my book, <a href=\"https://nickbearman.com/rgis.html\">Using R as a GIS</a>, which is always a lovely feeling.</p>\n<p>We also explored the inner workings of GDAL Virtual File Systems and after a bit of experimenting managed to be able to download, extract and read in a shapefile in one line. I probably won't be using it in the course - there's a lot going on in that one line - but it's a cool bit of code! Thanks! Check out <a href=\"https://gdal.org/en/stable/user/virtual_file_systems.html\">https://gdal.org/en/stable/user/virtual_file_systems.html</a> for more details.</p>\n<div class=\"cell\">\n<div class=\"sourceCode cell-code\" id=\"cb1\" style=\"background: #f1f3f5;\"><pre class=\"sourceCode r code-with-copy\"><code class=\"sourceCode r\"><span id=\"cb1-1\">sthelens <span class=\"ot\" style=\"color: #003B4F;\nbackground-color: null;\nfont-style: inherit;\">&lt;-</span> <span class=\"fu\" style=\"color: #4758AB;\nbackground-color: null;\nfont-style: inherit;\">st_read</span>(<span class=\"st\" style=\"color: #20794D;\nbackground-color: null;\nfont-style: inherit;\">\"/vsizip//vsicurl/http://www.nickbearman.com/files/sthelens_lsoa_2021.zip\"</span>)</span></code></pre></div>\n</div>\n<p>I also attended three other workshops over the two days, including <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/talk/SSQZQ3/\">Discovering PostGIS: An Introduction for QGIS Users</a>, <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/talk/UA8G9H/\">30 Maps in 3 Hours</a> and <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/talk/YVR8DV/\">NextGIS Web for QGIS teams: from deployment to collaboration</a>. They were all really good workshops and one consistent thing across all of the workshops is people's passion about the tools they are talking about and willingness to share this knowledge with others. I find the workshops some of the most interesting bits of the conference - and I couldn't attend all that I wanted to. Fortunately most of the workshop convenors share their materials via <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/schedule/\">talks.osgeo.org</a> so the material is available to work through later on.</p>\n<p>The final day of the conference featured the code sprint, where I spent a bit of time working on gitRmap. I also received my first <a href=\"https://github.com/nickbearman/gitRmap/pull/6\">PR</a> for this from <a href=\"https://github.com/yutannihilation\">Hiroaki Yutani</a> working with me, so that was cool! I also managed to submit the first <a href=\"https://github.com/yutannihilation/ggplot2qgis/pull/23\">PR</a> to his project ggplot2qgis as well, so a great day for collaboration!</p>\n<p>The conference is also a great time and place to meet people - I met a wide range of users and developers, some who I had heard of before and many who I had not. Everyone there is enthusiastic about open source geospatial and it is a special place to be. As usual, we had a group photo which is a great memory of the week:</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/foss4g-2026-group-photo.jpg\"/></p>\n<figcaption>FOSS4G 2026 group photo - it's all about the people!</figcaption>\n</figure>\n</div>\n</section>\n<section class=\"level3\" id=\"cagis-st-louis-usa\">\n<h3 class=\"anchored\" data-anchor-id=\"cagis-st-louis-usa\">CaGIS, St Louis, USA</h3>\n<p>The following week I was also fortunate to be able to attend the CaGIS (Cartography and Geographic Information Society) conference in St Louis, MO, USA. I work as Cartographic Editor for CaGIS so it was a great opportunity to meet colleagues and authors who I exchange many emails with but rarely meet in person. This conference has a much more academic flavour than FOSS4G, and is also much smaller - about 120 people. We had two days of conference and one day of workshops, and also there was Geo-Resolution as well, which is a one day conference for the Geospatial Intelligence and Defence communities, which have a large presence in St.\u00a0Louis and the surrounding areas. It's a very different flavour again and always interesting to see how different people use geospatial.</p>\n</section>\n<section class=\"level3\" id=\"booking-trains-in-japan\">\n<h3 class=\"anchored\" data-anchor-id=\"booking-trains-in-japan\">Booking Trains in Japan</h3>\n<p>My travel plans included getting the train from Tokyo to Hiroshima. While Hiroshima does have an airport, my flight into Japan came in to Tokyo. The Japanese train service has a very good reputation, and the journey from Tokyo to Hiroshima is ~4.5 hours, so I decided to take the train instead.</p>\n<p>In fact, the conference venue travel page explicitly states \"There is no air service between Hiroshima and Osaka, Nagoya or Fukuoka. The JR railway lines provide convenient access instead.\"</p>\n<p>Thanks to <a href=\"https://www.seat61.com/Japan.htm\">Seat61</a>, I found the train information via <a href=\"https://booking.baolau.com/en/\">Baolau</a>. Japanese trains don't have advanced tickets - it is a flat fare whether you buy 1 month before or on the day. If you book ahead, you can reserve a seat, but for the journeys I'm making there are 2 or more trains an hour. So I booked the first one ahead of time (Tokyo to Hiroshima) but for the return journey, breaking in Kyoto, I will buy on the day.</p>\n<p><em>In the end I missed my scheduled train from Tokyo to Hiroshima - but you can just catch the next one, even with an advanced ticket. You just don't get a reserved seat.</em></p>\n<div class=\"columns\">\n<div class=\"column\" style=\"width:45%;\">\n<p>I also particularly liked this graphic from the ICC website, about how to travel to the conference centre:</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/icc-hiroshima-e-access.png\"/></p>\n<figcaption>From https://www.pcf.city.hiroshima.jp/icch/location.html</figcaption>\n</figure>\n</div>\n</div><div class=\"column\" style=\"width:45%;\">\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/travelling-on-a-bullet-train.jpg\"/></p>\n<figcaption>Travelling on a Bullet Train</figcaption>\n</figure>\n</div>\n<hr/>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/bento-boxes-lunch.jpg\"/></p>\n<figcaption>Lunch at the conference - Bento Boxes!</figcaption>\n</figure>\n</div>\n<hr/>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/hiroshima_peace_park_night.jpg\"/></p>\n<figcaption>Hiroshima Peace Park at night</figcaption>\n</figure>\n</div>\n</div>\n</div>\n</section>","doi":"https://doi.org/10.59350/5jx8a-xj967","guid":"https://nickbearman.github.io/blog/2026-09-foss4g-2026/","image":"https://nickbearman.github.io/blog/2026-09-foss4g-2026/hiroshima_dome_header.jpg","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1790640000,"rid":"gh43c-46k26","summary":"I was fortunate enough to be able to attend FOSS4G in Hiroshima, Japan, my third international FOSS4G conference. Japan is an amazing country - I have included some extra photos at the bottom if you are interested, and also a practical tip about booking trains!","tags":["Conference","FOSS4G","In Person","Open Source","Train"],"title":"FOSS4G 2026, Hiroshioma, Japan - All About the People!","updated_at":1792599799,"url":"https://nickbearman.github.io/blog/2026-09-foss4g-2026/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/10/Video_Games_Go_Choral.webp\" alt=\"A choir stands in front of a video game background.\" width=\"256\" height=\"256\" class=\"alignleft\">\n\n<p>London's musical entertainment scene has something for everyone - from long-running blockbuster shows to no-name bands in dingy basements. Somewhere in the middle is monophonic plainchant versions of the Halo theme sung in a church.</p>\n\n<p>The acoustics at St Martin in the Field are echoey. That makes it rather hard to hear the amplified announcement to turn off our phones, but makes an <i lang=\"it\">a cappella</i> chorus absolutely soar.  The choir of London Voices have sung on <a href=\"https://www.london-voices.com/films-gaming\">just about every movie you've watched and game you've played</a> - so they're no stranger to more modern compositions.</p>\n\n<p>The programme strictly alternated between songs from popular games are some more traditional verses. As a tone-deaf musical ignoramus, I found some of the pieces hard to distinguish from each other and, perhaps, just a little repetitive. Delightfully, the choir is generous with its solos. Far too often these events have one main star while everyone else glares daggers at them. Here it seemed quite the opposite, with a range of voices brought to the forefront.</p>\n\n<p>Parts of the programme are a little odd. While Pilentze Pee is an incredible song and sung extremely well, I don't think there's much to tie it to The Wind Waker. Of course, the highlight was a cheeky rendition of \"Still Alive\" from Portal. An excellent arrangement sung with joy and gusto.</p>\n\n<p>An eclectic and interesting event, marred only by the uncomfortable pews of the church. Truly we must suffer for other people's art!</p>\n\n<iframe title=\"London Voices: Video Games Go Choral Trailer\" width=\"620\" height=\"349\" src=\"https://www.youtube.com/embed/WfhgC6G9EIg?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen=\"\"></iframe><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76517&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/bypqq-x7y72","guid":"https://shkspr.mobi/blog/?p=76517","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/10/Video_Games_Go_Choral.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791504000,"rid":"zbhdq-g3g57","summary":"London's musical entertainment scene has something for everyone - from long-running blockbuster shows to no-name bands in dingy basements. Somewhere in the middle is monophonic plainchant versions of the Halo theme sung in a church. The acoustics at St Martin in the Field are echoey.","tags":["/etc/","Gaming","Gig","Review"],"title":"Concert Review: London Voices - Video Games Go Choral","updated_at":1792592746,"url":"https://shkspr.mobi/blog/2026/10/concert-review-london-voices-video-games-go-choral/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users.</p>\n\n<p>I na\u00efvely believe there's a better way. As I've written before, you should sit down and <a href=\"https://shkspr.mobi/blog/2025/10/how-to-actually-test-your-readme/\"><em>actually</em> test your readme</a>. Spin up a fresh Virtual Machine, go through what you've written, and see if it makes sense.</p>\n\n<p>But it is <em>really</em> hard to ignore your own biases. Of course you know that certain commands require <code>sudo</code> and obviously when you wrote <code>-foo</code> you meant <code>--foo</code> and everyone knows that you have to reboot afterwards.</p>\n\n<p>How do you get rid of those biases? I paid people!</p>\n\n<p>As part of my <a href=\"https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/\">NLnet grant application for ActivityBot</a>, I said I wanted to test the install experience with real users and I was prepared to pay them \u20ac25 for an hour of their time. I stuck out the call on Mastodon, gathered a few people, and had a video call with them.</p>\n\n<p>The format was simple. I explained that I was looking for feedback on the first-run experience. I knew that it wasn't perfect and actively wanted constructive criticism. I asked the volunteer to share their screen and, crucially, to speak aloud. Tell me what they were doing. What they didn't understand, what confused them, what delighted them, what frustrated them, etc.</p>\n\n<p>I took notes by hand (fuck feeding the machine) and spent several hours being told what incorrect assumptions I'd made.</p>\n\n<p>Amongst the highlights I discovered were:</p>\n\n<ul>\n<li>The link to the demo tool was wrong.</li>\n<li>Some people read a README in the terminal.</li>\n<li>What does it actually mean to rename a file?</li>\n<li>How do you rename a hidden file?</li>\n<li>Should a demo tool be available on the web or just the user's local machine?</li>\n<li>Why does some text explicitly need to be quoted and some not?</li>\n<li>My jokes aren't funny and are actively confusing.</li>\n<li>Some of the technical terminology needed explaining.</li>\n<li>The ordering of the different sections was confusing.</li>\n<li>I hadn't actually explained what the software would do.</li>\n<li>A whole section which was technically interesting to me was utterly confusing to everyone else.</li>\n<li>Not all web servers use permissions in the same way.</li>\n</ul>\n\n<p>And on it went!</p>\n\n<p>After each session I updated the README based on what people found difficult, then I re-tested it with the next person.</p>\n\n<p>In total, I paid out around \u20ac150 to have a bunch of people criticise me to my face. Hey, cheaper than therapy, right?</p>\n\n<p>I know someone is going to say \"why not just ask an LLM to simulate a range of users?\" The answer is very simple - I want to speak to real people. People are brilliant! They can make you laugh, you can see their cat when it wanders on to the call, they bring a unique perspective to the problem, and they're really happy when you give them a \u20ac25 voucher. Some will gladly do it for free and make <em>you</em> happy!</p>\n\n<!--\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b Welcome to Comment Club! Shhhh! Only cool people can read this bit of the post.\n\nI did have a serious think about using an LLM for this. Ethics aside (and you can never really put ethics aside) I just didn't feel like I could trust their judgement. I get the argument that it is not much different from using a spellcheck - but I fundamentally don't want to feed the beast.\n\nAnyway, the three rules of Comment Club are\u2026\n\n1. You must not tell anyone about Comment Club - let them find out about it themselves.\n2. You *must* add some Comment Club Content to at least one of your blog posts. Doesn't have to be much, just a little note will do.\n3. You should drop an email to anyone whose comments you've read. Start by sending me a friendly message at comment.club@shkspr.mobi\n\nKeep your eyes peeled for more comments in future blog posts \ud83d\ude03\n\nBe seeing you \ud83d\udc4c\ud83d\udc41\n-->\n\n<p>When I was doing technical writing for GOV.UK, all my beautiful prose was given a second-eye by another human. They eviscerated all my flowery phrases and turned the document into something more readable. They were the ones who caught the mistakes that no spell chequer could. They were able to have a proper conversation with me. More importantly, I could <em>hear</em> the frustration in their voice - that's the thing which lets you know a mistake needs to be corrected.</p>\n\n<p>I don't claim that <a href=\"https://gitlab.com/edent/activity-bot/-/blob/main/README.md?ref_type=heads\">ActivityBot's README</a> is now perfect - far from it - but it is now demonstrably easier to follow. Developers need to talk to <em>real</em> people. You don't have to pay them if you can't afford it - but find a few people who will talk aloud while they try to follow your instructions. I guarantee you that your README will become much better for it.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76333&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/41s5v-7q247","guid":"https://shkspr.mobi/blog/?p=76333","image":"https://shkspr.mobi/blog/wp-content/uploads/2020/01/Storm-Trooper-Chocolate-Money.jpg","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791676800,"rid":"hqgd2-gjq74","summary":"Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users. I na\u00efvely believe there's a better way.","tags":["/etc/","ActivityBot","ActivityPub","Developers"],"title":"I paid people to try and follow my README","updated_at":1792592744,"url":"https://shkspr.mobi/blog/2026/10/i-paid-people-to-try-and-follow-my-readme/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>I'll be radically honest here - I never really got the idea of \"Test Driven Development\". I'm much more of a \"telnet into production and damn the consequences\" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again.</p>\n\n<p>The essence of unit testing is pretty simple:</p>\n\n<ol>\n<li>Feed a function some data</li>\n<li>See how it responds</li>\n</ol>\n\n<p>Send it good data and if it returns \"true\" the test passes. Send it bad data and make sure it returns false. Easy peasy lemon squeezy.</p>\n\n<p>As part of my <a href=\"https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/\">NLnet funded work on ActivityBot</a>, I'm writing some proper tests for my bot framework. I am hopeful that some of them will be useful to other projects doing similar things.</p>\n\n<p>There are four major classes of tests - here are the problems I've found while writing them.</p>\n\n<h2 id=\"basic-text-validation\"><a href=\"#basic-text-validation\">Basic text validation</a></h2>\n\n<p>A basic test might be how to validate that an ActivityPub username is valid. Typically it would be something like <code>@edent@mastodon.social</code> - what assumptions do we make with that? It starts with an <code>@</code> and has another <code>@</code> somewhere in it, dividing into two parts, the user and the server.  The server has to be a valid domain name - but that can include non-ASCII Internationalised Domain Names and Emoji domains.</p>\n\n<p>What about the user part? Is it just A-Z and 0-9? Can it have dots? What about apostrophes like an email address?</p>\n\n<p>Basically, how easy is it find the specifications for all the various component parts of ActivityPub? Not very!</p>\n\n<h2 id=\"message-validation\"><a href=\"#message-validation\">Message validation</a></h2>\n\n<p>What does a <a href=\"https://www.rfc-editor.org/info/rfc7033/\">WebFinger response</a> look like? Which parts are mandatory and what values can they contain?</p>\n\n<p>With any message that your project generates, how can you be sure that other ActivityPub servers will understand it?</p>\n\n<p>Similarly, when we receive a follow message from an external server, what should it look like?</p>\n\n<p>There are some <a href=\"https://github.com/steve-bate/fediverse-jsonschema\">JSON Schemas</a> which can be used to validate some type of messages.</p>\n\n<p>But, again, there are <em>many</em> scattered specifications.</p>\n\n<h2 id=\"signature-verification\"><a href=\"#signature-verification\">Signature verification</a></h2>\n\n<p>As I've written about before, <a href=\"https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/\">HTTP Signatures are difficult</a>. There are two main problems. The first is verifying signatures.</p>\n\n<p>In order to do that, you need to gather a bunch of signatures and then find a way to replay them into the test. So I'm in the process of saving signatures which I can then share with others.</p>\n\n<p>The second problem is <em>generating</em> signatures.  Sure, you can run tests on the signatures - but there's only one <em>real</em> way to test whether they're acceptable\u2026</p>\n\n<h2 id=\"communicating-with-others\"><a href=\"#communicating-with-others\">Communicating with others</a></h2>\n\n<p>Passing tests means nothing if you can't communicate with others and they can't communicate with you.</p>\n\n<p>Some tests are easier than others. Sending a signed request should be easy and repeatable. But receiving messages isn't easily automated. It requires being able to control an external server and asking that to send messages.</p>\n\n<p>With the <a href=\"https://docs.joinmastodon.org/client/intro/\">Mastodon API</a> it is possible to make requests which will interact with your server - but only if it is a <em>public</em> server.</p>\n\n<h2 id=\"putting-it-all-together\"><a href=\"#putting-it-all-together\">Putting it all together</a></h2>\n\n<p>I'm putting <a href=\"https://gitlab.com/edent/activity-bot/-/tree/main/tests?ref_type=heads\">all my tests online</a>. I'm also sharing a <a href=\"https://gitlab.com/edent/activity-bot/-/tree/main/tests/headers?ref_type=heads\">collection of messages and signatures</a> which you can use in your tests.</p>\n\n<p>I'd love to know if you find them useful.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=75774&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/nmxzr-19z59","guid":"https://shkspr.mobi/blog/?p=75774","image":"https://shkspr.mobi/blog/wp-content/uploads/2012/09/709ae69b.png","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791590400,"rid":"s1wdt-2k111","summary":"I'll be radically honest here - I never really got the idea of \"Test Driven Development\". I'm much more of a \"telnet into production and damn the consequences\" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again.","tags":["/etc/","ActivityBot","ActivityPub","PHP"],"title":"Some quick thoughts on Unit Testing ActivityPub","updated_at":1792592742,"url":"https://shkspr.mobi/blog/2026/10/some-thoughts-on-unit-testing-activitypub/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>Let's get this out of the way first - this is a <em>terrible</em> idea!  I want my two-factor authentication codes to be easily accessible to me, so I've built a watch app which will display them. That's really convenient for me - but makes for a terrible security posture. Anyone who steals my watch will be able to see my one-time code. If they're clever, they can decompile the app and find the secret which powers the code generation.</p>\n\n<p>You should absolutely not do this and, if you ignore my warnings, you should only do this for codes which aren't critical to your security.</p>\n\n<!-- \n\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\nWelcome to Comment Club! If this is your first time - hello! This content is only available to sophisticated nerds who read my HTML comments.\n\nYou know what? I *do* use this. I have a low-ish security account which constantly needs my MFA number. Sometimes I can't be bothered unlocking my phone to get it. Of course, on my wrist I save it under a different name. Perhaps I'm an idiot for doing so, but sometimes convenience wins.\n\nAnyway, the three rules of Comment Club are\u2026\n\n1. You must not tell anyone about Comment Club - let them find out about it themselves.\n2. You *must* add some Comment Club Content to at least one of your blog posts. Doesn't have to be much, just a little note will do.\n3. You should drop an email to anyone whose comments you've read. Start by sending me a friendly message at comment.club@shkspr.mobi\n\nKeep your eyes peeled for more comments in my other blog posts \ud83d\ude03\n\nTTFN.\n-->\n\n<p>In my defence, an attacker would still need my username and password to make any use of these codes - but please don't trust your security to my deranged witterings.</p>\n\n<h2 id=\"what-it-looks-like\"><a href=\"#what-it-looks-like\">What It Looks Like</a></h2>\n\n<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/09/MFA.webp\" alt=\"My wristwatch showing &quot;HSBC MFA Code 751646&quot;. Don't worry, this isn't my real code. OR IS IT?!?!\" width=\"1024\" height=\"576\" class=\"aligncenter\">\n\n<h2 id=\"prior-art\"><a href=\"#prior-art\">Prior Art</a></h2>\n\n<p>I'm sure many other people have done this. Indeed, three years ago I built <a href=\"https://shkspr.mobi/blog/2023/07/an-eink-wrist-mounted-totp-generator/\">An eInk, Wrist-Mounted, TOTP Generator</a>.</p>\n\n<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2023/07/Wrist-TOTP.jpg\" alt=\"A chunky wristwatch showing the time and a selection of 6 digit codes and their corresponding entities.\" width=\"1024\" height=\"1024\" class=\"aligncenter\">\n\n<h2 id=\"the-service\"><a href=\"#the-service\">The Service</a></h2>\n\n<p>This is in my <code>Service.cpp</code> - as you can see, it's pretty simple. This won't let you add in hundreds of codes - you get one and that's it. There's no countdown when a code is about to expire, so I recommend waiting until a new one pops up before you start typing it in.</p>\n\n<p>I'll be candid, I haven't used C++ properly since university - and that was in the last century. I've mostly just cut things out to see what breaks. Feel free to educate me on the correct idiomatic code.</p>\n\n<p>You will need to set your own secret in here.</p>\n\n<pre><code class=\"language-cpp\">//  Standard includes.\n#include \"Service.hpp\"\n#include \"SDK/Messages/CommandMessages.hpp\"\n#include \"SDK/Messages/MessageGuard.hpp\"\n\n//  Needed for PureTOTP.\n#include &lt;iostream&gt;\n#include \"TOTP.h\"\n\n#include &lt;string&gt;\n#include &lt;sstream&gt;\n\n//  What this Glance can do.\nService::Service(SDK::Kernel &amp;kernel)\n    : mKernel(kernel)\n    , mGlanceUI()\n    , mGlanceTitle()\n    , mGlanceValue()\n{}\n\nvoid Service::createGuiControls()\n{\n    //  Set the main title in the Glance.\n    mGlanceTitle = mGlanceUI.createText();\n    mGlanceTitle.pos({ kTitleX, kTitleY }, { kTitleW, kTitleH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_TEAL)\n        .setText(\"HSBC MFA Code\")\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n\n    //  Set the default text for the value.\n    mGlanceValue = mGlanceUI.createText();\n    mGlanceValue.pos({ 20, kValueY }, { 200, kValueH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_YELLOW_DARK)\n        .setText( \"Waiting for update\" )\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n}\n\nvoid Service::glanceUpdate()\n{\n    //  Set the Base32 encoded secret (all uppercase).\n    std::string secret = \"QWERTYUIOP\";\n    //  Generate the code based on the Unix timestamp right now.\n    const std::time_t t = std::time(0);\n    std::string code = TOTP::GenerateTOTP( 6, 30, TOTP::HMAC_SHA1, secret, t );\n\n    std::ostringstream ss;\n    ss &lt;&lt; t;\n\n    //  Convert the string to char* and display it as the value.\n    mGlanceValue.print( code.c_str() ); \n\n    //  Show the Unix time to check the timezone is correct.\n    // mGlanceTitle.print( ss.str().c_str() );\n}\n\n//  No service.\nService::~Service(){}\n\n//  When this glance is opened.\nvoid Service::run()\n{\n    while (true) {\n        SDK::MessageBase *msg;\n        if(!mKernel.comm.getMessage(msg)) {\n            continue;\n        }\n\n        switch (msg-&gt;getType()) {\n            //  The Glance has started.\n            case SDK::MessageType::EVENT_GLANCE_START:\n                if (configGui()) {\n                    createGuiControls();\n                } else {\n                    mKernel.comm.releaseMessage(msg);\n                    mKernel.sys.exit(0);\n                }\n                break;\n            //  The Glance has been stopped.\n            case SDK::MessageType::COMMAND_APP_STOP:\n            case SDK::MessageType::EVENT_GLANCE_STOP:\n                mKernel.comm.releaseMessage(msg);\n                return;\n                break;\n\n            //  Every second, perform an action.\n            case SDK::MessageType::EVENT_GLANCE_TICK:\n                onGlanceTick();\n                break;\n\n            default:\n                break;\n        }\n\n        mKernel.comm.releaseMessage(msg);\n    }\n}\n\nvoid Service::onGlanceTick()\n{\n    if (mGlanceUI.isInvalid()) {\n        if (auto upd = SDK::make_msg&lt;SDK::Message::RequestGlanceUpdate&gt;(mKernel)) {\n            upd-&gt;name           = APP_NAME;\n            upd-&gt;controls       = mGlanceUI.data();\n            upd-&gt;controlsNumber = static_cast&lt;uint32_t&gt;(mGlanceUI.size());\n            upd.send(100);\n        }\n\n        mGlanceUI.setValid();\n    }\n    glanceUpdate();\n}\n\nbool Service::configGui()\n{\n    bool status = false;\n    if (auto gc = SDK::make_msg&lt;SDK::Message::RequestGlanceConfig&gt;(mKernel)) {\n        if (gc.send(100) &amp;&amp; gc.ok()) {\n            if (gc-&gt;maxControls &gt;= 3) {\n                mGlanceUI.setWidth(gc-&gt;width);\n                mGlanceUI.setHeight(gc-&gt;height);\n                status = true;\n            }\n        }\n    }\n    return status;\n}\n</code></pre>\n\n<h2 id=\"the-library\"><a href=\"#the-library\">The Library</a></h2>\n\n<p>There are dozens of C++ libraries for RFC 6238 generation. I ended up using <a href=\"https://github.com/Einztein/PureTOTP\">PureTOTP</a> as it didn't require any external dependencies and was only two files.  As I only needed SHA1 TOTP codes, I stripped out a bunch of functionality which was unnecessary for me. Similarly, I only wanted to generate 6 digit codes with a 30 second period - so took out anything which did more than that.</p>\n\n<p>I've tidied up the code and comments where I can. The original was MIT Licenced - so this is as well.</p>\n\n<p>There are two parts to this. First, the header</p>\n\n<pre><code class=\"language-_\">// TOTP.h\n//\n#pragma once\n#include &lt;string&gt;\nclass TOTP\n{\npublic:\n    const static int HMAC_SHA1 = 20;    //  Length of HMAC.\n\n    static std::string GenerateTOTP(const std::string&amp; secret);\n    static std::string GenerateTOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp; secret, int specTime);\n\n    TOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp; secret): digit(digit), refreshSeconds(refreshSeconds), algorithm(algorithm), secret(secret) {}\n    TOTP(const std::string&amp; secret): digit(6), refreshSeconds(30), algorithm(HMAC_SHA1), secret(secret) {}\n\n    std::string currentTOTP();\n\nprivate:\n    int digit, refreshSeconds, algorithm;\n\n    std::string secret;\n};\n</code></pre>\n\n<p>Next is the standalone library which does the TOTP generation, <code>TOTP.cpp</code></p>\n\n<pre><code class=\"language-cpp\">// TOTP.cpp\n//\n#include \"TOTP.h\"\n#include &lt;vector&gt;\n#include &lt;stack&gt;\n#include &lt;queue&gt;\n#include &lt;sstream&gt;\n#include &lt;iomanip&gt;\n#include &lt;cstdint&gt;\n#include &lt;cstring&gt;\n#include &lt;array&gt;\n#include &lt;stdexcept&gt;\n#include &lt;algorithm&gt;\nusing namespace std;\n\nstruct JEVP_MD\n{\n    enum Kind {\n        SHA1\n    };\n\n    Kind kind;\n    int  md_size;\n    int  block_size;\n};\n\nstatic inline uint32_t rotl32(uint32_t x, uint32_t n) { return (x &lt;&lt; n) | (x &gt;&gt; (32 - n)); }\nstatic inline uint32_t rotr32(uint32_t x, uint32_t n) { return (x &gt;&gt; n) | (x &lt;&lt; (32 - n)); }\nstatic inline uint64_t rotr64(uint64_t x, uint64_t n) { return (x &gt;&gt; n) | (x &lt;&lt; (64 - n)); }\n\nstatic uint32_t load_be32(const unsigned char* p)\n{\n    return (uint32_t(p[0]) &lt;&lt; 24) |\n           (uint32_t(p[1]) &lt;&lt; 16) |\n           (uint32_t(p[2]) &lt;&lt;  8) |\n            uint32_t(p[3]);\n}\n\nstatic void store_be32(uint32_t v, unsigned char* p)\n{\n    p[0] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 24) &amp; 0xFF);\n    p[1] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 16) &amp; 0xFF);\n    p[2] = static_cast&lt;unsigned char&gt;((v &gt;&gt;  8) &amp; 0xFF);\n    p[3] = static_cast&lt;unsigned char&gt;( v        &amp; 0xFF);\n}\n\nstatic void store_be64(uint64_t v, unsigned char* p)\n{\n    p[0] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 56) &amp; 0xFF);\n    p[1] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 48) &amp; 0xFF);\n    p[2] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 40) &amp; 0xFF);\n    p[3] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 32) &amp; 0xFF);\n    p[4] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 24) &amp; 0xFF);\n    p[5] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 16) &amp; 0xFF);\n    p[6] = static_cast&lt;unsigned char&gt;((v &gt;&gt;  8) &amp; 0xFF);\n    p[7] = static_cast&lt;unsigned char&gt;( v        &amp; 0xFF);\n}\n\nstatic vector&lt;unsigned char&gt; sha1_hash(const unsigned char* data, size_t len)\n{\n    vector&lt;unsigned char&gt; msg(data, data + len);\n    uint64_t              bit_len = static_cast&lt;uint64_t&gt;(len) * 8;\n\n    msg.push_back(0x80);\n    while ((msg.size() % 64) != 56) msg.push_back(0x00);\n\n    unsigned char lenbuf[8];\n    store_be64(bit_len, lenbuf);\n    msg.insert(msg.end(), lenbuf, lenbuf + 8);\n\n    uint32_t h0 = 0x67452301;\n    uint32_t h1 = 0xEFCDAB89;\n    uint32_t h2 = 0x98BADCFE;\n    uint32_t h3 = 0x10325476;\n    uint32_t h4 = 0xC3D2E1F0;\n\n    for (size_t chunk = 0; chunk &lt; msg.size(); chunk += 64)\n    {\n        uint32_t w[80];\n        for (int i = 0; i &lt; 16; ++i)\n            w[i] = load_be32(&amp;msg[chunk + i * 4]);\n        for (int i = 16; i &lt; 80; ++i)\n            w[i] = rotl32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);\n\n        uint32_t a = h0, b = h1, c = h2, d = h3, e = h4;\n\n        for (int i = 0; i &lt; 80; ++i)\n        {\n            uint32_t f, k;\n            if (i &lt; 20)\n            {\n                f = (b &amp; c) | ((~b) &amp; d);\n                k = 0x5A827999;\n            }\n            else if (i &lt; 40)\n            {\n                f = b ^ c ^ d;\n                k = 0x6ED9EBA1;\n            }\n            else if (i &lt; 60)\n            {\n                f = (b &amp; c) | (b &amp; d) | (c &amp; d);\n                k = 0x8F1BBCDC;\n            }\n            else\n            {\n                f = b ^ c ^ d;\n                k = 0xCA62C1D6;\n            }\n\n            uint32_t temp = rotl32(a, 5) + f + e + k + w[i];\n\n            e = d;\n            d = c;\n            c = rotl32(b, 30);\n            b = a;\n            a = temp;\n        }\n\n        h0 += a;\n        h1 += b;\n        h2 += c;\n        h3 += d;\n        h4 += e;\n    }\n\n    vector&lt;unsigned char&gt; out(20);\n    store_be32(h0, &amp;out[ 0]);\n    store_be32(h1, &amp;out[ 4]);\n    store_be32(h2, &amp;out[ 8]);\n    store_be32(h3, &amp;out[12]);\n    store_be32(h4, &amp;out[16]);\n    return out;\n}\n\nstatic const JEVP_MD g_sha1   = {JEVP_MD::SHA1, 20, 64};\n\nconst JEVP_MD* JEVP_sha1() { return &amp;g_sha1; }\n\nunsigned char* JHMAC(const JEVP_MD* JEVP_MD, const void* key, int key_len, const unsigned char* data, size_t data_len, unsigned char* md, unsigned int* md_len)\n{\n    if (!JEVP_MD || !key || key_len &lt; 0 || !data)\n        return nullptr;\n\n    static thread_local unsigned char static_md[64];\n\n    if (!md)\n        md = static_md;\n\n    vector&lt;unsigned char&gt; key_block(JEVP_MD-&gt;block_size, 0);\n    const unsigned char*  key_bytes = static_cast&lt;const unsigned char*&gt;(key);\n\n    vector&lt;unsigned char&gt; real_key;\n    if (key_len &gt; JEVP_MD-&gt;block_size)\n    {\n        real_key = sha1_hash(key_bytes, static_cast&lt;size_t&gt;(key_len));\n        std::copy(real_key.begin(), real_key.end(), key_block.begin());\n    }\n    else\n    {\n        std::copy(key_bytes, key_bytes + key_len, key_block.begin());\n    }\n\n    vector&lt;unsigned char&gt; ipad(JEVP_MD-&gt;block_size);\n    vector&lt;unsigned char&gt; opad(JEVP_MD-&gt;block_size);\n    for (int i = 0; i &lt; JEVP_MD-&gt;block_size; ++i)\n    {\n        ipad[i] = static_cast&lt;unsigned char&gt;(key_block[i] ^ 0x36);\n        opad[i] = static_cast&lt;unsigned char&gt;(key_block[i] ^ 0x5C);\n    }\n\n    vector&lt;unsigned char&gt; inner;\n    inner.reserve(JEVP_MD-&gt;block_size + data_len);\n    inner.insert(inner.end(), ipad.begin(), ipad.end());\n    inner.insert(inner.end(), data, data + data_len);\n\n    vector&lt;unsigned char&gt; inner_hash = sha1_hash(inner.data(), inner.size());\n\n    vector&lt;unsigned char&gt; outer;\n    outer.reserve(JEVP_MD-&gt;block_size + inner_hash.size());\n    outer.insert(outer.end(), opad.begin(), opad.end());\n    outer.insert(outer.end(), inner_hash.begin(), inner_hash.end());\n\n    vector&lt;unsigned char&gt; final_hash = sha1_hash(outer.data(), outer.size());\n\n    std::memcpy(md, final_hash.data(), final_hash.size());\n    if (md_len) *md_len = static_cast&lt;unsigned int&gt;(final_hash.size());\n\n    return md;\n}\n\n//  Base32 decoding table.\n/* clang-format off */\nconst int base32_reverse_map[128] =\n{\n    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15\n    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 31\n    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 47\n//      (49)   2    3    4    5    6    7  (56)\n    255, 255,  26,  27,  28,  29,  30,  31, 255, 255, 255, 255, 255, 255, 255, 255, // 63\n// (64)   A    B    C    D    E    F    G    H    I    J    K    L    M    N    O\n    255,   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14, // 79\n//   P    Q    R    S    T    U    V    W    X    Y    Z  (91)\n    15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25, 255, 255, 255, 255, 255, // 95\n// (96)   A    B    C    D    E    F    G    H    I    J    K    L    M    N    O  // lower case instead\n    255,   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14, // 111\n//   P    Q    R    S    T    U    V    W    X    Y    Z (123)                     // lower case instead\n    15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25, 255, 255, 255, 255, 255  // 127\n};\n/* clang-format on */\n\n//  Base32 decoding function.\nvector&lt;unsigned char&gt; base32_decode(const string&amp; base32)\n{\n    queue&lt;int&gt; q_rev;\n    for (const char&amp; c : base32)\n    {\n        if (c &lt;= 49 || (c &gt;= 56 &amp;&amp; c &lt;= 64) || (c &gt;= 91 &amp;&amp; c &lt;= 96) || c &gt;= 123)\n        { //    Illegal lower case will be treat as upper case thanks to base32_reverse_map.\n            if (c == '=') break;\n        }\n        else\n        {\n            q_rev.push(base32_reverse_map[(int)c]); //  Get integers of base32.\n        }\n    }\n    queue&lt;bool&gt; q_bin;\n    while (!q_rev.empty()) //   Transform the integers into bits and queue them up.\n    {\n        int now = q_rev.front(); // Get a integer from queue.\n        q_rev.pop();\n        stack&lt;bool&gt; sb;\n        for (int i = 0; i &lt; 5; i++) //  Push the bits of this integer into stack\n        {                           //  5 bits into 1 group\n            sb.push(now &amp; 1);       //  Push the last bit of this integer into stack.\n            now &gt;&gt;= 1;              //  Flush the last bit.\n        }\n        while (!sb.empty()) //  Flush the bits of this integer into binary queue.\n        {\n            q_bin.push(sb.top()); //    Pop bits from stack into binary queue.\n            sb.pop();\n        }\n    }\n    vector&lt;unsigned char&gt; uc;\n    while (!q_bin.empty()) //   Transform the bits into bytes.\n    {\n        int thehex = 0;\n        for (int i = 0; i &lt; 8; i++) //  Read a byte from the bits queue.\n        {\n            if (q_bin.empty())\n            {                     //    If the bits queue is empty now,\n                thehex &lt;&lt;= 8 - i; //    set 0 on the last bits of a byte.\n                break;\n            }\n            thehex &lt;&lt;= 1;                 //    Make the space for the new bit.\n            thehex |= (int)q_bin.front(); //    Push this bit into byte.\n            q_bin.pop();\n        }\n        uc.push_back((unsigned char)thehex);\n    }\n\n    if (uc.back() == 0) uc.pop_back();     //   TOTP may abandon last zero.\n    if (uc.size() % 2 != 0) uc.pop_back(); //   TOTP may abandon odd res.\n    return uc;\n}\n\n//  Get Unix Time.\ntime_t getUnixTime()\n{\n    return time(0);\n}\n\n//  Calculate the TOTP time signature.\ntime_t getTimeSign(int refreshPeriod, time_t specUnixTime)\n{\n    time_t outTime = specUnixTime / refreshPeriod;\n    return outTime;\n}\n\n//  Transform time_t aka &lt;long long&gt; to bytes.\nvector&lt;unsigned char&gt; llong2bytes(time_t tt)\n{\n    uint64_t value = static_cast&lt;uint64_t&gt;(tt);\n\n    vector&lt;unsigned char&gt; bytes(8);\n\n    for (int i = 7; i &gt;= 0; --i)\n    {\n        bytes[i] = static_cast&lt;unsigned char&gt;(value &amp; 0xFF);\n        value &gt;&gt;= 8;\n    }\n\n    return bytes;\n}\n\n//  Fast Exponentiation algorithm.\nunsigned fastpow(unsigned base, unsigned index)\n{\n    if (index == 0) return 1;\n    unsigned t = 1;\n    while (index != 0)\n    {\n        if (index &amp; 1) t *= base;\n        index &gt;&gt;= 1;\n        base *= base;\n    }\n    return t;\n}\n\n//  Calculate TOTP according to RFC standard.\nstd::string hash_to_TOTP(const unsigned char* hmac, unsigned int length, int digit)\n{\n    int offset = hmac[length - 1] &amp; 0x0F;\n\n    uint32_t bin_code = ((hmac[offset]     &amp; 0x7F) &lt;&lt; 24) |\n                        ((hmac[offset + 1] &amp; 0xFF) &lt;&lt; 16) |\n                        ((hmac[offset + 2] &amp; 0xFF) &lt;&lt; 8)  |\n                        ( hmac[offset + 3] &amp; 0xFF);\n\n    uint32_t mod = 1;\n    for (int i = 0; i &lt; digit; ++i) mod *= 10;\n\n    uint32_t otp = bin_code % mod;\n\n    std::ostringstream oss;\n    oss &lt;&lt; std::setw(digit) &lt;&lt; std::setfill('0') &lt;&lt; otp;\n    return oss.str();\n}\n\nstd::string TOTP::GenerateTOTP(const std::string&amp; secret)\n{\n    return GenerateTOTP(6, 30, HMAC_SHA1, secret, getUnixTime());\n}\n\nstd::string TOTP::GenerateTOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp; secret, int specTime)\n{\n    vector&lt;unsigned char&gt; h_msg = llong2bytes(getTimeSign(refreshSeconds, specTime)); //    Get current time signature and transform time_t aka &lt;long long&gt; to bytes.\n    vector&lt;unsigned char&gt; h_key = base32_decode(secret); // Transform base32 encoded secret key to decoded bytes.\n\n    unsigned char* out_hmac = new unsigned char[algorithm]; //  Perform openssl function to calculate HMAC-SHA1 hash.\n    unsigned int   out_hmac_length;\n    const JEVP_MD* emd;\n    emd = JEVP_sha1();\n    JHMAC(emd, h_key.data(), (int)h_key.size(), h_msg.data(), (int)h_msg.size(), out_hmac, &amp;out_hmac_length);\n\n    string fine = hash_to_TOTP(out_hmac, out_hmac_length, digit);\n    delete[] out_hmac;\n\n    return fine; // Calculate TOTP according to RFC standard.\n}\n\nstd::string TOTP::currentTOTP()\n{\n    return GenerateTOTP(digit, refreshSeconds, algorithm, secret, getUnixTime());\n}\n</code></pre><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76104&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/0w6m9-ym602","guid":"https://shkspr.mobi/blog/?p=76104","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/09/MFA.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792281600,"rid":"52ejt-3v382","summary":"Let's get this out of the way first - this is a terrible idea! I want my two-factor authentication codes to be easily accessible to me, so I've built a watch app which will display them. That's really convenient for me - but makes for a terrible security posture. Anyone who steals my watch will be able to see my one-time code.","tags":["/etc/","C++","HowTo","UnaWatch","Watch"],"title":"Una Watch TOTP - MFA on your wrist!","updated_at":1792592740,"url":"https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/10/randj.webp\" alt=\"Book cover.\" width=\"200\" class=\"alignleft\">\n\n<p>The thing about Romeo and Juliet is that it is a true story. It isn't literally true, but it <em>is</em> literary true. We see the same motif repeated throughout all cultures and all times. Caroline Lea (writing as Caroline Finch) asks an important question - what if every pair of star-cross'd lovers were reincarnations of the original Verona duo?</p>\n\n<p>The essence of tragedy is that the end is unavoidable. You've probably watched Romeo sup poison a hundred times. It is <em>inevitable</em> that Juliet will take her own life in response. Horrific! Yet, not only do you refuse to look away, you gleefully seek it out for another viewing. There's a reason why Tony and Maria cannot escape their destinies in West Side Story - something more than fate is pushing them onto a path that will accept no deviation. The only way through is, of course, death.</p>\n\n<p>The Thousand Deaths starts slowly - the first \"act\" felt like an excuse to retell the classic tale in a new setting. Nothing wrong with that, of course, and it is suffused with lovely writing - but it felt a bit \"so what?\"</p>\n\n<p>And then they die.</p>\n\n<p>As the acts progress and they get closer to the their thousandth death the pace picks up. The writing is absolutely gorgeous, packed with allusions and references for the careful reader (more on that later). The story is heartbreaking - as well you know - and the author has the perfect knack of keeping her characters on just the right side of histrionics.</p>\n\n<p>I'd sort of classify this as <em>almost</em> romantasy. There's an undercurrent of sci-fi and fantasy which can't be ignored - but you can also hear the stirring of the violins in the background. Our hero and heroine come together and are violently broken apart for our insatiable entertainment. Do we care about whether that's good for them? What does it feel like to be a Mercutio forever used as a pawn-sacrifice? Does Tybalt resent his forcible reincarnation into a perpetual baddie? Are we culpable for insisting that they strut upon an endless stage just so we can wipe tears from our eyes?</p>\n\n<p>The book builds to a fine crescendo. It is heartfelt and <em>utterly</em> tragic. The ending will, I suspect, divide people. I found it mostly satisfactory and completely in keeping with the rest of the story.</p>\n\n<p>One final thing - the end notes are <em>superb</em>. They go through all the references you might have missed, explain where certain phrases came from, and point the curious reader to the books which were an inspiration. Too many authors treat their literary allusions as a precious puzzle and jealously guard them. I'm delighted that Caroline Finch is generous enough to share her secrets with us.</p>\n\n<p>Many thanks to NetGalley for the review copy. The book will be released in 2027 - I strongly recommend putting it on your pre-order list.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76683&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/wkhx2-yfh44","guid":"https://shkspr.mobi/blog/?p=76683","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/10/randj.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791849600,"rid":"tsnbv-x9v44","summary":"The thing about Romeo and Juliet is that it is a true story. It isn't literally true, but it is literary true. We see the same motif repeated throughout all cultures and all times. Caroline Lea (writing as Caroline Finch) asks an important question - what if every pair of star-cross'd lovers were reincarnations of the original Verona duo?","tags":["/etc/","Book Review","NetGalley","Shakespeare"],"title":"Book Review: The Thousand Deaths of Romeo and Juliet - Caroline Finch","updated_at":1792592736,"url":"https://shkspr.mobi/blog/2026/10/book-review-the-thousand-deaths-of-romeo-and-juliet-caroline-finch/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>I don't know why hardware manufacturers find it so hard to write decent documentation. As I've said before <a href=\"https://shkspr.mobi/blog/2025/10/how-to-actually-test-your-readme/\">you need to <em>actually</em> test your readme</a>.  The Una Watch development instructions are scattered across a bunch of different files.</p>\n\n<p>So here's how to go from zero to your first \"Hello, World!\" Glance app with the minimum of fuss.</p>\n\n<p>This all assumes a fairly modern Debian / Ubuntu / Pop OS Linux.</p>\n\n<h2 id=\"prerequisites\"><a href=\"#prerequisites\">Prerequisites</a></h2>\n\n<p>This installs most of the tools you will need:</p>\n\n<pre><code class=\"language-bash\">sudo apt update\nsudo apt install -y git python3 python3-pip cmake build-essential\n</code></pre>\n\n<h2 id=\"stm32cubeclt\"><a href=\"#stm32cubeclt\">STM32CubeCLT</a></h2>\n\n<p>Visit <a href=\"https://www.st.com/en/development-tools/stm32cubeclt.html\">https://www.st.com/en/development-tools/stm32cubeclt.html</a> and download the Linux version of the tool. Inexplicably, you need to give your email address. I recommend giving a disposable one from <a href=\"https://10minutemail.com/\">https://10minutemail.com/</a></p>\n\n<p>After downloading, unzip the file.</p>\n\n<p>On the command line, <code>cd</code> into the directory containing the unzipped file.</p>\n\n<pre><code class=\"language-bash\">chmod +x stm32cubeclt_*.sh\nsudo ./stm32cubeclt_*.sh\n</code></pre>\n\n<p>You will need to agree to their licence. Just hit <code>y</code>.</p>\n\n<p>That installs everything in <code>/opt/st/stm32cubeclt_1.22.0/</code></p>\n\n<p>You will need to make that available to your PATH.</p>\n\n<pre><code class=\"language-bash\">export PATH=\"/opt/st/stm32cubeclt_1.22.0/GNU-tools-for-STM32/bin:$PATH\"\n</code></pre>\n\n<h2 id=\"clone-the-repo\"><a href=\"#clone-the-repo\">Clone the repo</a></h2>\n\n<p>You will need to copy <a href=\"https://github.com/UNAWatch/una-sdk/\">the Una SDK</a>. I keep my git repos in <code>~/git/</code> - you will need the location of the directory later.</p>\n\n<pre><code class=\"language-bash\">cd ~/git/\ngit clone git@github.com:UNAWatch/una-sdk.git\ncd una-sdk/\ngit submodule update --init ThirdParty/lvgl\n</code></pre>\n\n<p>Then update all the Python dependencies.</p>\n\n<pre><code class=\"language-bash\">export UNA_SDK=\"$PWD\"\npython3 -m pip install --upgrade pip\npython3 -m pip install -r \"$UNA_SDK/Utilities/Scripts/app_packer/requirements.txt\"\n</code></pre>\n\n<h2 id=\"check-it-has-all-worked\"><a href=\"#check-it-has-all-worked\">Check it has all worked</a></h2>\n\n<p>Paste this into your terminal. Check for any errors.</p>\n\n<pre><code class=\"language-bash\">which arm-none-eabi-gcc || true\nwhich cmake || true\nwhich make || true\nwhich python3 || true\npython3 -m pip --version\narm-none-eabi-gcc --version || true\ncmake --version || true\nmake --version || true\n</code></pre>\n\n<h2 id=\"build-the-demo-app\"><a href=\"#build-the-demo-app\">Build the demo app</a></h2>\n\n<p>If all of the above has worked, you should be able to build the demo app with:</p>\n\n<pre><code class=\"language-bash\">cd /tmp\nmkdir -p MyAlarm\ncp -r \"$UNA_SDK/Examples/Apps/Alarm/\"* MyAlarm\nmkdir -p MyAlarm/build\ncmake -G \"Unix Makefiles\" -S MyAlarm/Software/Apps/Alarm-CMake -B MyAlarm/build\ncmake --build MyAlarm/build\n</code></pre>\n\n<h2 id=\"make-it-permanent\"><a href=\"#make-it-permanent\">Make it permanent</a></h2>\n\n<p>Store the paths permanently. If you put cloned the Una SDK to somewhere other than <code>~/git/</code> you'll need to edit this next bit of code:</p>\n\n<pre><code class=\"language-bash\">echo 'export UNA_SDK=\"$HOME/git/una-sdk\"' &gt;&gt; ~/.bashrc\necho 'export PATH=\"/opt/st/stm32cubeclt_1.22.0/GNU-tools-for-STM32/bin:$PATH\"' &gt;&gt; ~/.bashrc\nsource ~/.bashrc\n</code></pre>\n\n<h2 id=\"build-a-glance-app\"><a href=\"#build-a-glance-app\">Build a Glance app</a></h2>\n\n<p>At the moment, the only way to build a GUI app is using the Windows only (Booooo!) TouchGFX<sup id=\"fnref:76095.windoze\"><a href=\"#fn:76095.windoze\" class=\"footnote-ref\" title=\"Just after I wrote this, the SDK was updated to allow LVGL which should work on Linux. There's no documentation for it yet that I could find.\" role=\"doc-noteref\">0</a></sup>. So, instead, we'll make a \"Hello World\" glance. All it will do is draw some text.</p>\n\n<p>The <code>GlanceFloors</code> demo app is the simplest one to copy.</p>\n\n<pre><code class=\"language-bash\">cd $UNA_SDK/Examples/Apps/\ncp -r  GlanceFloors/ GlanceHello\n</code></pre>\n\n<p>Use an IDE or a text editor to open up the new GlanceHello directory.</p>\n\n<ul>\n<li>Rename the directory <code>GlanceHello/Software/App/GlanceFloors-CMake/</code> to <code>GlanceHello/Software/App/GlanceHello-CMake/</code></li>\n<li>Edit the file <code>GlanceHello/Software/App/GlanceFloors-CMake/CMakeLists.txt</code> - specifically the <code># App configuration</code> section.</li>\n<li>Change the <code>APP_NAME</code> to <code>GlanceHello</code></li>\n<li>Change the <code>APP_USER_NAME</code> to <code>Hello</code></li>\n</ul>\n\n<p>For a demo, you can leave the <code>DEV_ID</code> and <code>APP_ID</code> as their default values.</p>\n\n<h2 id=\"minimum-viable-app\"><a href=\"#minimum-viable-app\">Minimum viable app</a></h2>\n\n<p>You will need to edit two more files.</p>\n\n<p>In <code>GlanceHello/Software/Libs/Header/Service.hpp</code> remove the following lines:</p>\n\n<pre><code class=\"language-CPP\">SDK::Sensor::Connection mSensorFloors;\nuint32_t                mFloorsValue;\nbool                    mDataReceived;\n</code></pre>\n\n<p>Then save the file.</p>\n\n<p>Edit <code>GlanceHello/Software/Libs/Source/Service.cpp</code> and replace its entire contents with this:</p>\n\n<pre><code class=\"language-CPP\">#include \"Service.hpp\"\n#include \"SDK/Messages/CommandMessages.hpp\"\n#include \"SDK/Messages/MessageGuard.hpp\"\n\nService::Service(SDK::Kernel &amp;kernel)\n    : mKernel(kernel)\n    , mGlanceUI()\n    , mGlanceTitle()\n    , mGlanceValue()\n{}\n\nService::~Service(){}\n\nvoid Service::run()\n{\n    while (true) {\n        SDK::MessageBase *msg;\n        if(!mKernel.comm.getMessage(msg)) {\n            continue;\n        }\n\n        switch (msg-&gt;getType()) {\n\n            case SDK::MessageType::EVENT_GLANCE_START:\n                if (configGui()) {\n                    createGuiControls();\n                } else {\n                    mKernel.comm.releaseMessage(msg);\n                    mKernel.sys.exit(0);\n                }\n                break;\n\n            case SDK::MessageType::COMMAND_APP_STOP:\n            case SDK::MessageType::EVENT_GLANCE_STOP:\n                mKernel.comm.releaseMessage(msg);\n                return;\n                break;\n\n            case SDK::MessageType::EVENT_GLANCE_TICK:\n                onGlanceTick();\n                break;\n\n            default:\n                break;\n        }\n\n        mKernel.comm.releaseMessage(msg);\n    }\n}\n\nvoid Service::onGlanceTick()\n{\n    if (mGlanceUI.isInvalid()) {\n        if (auto upd = SDK::make_msg&lt;SDK::Message::RequestGlanceUpdate&gt;(mKernel)) {\n            upd-&gt;name           = APP_NAME;\n            upd-&gt;controls       = mGlanceUI.data();\n            upd-&gt;controlsNumber = static_cast&lt;uint32_t&gt;(mGlanceUI.size());\n            upd.send(100);\n        }\n\n        mGlanceUI.setValid();\n}\n}\n\nbool Service::configGui()\n{\n    bool status = false;\n    if (auto gc = SDK::make_msg&lt;SDK::Message::RequestGlanceConfig&gt;(mKernel)) {\n        if (gc.send(100) &amp;&amp; gc.ok()) {\n            if (gc-&gt;maxControls &gt;= 3) {\n                mGlanceUI.setWidth(gc-&gt;width);\n                mGlanceUI.setHeight(gc-&gt;height);\n                status = true;\n            }\n        }\n    }\n    return status;\n}\n\nvoid Service::createGuiControls()\n{\n    mGlanceTitle = mGlanceUI.createText();\n    mGlanceTitle.pos({ kTitleX, kTitleY }, { kTitleW, kTitleH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_TEAL)\n        .setText(\"Hello, World!\")\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n    mGlanceValue = mGlanceUI.createText();\n    mGlanceValue.pos({ 0, kValueY }, { 200, kValueH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_WHITE)\n        .setText(\"I know C++\")\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n}\n</code></pre>\n\n<p>Save the file.</p>\n\n<h2 id=\"compile-it\"><a href=\"#compile-it\">Compile it</a></h2>\n\n<p>On the command line</p>\n\n<pre><code class=\"language-bash\">cd $UNA_SDK/Examples/Apps/GlanceHello/Software/App/GlanceHello-CMake\ncmake -G \"Unix Makefiles\" -S . -B build\ncmake --build build\n</code></pre>\n\n<p>It should finish with a success message of <code>[100%] Built target GlanceHelloApp</code></p>\n\n<h2 id=\"copying-it-to-your-watch\"><a href=\"#copying-it-to-your-watch\">Copying it to your watch</a></h2>\n\n<ul>\n<li>Plug your watch into your computer with a USB-C cable.</li>\n<li>On your computer, open the watch's filesystem.</li>\n<li>Open the <code>Apps</code> directory.</li>\n<li>Create a new directory.</li>\n<li>Name the new directory with the string of characters from the <code>APP_ID</code> which is set in <code>CMakeLists.txt</code></li>\n<li>The app is stored in <code>GlanceHello/Output/</code> and will be called <code>Hello_1.5.0-rc5.uapp</code> or similar.</li>\n<li>Copy the <code>.uapp</code> file from your computer to the directory you just created on the watch.</li>\n<li>Eject the watch.</li>\n<li>Unplug the cable.</li>\n<li>Restart the watch by pressing the \"Up\" button until you get to \"Power Off\", then press \"Select\", then press \"Select\" again.</li>\n<li>Turn the watch on by holding down \"Select\".</li>\n<li>Once the watch has fully restarted, press the \"Down\" button until you see the \"Hello\" glance.</li>\n</ul>\n\n<p>If it has worked, you should see something like this:</p>\n\n<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/09/cpp.webp\" alt=\"A watch displaying &quot;Hello World&quot; and &quot;I know C++&quot;. The latter if off centre because I don't know C++.\" width=\"1926\" height=\"2048\" class=\"aligncenter\">\n\n<p>Hurrah! Treat yourself to a beer \ud83c\udf7b</p>\n\n<div id=\"footnotes\" role=\"doc-endnotes\">\n<hr aria-label=\"Footnotes\">\n<ol start=\"0\">\n\n<li id=\"fn:76095.windoze\">\n<p>Just after I wrote this, the SDK was updated to allow <a href=\"https://lvgl.io/\">LVGL</a> which should work on Linux. There's no documentation for it yet that I could find.&nbsp;<a href=\"#fnref:76095.windoze\" class=\"footnote-backref\" role=\"doc-backlink\">\u21a9\ufe0e</a></p>\n</li>\n\n</ol>\n</div><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76095&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/0std8-hz104","guid":"https://shkspr.mobi/blog/?p=76095","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/09/cpp.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791244800,"rid":"vrwx5-w7v30","summary":"I don't know why hardware manufacturers find it so hard to write decent documentation. As I've said before you need to actually test your readme. The Una Watch development instructions are scattered across a bunch of different files. So here's how to go from zero to your first \"Hello, World!\" Glance app with the minimum of fuss. This all assumes a fairly modern Debian / Ubuntu / Pop OS Linux.","tags":["/etc/","C++","HowTo","Programming","UnaWatch"],"title":"Una Watch - SDK and Writing Your First App","updated_at":1792592734,"url":"https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/","version":"v1"}},{"document":{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/10/are-you-really-the-doctor.avif\" alt=\"Book cover featuring a pair of crossed plasters in a dark brown colour.\" width=\"200\" class=\"alignleft\">\n\n<p>A few months ago, I went to see <a href=\"https://www.bbc.co.uk/programmes/m002vbxx\">Dr Hutchinson record his stand-up comedy special for the BBC</a> - it's about his experiences working in the NHS as a Black doctor. As the title of the book implies, some of the public can be a mite suspicious that anyone with even a hint of melanin can rise beyond being a porter or, at a push, a nurse.</p>\n\n<p>Much like <a href=\"https://shkspr.mobi/blog/2019/01/book-review-this-is-going-to-hurt/\">Adam Kay's \"This Is Going To Hurt\"</a>, this is part biography, part rant about the NHS, and part humorous look at what it takes to be a modern doctor. Hutchinson's book has rather less gallows humour and rather more racism.</p>\n\n<p>The problems of racial inequality in the NHS aren't just focussed on patients; they're also reflected in the way staff are treated. Rather than just recounting all the abuse he's been subject to, the book takes a good look at the actual statistics. Of course, this makes for slightly grim reading at times, so the frequent injections of humour are most welcome.</p>\n\n<p>The UK quite often seems in the shadow of larger cultures. As the book points out:</p>\n\n<blockquote><p>This same issue is understudied in the UK \u2013 to the point where the absence of research is glaring. While we are clearly not just a little America, we aren't so different that we can be confident we're not replicating the same here.</p></blockquote>\n\n<p>Because of that, at times the book does feel a little bit like the \"<a href=\"https://knowyourmeme.com/memes/we-have-food-at-home\">we have [racism] at home</a>\" meme.</p>\n\n<p>There's an almost relentlessly cheery irreverence to some of the issues - but what else can you do? Sometimes it takes a good dose of comedy to cut through the bleakness. There's a slight streak of ageism within the book - especially when it comes to older doctors trying to use new technology. Although, to be fair, that was also my experience when working at NHSX. There are some people within the system who are desperate for it to remain preserved in aspic as even minor improvements are a threat to their position in the hierarchy.</p>\n\n<p>A minor grumble, the problem I have with books like this - and with <a href=\"https://shkspr.mobi/blog/2019/05/review-the-secret-barrister/\">The Secret Barrister</a> - is that a lot of the moaning about how hard it is to become a professional doctor / lawyer just generates a giant shrug from me. Every piece of medical media in the last 50 years talks about how bone-crunchingly hard it is. All I can think of is \"none of this should be coming as a surprise to you!\" Should it be easier (and less sleep depriving) to be a doctor? Sure! But given it isn't, perhaps dial down on the clich\u00e9s of how uniquely awful your <em>chosen</em> career is.</p>\n\n<p>The book isn't just a list of complaints, of course, everything is backed up with statistics (although unreferenced). Thankfully, the book ends with some practical suggestions of how the NHS can improve. It would certainly be interesting to read a version of this from 2046 to see how much has changed - given the improvements already bearing fruit, I'd hope that the UK health service would become much less racist over time.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76754&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/bc4fr-qcx14","guid":"https://shkspr.mobi/blog/?p=76754","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/10/are-you-really-the-doctor.avif","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792195200,"rid":"mnchs-wqn69","summary":"A few months ago, I went to see Dr Hutchinson record his stand-up comedy special for the BBC - it's about his experiences working in the NHS as a Black doctor. As the title of the book implies, some of the public can be a mite suspicious that anyone with even a hint of melanin can rise beyond being a porter or, at a push, a nurse.","tags":["/etc/","Book Review","NHS","Racism"],"title":"Book Review: Are You *Really* The Doctor? by Matthew Hutchinson","updated_at":1792592730,"url":"https://shkspr.mobi/blog/2026/10/book-review-are-you-really-the-doctor-by-matthew-hutchinson/","version":"v1"}}],"items":[{"authors":[{"contributor_roles":[],"family":"Kaczmarek","given":"Ursula","url":"https://orcid.org/0000-0002-8244-1373"},{"affiliation":[{"id":"https://ror.org/03wc8by49","name":"Posit Software, PBC (United States)"}],"contributor_roles":[],"family":"Clawson","given":"James","url":"https://orcid.org/0009-0008-6756-0607"}],"blog":{"authors":null,"community_id":"3446712e-ab03-4bb7-8641-571dcda8d8cd","created":1787702400,"current_feed_url":null,"description":"Tracking disruptions to U.S. federal science funding, grant by grant.","doi":"https://doi.org/10.59350/grantwitness","favicon":"https://rogue-scholar.org/api/communities/3446712e-ab03-4bb7-8641-571dcda8d8cd/logo","feed_format":"application/atom+xml","feed_url":"https://grantwitness.org/updates/rogue-scholar.xml","filter":null,"generator":"Other","home_page_url":"https://grantwitness.org/updates?type=updates","issn":null,"language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"grantwitness","status":"active","subfield":"3321","title":"Grant Witness","updated":1792540800,"use_api":null},"blog_name":"Grant Witness","blog_slug":"grantwitness","content_html":"<p>With our latest update, <a href=\"https://www.lostfunds.org\">Lost Funds</a> tallies a current disruption of $170 billion in funds Congress committed to states and communities.    A familiar end-of-fiscal year pattern has yet again emerged in the most recent federal spending data: upticks in spending as agencies move to deplete their appropriated funds before they revert back to the Treasury. The latest data reflects spending numbers for a few agencies that fully cover the 2026 Fiscal Year. Other agencies like the Department of Education and EPA are still lagging a bit in their end of year reporting.</p>\n<h2>Reporting updates</h2>\n<p>While our overall disruption figure has lowered, 21 local governments, 1 school district, 30 businesses, and 211 other recipients have joined the ranks of affected recipients. These recipient counts change as we ascertain new areas of funding disruption. A new impact will often introduce new recipients. Likewise, when new spending comes in to remove an impact from a state, we remove those recipients from the counts.</p>\n<p>The biggest change in reported spending arises in the <strong>residents_snap_benefits</strong> impact area, which had more money coming in, leading to smaller losses in that impact.</p>\n<h2>New impact areas</h2>\n<p>With this update we're tracking seven new areas of funding disruption:</p>\n<ol>\n<li><strong>international_education:</strong> Department of Education funding supporting international studies and foreign language programs, along with Fulbright-Hays awards</li>\n<li><strong>full_service_community_schools:</strong> Department of Education funding related to the Full-Service Community Schools Program, which supports coordinated academic, health, social, and family services in schools in underserved communities</li>\n<li><strong>state_personnel_development:</strong> Department of Education funding that sustains the State Personnel Development Grants Program providing career prep and professional development opportunities to  special education personnel</li>\n<li><strong>libraries_services_children:</strong> Institute of Museum and Library Services funding supporting the Grants to States program, the largest source of federal funding for library services</li>\n<li><strong>state_grid_resilience:</strong> Department of Energy funding relating to the state portion of the Grid Resilience State and Tribal Formula Grants Program, which funds improvements to strengthen electric-grid resilience (note this new impact area expands upon and absorbs the impact previously called <strong>pr_energy_resilience</strong>)</li>\n<li><strong>industrial_demonstrations:</strong> Department of Energy funding that backs the Industrial Demonstrations Program at the Office of Clean Energy Demonstrations</li>\n<li><strong>maine_conservation_funding:</strong> A National Oceanic and Atmospheric Administration grant providing for salt-marsh restoration and coastal resilience in Maine</li>\n</ol>\n<p>We found Education Week's <a href=\"https://www.edweek.org/policy-politics/trump-slashed-billions-for-education-in-2025-see-our-list-of-affected-grants/2026/01\">coverage</a> of Department of Education grant <a href=\"https://www.edweek.org/policy-politics/in-trumps-first-year-at-least-12-billion-in-school-funding-disruptions/2026/01\">terminations</a> to be a particularly valuable signal source as we continue to expand our coverage of lost funds. We also continue to track relevant litigation alongside impact areas as states fight back against rescission of the sizable commitments Congress made to them and have noted no new plaintiff states with this update.</p>","doi":"https://doi.org/10.59350/hz0sq-93s53","guid":"https://doi.org/10.59350/hz0sq-93s53","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792540800,"rid":"qn5vd-ry598","summary":"With our latest update, Lost Funds tallies a current disruption of $170 billion in funds Congress committed to states and communities. A familiar end-of-fiscal year pattern has yet again emerged in the most recent federal spending data: upticks in spending as agencies move to deplete their appropriated funds before they revert back to the Treasury.","title":"Lost Funds: Data Update for October 15","updated_at":1792615505,"url":"https://grantwitness.org/updates/lost-funds-data-update-for-october-15","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Kun","given":"Jeremy"}],"blog":{"authors":[{"name":"Jeremy Kun"}],"community_id":"a9da26d0-1f88-4154-a730-28bb864b1c11","created":1780876800,"current_feed_url":null,"description":"Recent content on Math \u2229 Programming","doi":"https://doi.org/10.59350/jeremykun","favicon":"https://rogue-scholar.org/api/communities/a9da26d0-1f88-4154-a730-28bb864b1c11/logo","feed_format":"application/rss+xml","feed_url":"https://www.jeremykun.com/index.xml","filter":null,"generator":"Hugo","home_page_url":"https://www.jeremykun.com/","issn":null,"language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":null,"slug":"jeremykun","status":"active","subfield":"2605","title":"Math \u2229 Programming","updated":1792607261,"use_api":null},"blog_name":"Math \u2229 Programming","blog_slug":"jeremykun","content_html":"Table of Contents\nThis is part of a tutorial series on the CKKS homomorphic encryption scheme.\nIn the last article, we described the ciphertext-ciphertext addition and multiplication operations. We ended with two challenges, one to restore the squared scaling factor $\\Delta^2$ back to $\\Delta$, and the other to convert a three-tuple ciphertext (decrypting under $(1, s, s^2)$) back to a two-tuple ciphertext (decrypting under $(1, s)$).\nThis article covers the latter challenge, which is called relinearization.","doi":"https://doi.org/10.59350/ge6c9-9p826","guid":"https://www.jeremykun.com/2026/10/21/ckks-key-switching-and-relinearization/","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792540800,"rid":"35rkb-jtj64","summary":"Table of Contents This is part of a tutorial series on the CKKS homomorphic encryption scheme. In the last article, we described the ciphertext-ciphertext addition and multiplication operations.","title":"CKKS \u2014 Key Switching and Relinearization","updated_at":1792607704,"url":"https://www.jeremykun.com/2026/10/21/ckks-key-switching-and-relinearization/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Bearman","given":"Nick","url":"https://orcid.org/0000-0002-8396-4061"}],"blog":{"authors":null,"community_id":"890db2a2-185e-4579-9f86-c8d46c887729","created":1779235200,"current_feed_url":null,"description":"A blog on my teaching, open source GIS, travel and life","doi":"https://doi.org/10.59350/nickbearman","favicon":"https://rogue-scholar.org/api/communities/890db2a2-185e-4579-9f86-c8d46c887729/logo","feed_format":"application/rss+xml","feed_url":"https://nickbearman.com/blog.xml","filter":null,"generator":"Quarto","home_page_url":"https://nickbearman.github.io/blog.html","issn":null,"language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"nickbearman","status":"active","subfield":"2306","title":"Nick Bearman","updated":1790722800,"use_api":null},"blog_name":"Nick Bearman","blog_slug":"nickbearman","content_html":"<p>I was fortunate enough to be able to attend FOSS4G in Hiroshima, Japan, my third international FOSS4G conference. Japan is an amazing country - I have included some extra photos at the bottom if you are interested, and also a practical tip about booking trains!</p>\n<p>Hiroshima is a fantastic setting, and we were based in the International Conference Centre, right next to the Peace Memorial Park commemorating lives lost when the nuclear weapon was dropped on Hiroshima during World War II, and their continuing campaign for nuclear disarmament and wider peace. The museum is well worth a visit if you ever have the opportunity, a thought provoking and humbling experience.</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/hiroshima_dome.jpg\"/></p>\n<figcaption>The Hiroshima Bomb Dome, the only structure left standing in the area where the first atomic bomb exploded on 6 August 1945.</figcaption>\n</figure>\n</div>\n<section class=\"level3\" id=\"foss4g-2026\">\n<h3 class=\"anchored\" data-anchor-id=\"foss4g-2026\">FOSS4G 2026</h3>\n<p>The FOSS4G conference was excellent and an amazing opportunity to meet people, make new friends and refresh existing friendships. This year 828 people met together to talk about open source geospatial software, share their work and get comments and feedback.</p>\n<p>One big topic (but not the only topic!) was AI, or more specifically Generative AI and Large Language Models. Many people are using them - and there were some really interesting presentations about the benefits of them, and new ideas of how we might work with them. One great presentation was <a href=\"https://talks.osgeo.org/foss4g-2026/talk/RT8SMH/\">Teaching AI to Contribute to Open Source: Lessons from MapLibre Agent Skills</a> where Stephanie May shared the experience of MapLibre working with AI agents. There was also a significant amount of discussion about not using AI, for a number of different reasons.</p>\n<p>There is a larger discussion going on in the OSGeo and QGIS communities about the impact of these tools. A number of PRs have been submitted, particularly for QGIS, which are very hard to review and result in the core contributors wasting a lot of time trying to understand these changes. As a result, there is discussion about banning AI use with the QGIS codebase. This is still under active discussion, and as QGIS is an open project, you can see (and take part in if you want to) the discussions - for example via the <a href=\"https://lists.osgeo.org/pipermail/qgis-developer/\">QGIS Developer list</a> and QEPs on the QGIS GitHub page <a href=\"https://github.com/qgis/QGIS-Enhancement-Proposals/pull/388\">here</a> and <a href=\"https://github.com/qgis/QGIS-Enhancement-Proposals/pull/387\">here</a>.</p>\n<p>OSGeo made a statement that they are not taking a view on LLM usage and that it is for the individual projects to decide. This was mentioned in the opening ceremony and in the <a href=\"https://wiki.osgeo.org/wiki/Board_Meeting_2026-07-28#AI_policy_discussion\">OSGeo Board meeting minutes</a>. Since this has been adopted, there has been a motion put forward to change this. It is a rapidly moving field, and sometimes I find it quite hard to keep up with developments.</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/osgeo-ai.jpg\"/></p>\n<figcaption>The OSGeo statement on AI, on 1st Sept 2026.</figcaption>\n</figure>\n</div>\n<p>There was also a great <a href=\"https://talks.osgeo.org/foss4g-2026/talk/P9XGVU/\">AMA (Ask Me Anything)</a> session on QGIS run by Marco Bernasocchi where people could ask anything they wanted to. I discovered the difference between a CRS (Coordinate Reference System) and a SRS (Spatial Reference System) - CRS is usual in QGIS, but SRS appears occasionally. A SRS includes a geoid, a CRS does not. Also one of the many limitations of the shapefile format is that you cannot reorder columns in it - in other formats, like geopackage, you can.</p>\n<p>During the main conference, I also presented <a href=\"https://talks.osgeo.org/foss4g-2026/talk/UYL89P/\">gitRmap</a> as a lightning talk - it was great to share this tool with the world and get some useful feedback. One of the highlights for me was one of the keynote presentations <a href=\"https://talks.osgeo.org/foss4g-2026/talk/S9RDGB/\">Sustaining Open Source: What We Can Learn from the Cooperative Model</a> where Ela Kagel discussed elements from the Cooperative Model that might be useful for the Open Source community, and I also learnt about <a href=\"https://coopcycle.org/en/\">Coop Cycle</a>, a group of cooperative based cycle couriers.</p>\n</section>\n<section class=\"level3\" id=\"workshops\">\n<h3 class=\"anchored\" data-anchor-id=\"workshops\">Workshops</h3>\n<p>This year I had the opportunity to present a workshop at the conference. It was a great opportunity to share some R and GIS code with enthusiastic folks, and I also managed to sell a copy of my book, <a href=\"https://nickbearman.com/rgis.html\">Using R as a GIS</a>, which is always a lovely feeling.</p>\n<p>We also explored the inner workings of GDAL Virtual File Systems and after a bit of experimenting managed to be able to download, extract and read in a shapefile in one line. I probably won't be using it in the course - there's a lot going on in that one line - but it's a cool bit of code! Thanks! Check out <a href=\"https://gdal.org/en/stable/user/virtual_file_systems.html\">https://gdal.org/en/stable/user/virtual_file_systems.html</a> for more details.</p>\n<div class=\"cell\">\n<div class=\"sourceCode cell-code\" id=\"cb1\" style=\"background: #f1f3f5;\"><pre class=\"sourceCode r code-with-copy\"><code class=\"sourceCode r\"><span id=\"cb1-1\">sthelens <span class=\"ot\" style=\"color: #003B4F;\nbackground-color: null;\nfont-style: inherit;\">&lt;-</span> <span class=\"fu\" style=\"color: #4758AB;\nbackground-color: null;\nfont-style: inherit;\">st_read</span>(<span class=\"st\" style=\"color: #20794D;\nbackground-color: null;\nfont-style: inherit;\">\"/vsizip//vsicurl/http://www.nickbearman.com/files/sthelens_lsoa_2021.zip\"</span>)</span></code></pre></div>\n</div>\n<p>I also attended three other workshops over the two days, including <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/talk/SSQZQ3/\">Discovering PostGIS: An Introduction for QGIS Users</a>, <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/talk/UA8G9H/\">30 Maps in 3 Hours</a> and <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/talk/YVR8DV/\">NextGIS Web for QGIS teams: from deployment to collaboration</a>. They were all really good workshops and one consistent thing across all of the workshops is people's passion about the tools they are talking about and willingness to share this knowledge with others. I find the workshops some of the most interesting bits of the conference - and I couldn't attend all that I wanted to. Fortunately most of the workshop convenors share their materials via <a href=\"https://talks.osgeo.org/foss4g-2026-workshop/schedule/\">talks.osgeo.org</a> so the material is available to work through later on.</p>\n<p>The final day of the conference featured the code sprint, where I spent a bit of time working on gitRmap. I also received my first <a href=\"https://github.com/nickbearman/gitRmap/pull/6\">PR</a> for this from <a href=\"https://github.com/yutannihilation\">Hiroaki Yutani</a> working with me, so that was cool! I also managed to submit the first <a href=\"https://github.com/yutannihilation/ggplot2qgis/pull/23\">PR</a> to his project ggplot2qgis as well, so a great day for collaboration!</p>\n<p>The conference is also a great time and place to meet people - I met a wide range of users and developers, some who I had heard of before and many who I had not. Everyone there is enthusiastic about open source geospatial and it is a special place to be. As usual, we had a group photo which is a great memory of the week:</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/foss4g-2026-group-photo.jpg\"/></p>\n<figcaption>FOSS4G 2026 group photo - it's all about the people!</figcaption>\n</figure>\n</div>\n</section>\n<section class=\"level3\" id=\"cagis-st-louis-usa\">\n<h3 class=\"anchored\" data-anchor-id=\"cagis-st-louis-usa\">CaGIS, St Louis, USA</h3>\n<p>The following week I was also fortunate to be able to attend the CaGIS (Cartography and Geographic Information Society) conference in St Louis, MO, USA. I work as Cartographic Editor for CaGIS so it was a great opportunity to meet colleagues and authors who I exchange many emails with but rarely meet in person. This conference has a much more academic flavour than FOSS4G, and is also much smaller - about 120 people. We had two days of conference and one day of workshops, and also there was Geo-Resolution as well, which is a one day conference for the Geospatial Intelligence and Defence communities, which have a large presence in St.\u00a0Louis and the surrounding areas. It's a very different flavour again and always interesting to see how different people use geospatial.</p>\n</section>\n<section class=\"level3\" id=\"booking-trains-in-japan\">\n<h3 class=\"anchored\" data-anchor-id=\"booking-trains-in-japan\">Booking Trains in Japan</h3>\n<p>My travel plans included getting the train from Tokyo to Hiroshima. While Hiroshima does have an airport, my flight into Japan came in to Tokyo. The Japanese train service has a very good reputation, and the journey from Tokyo to Hiroshima is ~4.5 hours, so I decided to take the train instead.</p>\n<p>In fact, the conference venue travel page explicitly states \"There is no air service between Hiroshima and Osaka, Nagoya or Fukuoka. The JR railway lines provide convenient access instead.\"</p>\n<p>Thanks to <a href=\"https://www.seat61.com/Japan.htm\">Seat61</a>, I found the train information via <a href=\"https://booking.baolau.com/en/\">Baolau</a>. Japanese trains don't have advanced tickets - it is a flat fare whether you buy 1 month before or on the day. If you book ahead, you can reserve a seat, but for the journeys I'm making there are 2 or more trains an hour. So I booked the first one ahead of time (Tokyo to Hiroshima) but for the return journey, breaking in Kyoto, I will buy on the day.</p>\n<p><em>In the end I missed my scheduled train from Tokyo to Hiroshima - but you can just catch the next one, even with an advanced ticket. You just don't get a reserved seat.</em></p>\n<div class=\"columns\">\n<div class=\"column\" style=\"width:45%;\">\n<p>I also particularly liked this graphic from the ICC website, about how to travel to the conference centre:</p>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/icc-hiroshima-e-access.png\"/></p>\n<figcaption>From https://www.pcf.city.hiroshima.jp/icch/location.html</figcaption>\n</figure>\n</div>\n</div><div class=\"column\" style=\"width:45%;\">\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/travelling-on-a-bullet-train.jpg\"/></p>\n<figcaption>Travelling on a Bullet Train</figcaption>\n</figure>\n</div>\n<hr/>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/bento-boxes-lunch.jpg\"/></p>\n<figcaption>Lunch at the conference - Bento Boxes!</figcaption>\n</figure>\n</div>\n<hr/>\n<div class=\"quarto-figure quarto-figure-center\">\n<figure class=\"figure\">\n<p><img class=\"img-fluid figure-img\" src=\"https://nickbearman.github.io/blog/2026-09-foss4g-2026/hiroshima_peace_park_night.jpg\"/></p>\n<figcaption>Hiroshima Peace Park at night</figcaption>\n</figure>\n</div>\n</div>\n</div>\n</section>","doi":"https://doi.org/10.59350/5jx8a-xj967","guid":"https://nickbearman.github.io/blog/2026-09-foss4g-2026/","image":"https://nickbearman.github.io/blog/2026-09-foss4g-2026/hiroshima_dome_header.jpg","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1790640000,"rid":"gh43c-46k26","summary":"I was fortunate enough to be able to attend FOSS4G in Hiroshima, Japan, my third international FOSS4G conference. Japan is an amazing country - I have included some extra photos at the bottom if you are interested, and also a practical tip about booking trains!","tags":["Conference","FOSS4G","In Person","Open Source","Train"],"title":"FOSS4G 2026, Hiroshioma, Japan - All About the People!","updated_at":1792599799,"url":"https://nickbearman.github.io/blog/2026-09-foss4g-2026/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/10/Video_Games_Go_Choral.webp\" alt=\"A choir stands in front of a video game background.\" width=\"256\" height=\"256\" class=\"alignleft\">\n\n<p>London's musical entertainment scene has something for everyone - from long-running blockbuster shows to no-name bands in dingy basements. Somewhere in the middle is monophonic plainchant versions of the Halo theme sung in a church.</p>\n\n<p>The acoustics at St Martin in the Field are echoey. That makes it rather hard to hear the amplified announcement to turn off our phones, but makes an <i lang=\"it\">a cappella</i> chorus absolutely soar.  The choir of London Voices have sung on <a href=\"https://www.london-voices.com/films-gaming\">just about every movie you've watched and game you've played</a> - so they're no stranger to more modern compositions.</p>\n\n<p>The programme strictly alternated between songs from popular games are some more traditional verses. As a tone-deaf musical ignoramus, I found some of the pieces hard to distinguish from each other and, perhaps, just a little repetitive. Delightfully, the choir is generous with its solos. Far too often these events have one main star while everyone else glares daggers at them. Here it seemed quite the opposite, with a range of voices brought to the forefront.</p>\n\n<p>Parts of the programme are a little odd. While Pilentze Pee is an incredible song and sung extremely well, I don't think there's much to tie it to The Wind Waker. Of course, the highlight was a cheeky rendition of \"Still Alive\" from Portal. An excellent arrangement sung with joy and gusto.</p>\n\n<p>An eclectic and interesting event, marred only by the uncomfortable pews of the church. Truly we must suffer for other people's art!</p>\n\n<iframe title=\"London Voices: Video Games Go Choral Trailer\" width=\"620\" height=\"349\" src=\"https://www.youtube.com/embed/WfhgC6G9EIg?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen=\"\"></iframe><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76517&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/bypqq-x7y72","guid":"https://shkspr.mobi/blog/?p=76517","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/10/Video_Games_Go_Choral.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791504000,"rid":"zbhdq-g3g57","summary":"London's musical entertainment scene has something for everyone - from long-running blockbuster shows to no-name bands in dingy basements. Somewhere in the middle is monophonic plainchant versions of the Halo theme sung in a church. The acoustics at St Martin in the Field are echoey.","tags":["/etc/","Gaming","Gig","Review"],"title":"Concert Review: London Voices - Video Games Go Choral","updated_at":1792592746,"url":"https://shkspr.mobi/blog/2026/10/concert-review-london-voices-video-games-go-choral/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users.</p>\n\n<p>I na\u00efvely believe there's a better way. As I've written before, you should sit down and <a href=\"https://shkspr.mobi/blog/2025/10/how-to-actually-test-your-readme/\"><em>actually</em> test your readme</a>. Spin up a fresh Virtual Machine, go through what you've written, and see if it makes sense.</p>\n\n<p>But it is <em>really</em> hard to ignore your own biases. Of course you know that certain commands require <code>sudo</code> and obviously when you wrote <code>-foo</code> you meant <code>--foo</code> and everyone knows that you have to reboot afterwards.</p>\n\n<p>How do you get rid of those biases? I paid people!</p>\n\n<p>As part of my <a href=\"https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/\">NLnet grant application for ActivityBot</a>, I said I wanted to test the install experience with real users and I was prepared to pay them \u20ac25 for an hour of their time. I stuck out the call on Mastodon, gathered a few people, and had a video call with them.</p>\n\n<p>The format was simple. I explained that I was looking for feedback on the first-run experience. I knew that it wasn't perfect and actively wanted constructive criticism. I asked the volunteer to share their screen and, crucially, to speak aloud. Tell me what they were doing. What they didn't understand, what confused them, what delighted them, what frustrated them, etc.</p>\n\n<p>I took notes by hand (fuck feeding the machine) and spent several hours being told what incorrect assumptions I'd made.</p>\n\n<p>Amongst the highlights I discovered were:</p>\n\n<ul>\n<li>The link to the demo tool was wrong.</li>\n<li>Some people read a README in the terminal.</li>\n<li>What does it actually mean to rename a file?</li>\n<li>How do you rename a hidden file?</li>\n<li>Should a demo tool be available on the web or just the user's local machine?</li>\n<li>Why does some text explicitly need to be quoted and some not?</li>\n<li>My jokes aren't funny and are actively confusing.</li>\n<li>Some of the technical terminology needed explaining.</li>\n<li>The ordering of the different sections was confusing.</li>\n<li>I hadn't actually explained what the software would do.</li>\n<li>A whole section which was technically interesting to me was utterly confusing to everyone else.</li>\n<li>Not all web servers use permissions in the same way.</li>\n</ul>\n\n<p>And on it went!</p>\n\n<p>After each session I updated the README based on what people found difficult, then I re-tested it with the next person.</p>\n\n<p>In total, I paid out around \u20ac150 to have a bunch of people criticise me to my face. Hey, cheaper than therapy, right?</p>\n\n<p>I know someone is going to say \"why not just ask an LLM to simulate a range of users?\" The answer is very simple - I want to speak to real people. People are brilliant! They can make you laugh, you can see their cat when it wanders on to the call, they bring a unique perspective to the problem, and they're really happy when you give them a \u20ac25 voucher. Some will gladly do it for free and make <em>you</em> happy!</p>\n\n<!--\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b Welcome to Comment Club! Shhhh! Only cool people can read this bit of the post.\n\nI did have a serious think about using an LLM for this. Ethics aside (and you can never really put ethics aside) I just didn't feel like I could trust their judgement. I get the argument that it is not much different from using a spellcheck - but I fundamentally don't want to feed the beast.\n\nAnyway, the three rules of Comment Club are\u2026\n\n1. You must not tell anyone about Comment Club - let them find out about it themselves.\n2. You *must* add some Comment Club Content to at least one of your blog posts. Doesn't have to be much, just a little note will do.\n3. You should drop an email to anyone whose comments you've read. Start by sending me a friendly message at comment.club@shkspr.mobi\n\nKeep your eyes peeled for more comments in future blog posts \ud83d\ude03\n\nBe seeing you \ud83d\udc4c\ud83d\udc41\n-->\n\n<p>When I was doing technical writing for GOV.UK, all my beautiful prose was given a second-eye by another human. They eviscerated all my flowery phrases and turned the document into something more readable. They were the ones who caught the mistakes that no spell chequer could. They were able to have a proper conversation with me. More importantly, I could <em>hear</em> the frustration in their voice - that's the thing which lets you know a mistake needs to be corrected.</p>\n\n<p>I don't claim that <a href=\"https://gitlab.com/edent/activity-bot/-/blob/main/README.md?ref_type=heads\">ActivityBot's README</a> is now perfect - far from it - but it is now demonstrably easier to follow. Developers need to talk to <em>real</em> people. You don't have to pay them if you can't afford it - but find a few people who will talk aloud while they try to follow your instructions. I guarantee you that your README will become much better for it.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76333&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/41s5v-7q247","guid":"https://shkspr.mobi/blog/?p=76333","image":"https://shkspr.mobi/blog/wp-content/uploads/2020/01/Storm-Trooper-Chocolate-Money.jpg","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791676800,"rid":"hqgd2-gjq74","summary":"Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users. I na\u00efvely believe there's a better way.","tags":["/etc/","ActivityBot","ActivityPub","Developers"],"title":"I paid people to try and follow my README","updated_at":1792592744,"url":"https://shkspr.mobi/blog/2026/10/i-paid-people-to-try-and-follow-my-readme/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>I'll be radically honest here - I never really got the idea of \"Test Driven Development\". I'm much more of a \"telnet into production and damn the consequences\" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again.</p>\n\n<p>The essence of unit testing is pretty simple:</p>\n\n<ol>\n<li>Feed a function some data</li>\n<li>See how it responds</li>\n</ol>\n\n<p>Send it good data and if it returns \"true\" the test passes. Send it bad data and make sure it returns false. Easy peasy lemon squeezy.</p>\n\n<p>As part of my <a href=\"https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/\">NLnet funded work on ActivityBot</a>, I'm writing some proper tests for my bot framework. I am hopeful that some of them will be useful to other projects doing similar things.</p>\n\n<p>There are four major classes of tests - here are the problems I've found while writing them.</p>\n\n<h2 id=\"basic-text-validation\"><a href=\"#basic-text-validation\">Basic text validation</a></h2>\n\n<p>A basic test might be how to validate that an ActivityPub username is valid. Typically it would be something like <code>@edent@mastodon.social</code> - what assumptions do we make with that? It starts with an <code>@</code> and has another <code>@</code> somewhere in it, dividing into two parts, the user and the server.  The server has to be a valid domain name - but that can include non-ASCII Internationalised Domain Names and Emoji domains.</p>\n\n<p>What about the user part? Is it just A-Z and 0-9? Can it have dots? What about apostrophes like an email address?</p>\n\n<p>Basically, how easy is it find the specifications for all the various component parts of ActivityPub? Not very!</p>\n\n<h2 id=\"message-validation\"><a href=\"#message-validation\">Message validation</a></h2>\n\n<p>What does a <a href=\"https://www.rfc-editor.org/info/rfc7033/\">WebFinger response</a> look like? Which parts are mandatory and what values can they contain?</p>\n\n<p>With any message that your project generates, how can you be sure that other ActivityPub servers will understand it?</p>\n\n<p>Similarly, when we receive a follow message from an external server, what should it look like?</p>\n\n<p>There are some <a href=\"https://github.com/steve-bate/fediverse-jsonschema\">JSON Schemas</a> which can be used to validate some type of messages.</p>\n\n<p>But, again, there are <em>many</em> scattered specifications.</p>\n\n<h2 id=\"signature-verification\"><a href=\"#signature-verification\">Signature verification</a></h2>\n\n<p>As I've written about before, <a href=\"https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/\">HTTP Signatures are difficult</a>. There are two main problems. The first is verifying signatures.</p>\n\n<p>In order to do that, you need to gather a bunch of signatures and then find a way to replay them into the test. So I'm in the process of saving signatures which I can then share with others.</p>\n\n<p>The second problem is <em>generating</em> signatures.  Sure, you can run tests on the signatures - but there's only one <em>real</em> way to test whether they're acceptable\u2026</p>\n\n<h2 id=\"communicating-with-others\"><a href=\"#communicating-with-others\">Communicating with others</a></h2>\n\n<p>Passing tests means nothing if you can't communicate with others and they can't communicate with you.</p>\n\n<p>Some tests are easier than others. Sending a signed request should be easy and repeatable. But receiving messages isn't easily automated. It requires being able to control an external server and asking that to send messages.</p>\n\n<p>With the <a href=\"https://docs.joinmastodon.org/client/intro/\">Mastodon API</a> it is possible to make requests which will interact with your server - but only if it is a <em>public</em> server.</p>\n\n<h2 id=\"putting-it-all-together\"><a href=\"#putting-it-all-together\">Putting it all together</a></h2>\n\n<p>I'm putting <a href=\"https://gitlab.com/edent/activity-bot/-/tree/main/tests?ref_type=heads\">all my tests online</a>. I'm also sharing a <a href=\"https://gitlab.com/edent/activity-bot/-/tree/main/tests/headers?ref_type=heads\">collection of messages and signatures</a> which you can use in your tests.</p>\n\n<p>I'd love to know if you find them useful.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=75774&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/nmxzr-19z59","guid":"https://shkspr.mobi/blog/?p=75774","image":"https://shkspr.mobi/blog/wp-content/uploads/2012/09/709ae69b.png","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791590400,"rid":"s1wdt-2k111","summary":"I'll be radically honest here - I never really got the idea of \"Test Driven Development\". I'm much more of a \"telnet into production and damn the consequences\" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again.","tags":["/etc/","ActivityBot","ActivityPub","PHP"],"title":"Some quick thoughts on Unit Testing ActivityPub","updated_at":1792592742,"url":"https://shkspr.mobi/blog/2026/10/some-thoughts-on-unit-testing-activitypub/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>Let's get this out of the way first - this is a <em>terrible</em> idea!  I want my two-factor authentication codes to be easily accessible to me, so I've built a watch app which will display them. That's really convenient for me - but makes for a terrible security posture. Anyone who steals my watch will be able to see my one-time code. If they're clever, they can decompile the app and find the secret which powers the code generation.</p>\n\n<p>You should absolutely not do this and, if you ignore my warnings, you should only do this for codes which aren't critical to your security.</p>\n\n<!-- \n\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\ud83d\udc4b\nWelcome to Comment Club! If this is your first time - hello! This content is only available to sophisticated nerds who read my HTML comments.\n\nYou know what? I *do* use this. I have a low-ish security account which constantly needs my MFA number. Sometimes I can't be bothered unlocking my phone to get it. Of course, on my wrist I save it under a different name. Perhaps I'm an idiot for doing so, but sometimes convenience wins.\n\nAnyway, the three rules of Comment Club are\u2026\n\n1. You must not tell anyone about Comment Club - let them find out about it themselves.\n2. You *must* add some Comment Club Content to at least one of your blog posts. Doesn't have to be much, just a little note will do.\n3. You should drop an email to anyone whose comments you've read. Start by sending me a friendly message at comment.club@shkspr.mobi\n\nKeep your eyes peeled for more comments in my other blog posts \ud83d\ude03\n\nTTFN.\n-->\n\n<p>In my defence, an attacker would still need my username and password to make any use of these codes - but please don't trust your security to my deranged witterings.</p>\n\n<h2 id=\"what-it-looks-like\"><a href=\"#what-it-looks-like\">What It Looks Like</a></h2>\n\n<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/09/MFA.webp\" alt=\"My wristwatch showing &quot;HSBC MFA Code 751646&quot;. Don't worry, this isn't my real code. OR IS IT?!?!\" width=\"1024\" height=\"576\" class=\"aligncenter\">\n\n<h2 id=\"prior-art\"><a href=\"#prior-art\">Prior Art</a></h2>\n\n<p>I'm sure many other people have done this. Indeed, three years ago I built <a href=\"https://shkspr.mobi/blog/2023/07/an-eink-wrist-mounted-totp-generator/\">An eInk, Wrist-Mounted, TOTP Generator</a>.</p>\n\n<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2023/07/Wrist-TOTP.jpg\" alt=\"A chunky wristwatch showing the time and a selection of 6 digit codes and their corresponding entities.\" width=\"1024\" height=\"1024\" class=\"aligncenter\">\n\n<h2 id=\"the-service\"><a href=\"#the-service\">The Service</a></h2>\n\n<p>This is in my <code>Service.cpp</code> - as you can see, it's pretty simple. This won't let you add in hundreds of codes - you get one and that's it. There's no countdown when a code is about to expire, so I recommend waiting until a new one pops up before you start typing it in.</p>\n\n<p>I'll be candid, I haven't used C++ properly since university - and that was in the last century. I've mostly just cut things out to see what breaks. Feel free to educate me on the correct idiomatic code.</p>\n\n<p>You will need to set your own secret in here.</p>\n\n<pre><code class=\"language-cpp\">//  Standard includes.\n#include \"Service.hpp\"\n#include \"SDK/Messages/CommandMessages.hpp\"\n#include \"SDK/Messages/MessageGuard.hpp\"\n\n//  Needed for PureTOTP.\n#include &lt;iostream&gt;\n#include \"TOTP.h\"\n\n#include &lt;string&gt;\n#include &lt;sstream&gt;\n\n//  What this Glance can do.\nService::Service(SDK::Kernel &amp;kernel)\n    : mKernel(kernel)\n    , mGlanceUI()\n    , mGlanceTitle()\n    , mGlanceValue()\n{}\n\nvoid Service::createGuiControls()\n{\n    //  Set the main title in the Glance.\n    mGlanceTitle = mGlanceUI.createText();\n    mGlanceTitle.pos({ kTitleX, kTitleY }, { kTitleW, kTitleH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_TEAL)\n        .setText(\"HSBC MFA Code\")\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n\n    //  Set the default text for the value.\n    mGlanceValue = mGlanceUI.createText();\n    mGlanceValue.pos({ 20, kValueY }, { 200, kValueH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_YELLOW_DARK)\n        .setText( \"Waiting for update\" )\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n}\n\nvoid Service::glanceUpdate()\n{\n    //  Set the Base32 encoded secret (all uppercase).\n    std::string secret = \"QWERTYUIOP\";\n    //  Generate the code based on the Unix timestamp right now.\n    const std::time_t t = std::time(0);\n    std::string code = TOTP::GenerateTOTP( 6, 30, TOTP::HMAC_SHA1, secret, t );\n\n    std::ostringstream ss;\n    ss &lt;&lt; t;\n\n    //  Convert the string to char* and display it as the value.\n    mGlanceValue.print( code.c_str() ); \n\n    //  Show the Unix time to check the timezone is correct.\n    // mGlanceTitle.print( ss.str().c_str() );\n}\n\n//  No service.\nService::~Service(){}\n\n//  When this glance is opened.\nvoid Service::run()\n{\n    while (true) {\n        SDK::MessageBase *msg;\n        if(!mKernel.comm.getMessage(msg)) {\n            continue;\n        }\n\n        switch (msg-&gt;getType()) {\n            //  The Glance has started.\n            case SDK::MessageType::EVENT_GLANCE_START:\n                if (configGui()) {\n                    createGuiControls();\n                } else {\n                    mKernel.comm.releaseMessage(msg);\n                    mKernel.sys.exit(0);\n                }\n                break;\n            //  The Glance has been stopped.\n            case SDK::MessageType::COMMAND_APP_STOP:\n            case SDK::MessageType::EVENT_GLANCE_STOP:\n                mKernel.comm.releaseMessage(msg);\n                return;\n                break;\n\n            //  Every second, perform an action.\n            case SDK::MessageType::EVENT_GLANCE_TICK:\n                onGlanceTick();\n                break;\n\n            default:\n                break;\n        }\n\n        mKernel.comm.releaseMessage(msg);\n    }\n}\n\nvoid Service::onGlanceTick()\n{\n    if (mGlanceUI.isInvalid()) {\n        if (auto upd = SDK::make_msg&lt;SDK::Message::RequestGlanceUpdate&gt;(mKernel)) {\n            upd-&gt;name           = APP_NAME;\n            upd-&gt;controls       = mGlanceUI.data();\n            upd-&gt;controlsNumber = static_cast&lt;uint32_t&gt;(mGlanceUI.size());\n            upd.send(100);\n        }\n\n        mGlanceUI.setValid();\n    }\n    glanceUpdate();\n}\n\nbool Service::configGui()\n{\n    bool status = false;\n    if (auto gc = SDK::make_msg&lt;SDK::Message::RequestGlanceConfig&gt;(mKernel)) {\n        if (gc.send(100) &amp;&amp; gc.ok()) {\n            if (gc-&gt;maxControls &gt;= 3) {\n                mGlanceUI.setWidth(gc-&gt;width);\n                mGlanceUI.setHeight(gc-&gt;height);\n                status = true;\n            }\n        }\n    }\n    return status;\n}\n</code></pre>\n\n<h2 id=\"the-library\"><a href=\"#the-library\">The Library</a></h2>\n\n<p>There are dozens of C++ libraries for RFC 6238 generation. I ended up using <a href=\"https://github.com/Einztein/PureTOTP\">PureTOTP</a> as it didn't require any external dependencies and was only two files.  As I only needed SHA1 TOTP codes, I stripped out a bunch of functionality which was unnecessary for me. Similarly, I only wanted to generate 6 digit codes with a 30 second period - so took out anything which did more than that.</p>\n\n<p>I've tidied up the code and comments where I can. The original was MIT Licenced - so this is as well.</p>\n\n<p>There are two parts to this. First, the header</p>\n\n<pre><code class=\"language-_\">// TOTP.h\n//\n#pragma once\n#include &lt;string&gt;\nclass TOTP\n{\npublic:\n    const static int HMAC_SHA1 = 20;    //  Length of HMAC.\n\n    static std::string GenerateTOTP(const std::string&amp; secret);\n    static std::string GenerateTOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp; secret, int specTime);\n\n    TOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp; secret): digit(digit), refreshSeconds(refreshSeconds), algorithm(algorithm), secret(secret) {}\n    TOTP(const std::string&amp; secret): digit(6), refreshSeconds(30), algorithm(HMAC_SHA1), secret(secret) {}\n\n    std::string currentTOTP();\n\nprivate:\n    int digit, refreshSeconds, algorithm;\n\n    std::string secret;\n};\n</code></pre>\n\n<p>Next is the standalone library which does the TOTP generation, <code>TOTP.cpp</code></p>\n\n<pre><code class=\"language-cpp\">// TOTP.cpp\n//\n#include \"TOTP.h\"\n#include &lt;vector&gt;\n#include &lt;stack&gt;\n#include &lt;queue&gt;\n#include &lt;sstream&gt;\n#include &lt;iomanip&gt;\n#include &lt;cstdint&gt;\n#include &lt;cstring&gt;\n#include &lt;array&gt;\n#include &lt;stdexcept&gt;\n#include &lt;algorithm&gt;\nusing namespace std;\n\nstruct JEVP_MD\n{\n    enum Kind {\n        SHA1\n    };\n\n    Kind kind;\n    int  md_size;\n    int  block_size;\n};\n\nstatic inline uint32_t rotl32(uint32_t x, uint32_t n) { return (x &lt;&lt; n) | (x &gt;&gt; (32 - n)); }\nstatic inline uint32_t rotr32(uint32_t x, uint32_t n) { return (x &gt;&gt; n) | (x &lt;&lt; (32 - n)); }\nstatic inline uint64_t rotr64(uint64_t x, uint64_t n) { return (x &gt;&gt; n) | (x &lt;&lt; (64 - n)); }\n\nstatic uint32_t load_be32(const unsigned char* p)\n{\n    return (uint32_t(p[0]) &lt;&lt; 24) |\n           (uint32_t(p[1]) &lt;&lt; 16) |\n           (uint32_t(p[2]) &lt;&lt;  8) |\n            uint32_t(p[3]);\n}\n\nstatic void store_be32(uint32_t v, unsigned char* p)\n{\n    p[0] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 24) &amp; 0xFF);\n    p[1] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 16) &amp; 0xFF);\n    p[2] = static_cast&lt;unsigned char&gt;((v &gt;&gt;  8) &amp; 0xFF);\n    p[3] = static_cast&lt;unsigned char&gt;( v        &amp; 0xFF);\n}\n\nstatic void store_be64(uint64_t v, unsigned char* p)\n{\n    p[0] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 56) &amp; 0xFF);\n    p[1] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 48) &amp; 0xFF);\n    p[2] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 40) &amp; 0xFF);\n    p[3] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 32) &amp; 0xFF);\n    p[4] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 24) &amp; 0xFF);\n    p[5] = static_cast&lt;unsigned char&gt;((v &gt;&gt; 16) &amp; 0xFF);\n    p[6] = static_cast&lt;unsigned char&gt;((v &gt;&gt;  8) &amp; 0xFF);\n    p[7] = static_cast&lt;unsigned char&gt;( v        &amp; 0xFF);\n}\n\nstatic vector&lt;unsigned char&gt; sha1_hash(const unsigned char* data, size_t len)\n{\n    vector&lt;unsigned char&gt; msg(data, data + len);\n    uint64_t              bit_len = static_cast&lt;uint64_t&gt;(len) * 8;\n\n    msg.push_back(0x80);\n    while ((msg.size() % 64) != 56) msg.push_back(0x00);\n\n    unsigned char lenbuf[8];\n    store_be64(bit_len, lenbuf);\n    msg.insert(msg.end(), lenbuf, lenbuf + 8);\n\n    uint32_t h0 = 0x67452301;\n    uint32_t h1 = 0xEFCDAB89;\n    uint32_t h2 = 0x98BADCFE;\n    uint32_t h3 = 0x10325476;\n    uint32_t h4 = 0xC3D2E1F0;\n\n    for (size_t chunk = 0; chunk &lt; msg.size(); chunk += 64)\n    {\n        uint32_t w[80];\n        for (int i = 0; i &lt; 16; ++i)\n            w[i] = load_be32(&amp;msg[chunk + i * 4]);\n        for (int i = 16; i &lt; 80; ++i)\n            w[i] = rotl32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);\n\n        uint32_t a = h0, b = h1, c = h2, d = h3, e = h4;\n\n        for (int i = 0; i &lt; 80; ++i)\n        {\n            uint32_t f, k;\n            if (i &lt; 20)\n            {\n                f = (b &amp; c) | ((~b) &amp; d);\n                k = 0x5A827999;\n            }\n            else if (i &lt; 40)\n            {\n                f = b ^ c ^ d;\n                k = 0x6ED9EBA1;\n            }\n            else if (i &lt; 60)\n            {\n                f = (b &amp; c) | (b &amp; d) | (c &amp; d);\n                k = 0x8F1BBCDC;\n            }\n            else\n            {\n                f = b ^ c ^ d;\n                k = 0xCA62C1D6;\n            }\n\n            uint32_t temp = rotl32(a, 5) + f + e + k + w[i];\n\n            e = d;\n            d = c;\n            c = rotl32(b, 30);\n            b = a;\n            a = temp;\n        }\n\n        h0 += a;\n        h1 += b;\n        h2 += c;\n        h3 += d;\n        h4 += e;\n    }\n\n    vector&lt;unsigned char&gt; out(20);\n    store_be32(h0, &amp;out[ 0]);\n    store_be32(h1, &amp;out[ 4]);\n    store_be32(h2, &amp;out[ 8]);\n    store_be32(h3, &amp;out[12]);\n    store_be32(h4, &amp;out[16]);\n    return out;\n}\n\nstatic const JEVP_MD g_sha1   = {JEVP_MD::SHA1, 20, 64};\n\nconst JEVP_MD* JEVP_sha1() { return &amp;g_sha1; }\n\nunsigned char* JHMAC(const JEVP_MD* JEVP_MD, const void* key, int key_len, const unsigned char* data, size_t data_len, unsigned char* md, unsigned int* md_len)\n{\n    if (!JEVP_MD || !key || key_len &lt; 0 || !data)\n        return nullptr;\n\n    static thread_local unsigned char static_md[64];\n\n    if (!md)\n        md = static_md;\n\n    vector&lt;unsigned char&gt; key_block(JEVP_MD-&gt;block_size, 0);\n    const unsigned char*  key_bytes = static_cast&lt;const unsigned char*&gt;(key);\n\n    vector&lt;unsigned char&gt; real_key;\n    if (key_len &gt; JEVP_MD-&gt;block_size)\n    {\n        real_key = sha1_hash(key_bytes, static_cast&lt;size_t&gt;(key_len));\n        std::copy(real_key.begin(), real_key.end(), key_block.begin());\n    }\n    else\n    {\n        std::copy(key_bytes, key_bytes + key_len, key_block.begin());\n    }\n\n    vector&lt;unsigned char&gt; ipad(JEVP_MD-&gt;block_size);\n    vector&lt;unsigned char&gt; opad(JEVP_MD-&gt;block_size);\n    for (int i = 0; i &lt; JEVP_MD-&gt;block_size; ++i)\n    {\n        ipad[i] = static_cast&lt;unsigned char&gt;(key_block[i] ^ 0x36);\n        opad[i] = static_cast&lt;unsigned char&gt;(key_block[i] ^ 0x5C);\n    }\n\n    vector&lt;unsigned char&gt; inner;\n    inner.reserve(JEVP_MD-&gt;block_size + data_len);\n    inner.insert(inner.end(), ipad.begin(), ipad.end());\n    inner.insert(inner.end(), data, data + data_len);\n\n    vector&lt;unsigned char&gt; inner_hash = sha1_hash(inner.data(), inner.size());\n\n    vector&lt;unsigned char&gt; outer;\n    outer.reserve(JEVP_MD-&gt;block_size + inner_hash.size());\n    outer.insert(outer.end(), opad.begin(), opad.end());\n    outer.insert(outer.end(), inner_hash.begin(), inner_hash.end());\n\n    vector&lt;unsigned char&gt; final_hash = sha1_hash(outer.data(), outer.size());\n\n    std::memcpy(md, final_hash.data(), final_hash.size());\n    if (md_len) *md_len = static_cast&lt;unsigned int&gt;(final_hash.size());\n\n    return md;\n}\n\n//  Base32 decoding table.\n/* clang-format off */\nconst int base32_reverse_map[128] =\n{\n    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15\n    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 31\n    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 47\n//      (49)   2    3    4    5    6    7  (56)\n    255, 255,  26,  27,  28,  29,  30,  31, 255, 255, 255, 255, 255, 255, 255, 255, // 63\n// (64)   A    B    C    D    E    F    G    H    I    J    K    L    M    N    O\n    255,   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14, // 79\n//   P    Q    R    S    T    U    V    W    X    Y    Z  (91)\n    15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25, 255, 255, 255, 255, 255, // 95\n// (96)   A    B    C    D    E    F    G    H    I    J    K    L    M    N    O  // lower case instead\n    255,   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14, // 111\n//   P    Q    R    S    T    U    V    W    X    Y    Z (123)                     // lower case instead\n    15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25, 255, 255, 255, 255, 255  // 127\n};\n/* clang-format on */\n\n//  Base32 decoding function.\nvector&lt;unsigned char&gt; base32_decode(const string&amp; base32)\n{\n    queue&lt;int&gt; q_rev;\n    for (const char&amp; c : base32)\n    {\n        if (c &lt;= 49 || (c &gt;= 56 &amp;&amp; c &lt;= 64) || (c &gt;= 91 &amp;&amp; c &lt;= 96) || c &gt;= 123)\n        { //    Illegal lower case will be treat as upper case thanks to base32_reverse_map.\n            if (c == '=') break;\n        }\n        else\n        {\n            q_rev.push(base32_reverse_map[(int)c]); //  Get integers of base32.\n        }\n    }\n    queue&lt;bool&gt; q_bin;\n    while (!q_rev.empty()) //   Transform the integers into bits and queue them up.\n    {\n        int now = q_rev.front(); // Get a integer from queue.\n        q_rev.pop();\n        stack&lt;bool&gt; sb;\n        for (int i = 0; i &lt; 5; i++) //  Push the bits of this integer into stack\n        {                           //  5 bits into 1 group\n            sb.push(now &amp; 1);       //  Push the last bit of this integer into stack.\n            now &gt;&gt;= 1;              //  Flush the last bit.\n        }\n        while (!sb.empty()) //  Flush the bits of this integer into binary queue.\n        {\n            q_bin.push(sb.top()); //    Pop bits from stack into binary queue.\n            sb.pop();\n        }\n    }\n    vector&lt;unsigned char&gt; uc;\n    while (!q_bin.empty()) //   Transform the bits into bytes.\n    {\n        int thehex = 0;\n        for (int i = 0; i &lt; 8; i++) //  Read a byte from the bits queue.\n        {\n            if (q_bin.empty())\n            {                     //    If the bits queue is empty now,\n                thehex &lt;&lt;= 8 - i; //    set 0 on the last bits of a byte.\n                break;\n            }\n            thehex &lt;&lt;= 1;                 //    Make the space for the new bit.\n            thehex |= (int)q_bin.front(); //    Push this bit into byte.\n            q_bin.pop();\n        }\n        uc.push_back((unsigned char)thehex);\n    }\n\n    if (uc.back() == 0) uc.pop_back();     //   TOTP may abandon last zero.\n    if (uc.size() % 2 != 0) uc.pop_back(); //   TOTP may abandon odd res.\n    return uc;\n}\n\n//  Get Unix Time.\ntime_t getUnixTime()\n{\n    return time(0);\n}\n\n//  Calculate the TOTP time signature.\ntime_t getTimeSign(int refreshPeriod, time_t specUnixTime)\n{\n    time_t outTime = specUnixTime / refreshPeriod;\n    return outTime;\n}\n\n//  Transform time_t aka &lt;long long&gt; to bytes.\nvector&lt;unsigned char&gt; llong2bytes(time_t tt)\n{\n    uint64_t value = static_cast&lt;uint64_t&gt;(tt);\n\n    vector&lt;unsigned char&gt; bytes(8);\n\n    for (int i = 7; i &gt;= 0; --i)\n    {\n        bytes[i] = static_cast&lt;unsigned char&gt;(value &amp; 0xFF);\n        value &gt;&gt;= 8;\n    }\n\n    return bytes;\n}\n\n//  Fast Exponentiation algorithm.\nunsigned fastpow(unsigned base, unsigned index)\n{\n    if (index == 0) return 1;\n    unsigned t = 1;\n    while (index != 0)\n    {\n        if (index &amp; 1) t *= base;\n        index &gt;&gt;= 1;\n        base *= base;\n    }\n    return t;\n}\n\n//  Calculate TOTP according to RFC standard.\nstd::string hash_to_TOTP(const unsigned char* hmac, unsigned int length, int digit)\n{\n    int offset = hmac[length - 1] &amp; 0x0F;\n\n    uint32_t bin_code = ((hmac[offset]     &amp; 0x7F) &lt;&lt; 24) |\n                        ((hmac[offset + 1] &amp; 0xFF) &lt;&lt; 16) |\n                        ((hmac[offset + 2] &amp; 0xFF) &lt;&lt; 8)  |\n                        ( hmac[offset + 3] &amp; 0xFF);\n\n    uint32_t mod = 1;\n    for (int i = 0; i &lt; digit; ++i) mod *= 10;\n\n    uint32_t otp = bin_code % mod;\n\n    std::ostringstream oss;\n    oss &lt;&lt; std::setw(digit) &lt;&lt; std::setfill('0') &lt;&lt; otp;\n    return oss.str();\n}\n\nstd::string TOTP::GenerateTOTP(const std::string&amp; secret)\n{\n    return GenerateTOTP(6, 30, HMAC_SHA1, secret, getUnixTime());\n}\n\nstd::string TOTP::GenerateTOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp; secret, int specTime)\n{\n    vector&lt;unsigned char&gt; h_msg = llong2bytes(getTimeSign(refreshSeconds, specTime)); //    Get current time signature and transform time_t aka &lt;long long&gt; to bytes.\n    vector&lt;unsigned char&gt; h_key = base32_decode(secret); // Transform base32 encoded secret key to decoded bytes.\n\n    unsigned char* out_hmac = new unsigned char[algorithm]; //  Perform openssl function to calculate HMAC-SHA1 hash.\n    unsigned int   out_hmac_length;\n    const JEVP_MD* emd;\n    emd = JEVP_sha1();\n    JHMAC(emd, h_key.data(), (int)h_key.size(), h_msg.data(), (int)h_msg.size(), out_hmac, &amp;out_hmac_length);\n\n    string fine = hash_to_TOTP(out_hmac, out_hmac_length, digit);\n    delete[] out_hmac;\n\n    return fine; // Calculate TOTP according to RFC standard.\n}\n\nstd::string TOTP::currentTOTP()\n{\n    return GenerateTOTP(digit, refreshSeconds, algorithm, secret, getUnixTime());\n}\n</code></pre><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76104&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/0w6m9-ym602","guid":"https://shkspr.mobi/blog/?p=76104","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/09/MFA.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792281600,"rid":"52ejt-3v382","summary":"Let's get this out of the way first - this is a terrible idea! I want my two-factor authentication codes to be easily accessible to me, so I've built a watch app which will display them. That's really convenient for me - but makes for a terrible security posture. Anyone who steals my watch will be able to see my one-time code.","tags":["/etc/","C++","HowTo","UnaWatch","Watch"],"title":"Una Watch TOTP - MFA on your wrist!","updated_at":1792592740,"url":"https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/10/randj.webp\" alt=\"Book cover.\" width=\"200\" class=\"alignleft\">\n\n<p>The thing about Romeo and Juliet is that it is a true story. It isn't literally true, but it <em>is</em> literary true. We see the same motif repeated throughout all cultures and all times. Caroline Lea (writing as Caroline Finch) asks an important question - what if every pair of star-cross'd lovers were reincarnations of the original Verona duo?</p>\n\n<p>The essence of tragedy is that the end is unavoidable. You've probably watched Romeo sup poison a hundred times. It is <em>inevitable</em> that Juliet will take her own life in response. Horrific! Yet, not only do you refuse to look away, you gleefully seek it out for another viewing. There's a reason why Tony and Maria cannot escape their destinies in West Side Story - something more than fate is pushing them onto a path that will accept no deviation. The only way through is, of course, death.</p>\n\n<p>The Thousand Deaths starts slowly - the first \"act\" felt like an excuse to retell the classic tale in a new setting. Nothing wrong with that, of course, and it is suffused with lovely writing - but it felt a bit \"so what?\"</p>\n\n<p>And then they die.</p>\n\n<p>As the acts progress and they get closer to the their thousandth death the pace picks up. The writing is absolutely gorgeous, packed with allusions and references for the careful reader (more on that later). The story is heartbreaking - as well you know - and the author has the perfect knack of keeping her characters on just the right side of histrionics.</p>\n\n<p>I'd sort of classify this as <em>almost</em> romantasy. There's an undercurrent of sci-fi and fantasy which can't be ignored - but you can also hear the stirring of the violins in the background. Our hero and heroine come together and are violently broken apart for our insatiable entertainment. Do we care about whether that's good for them? What does it feel like to be a Mercutio forever used as a pawn-sacrifice? Does Tybalt resent his forcible reincarnation into a perpetual baddie? Are we culpable for insisting that they strut upon an endless stage just so we can wipe tears from our eyes?</p>\n\n<p>The book builds to a fine crescendo. It is heartfelt and <em>utterly</em> tragic. The ending will, I suspect, divide people. I found it mostly satisfactory and completely in keeping with the rest of the story.</p>\n\n<p>One final thing - the end notes are <em>superb</em>. They go through all the references you might have missed, explain where certain phrases came from, and point the curious reader to the books which were an inspiration. Too many authors treat their literary allusions as a precious puzzle and jealously guard them. I'm delighted that Caroline Finch is generous enough to share her secrets with us.</p>\n\n<p>Many thanks to NetGalley for the review copy. The book will be released in 2027 - I strongly recommend putting it on your pre-order list.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76683&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/wkhx2-yfh44","guid":"https://shkspr.mobi/blog/?p=76683","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/10/randj.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791849600,"rid":"tsnbv-x9v44","summary":"The thing about Romeo and Juliet is that it is a true story. It isn't literally true, but it is literary true. We see the same motif repeated throughout all cultures and all times. Caroline Lea (writing as Caroline Finch) asks an important question - what if every pair of star-cross'd lovers were reincarnations of the original Verona duo?","tags":["/etc/","Book Review","NetGalley","Shakespeare"],"title":"Book Review: The Thousand Deaths of Romeo and Juliet - Caroline Finch","updated_at":1792592736,"url":"https://shkspr.mobi/blog/2026/10/book-review-the-thousand-deaths-of-romeo-and-juliet-caroline-finch/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<p>I don't know why hardware manufacturers find it so hard to write decent documentation. As I've said before <a href=\"https://shkspr.mobi/blog/2025/10/how-to-actually-test-your-readme/\">you need to <em>actually</em> test your readme</a>.  The Una Watch development instructions are scattered across a bunch of different files.</p>\n\n<p>So here's how to go from zero to your first \"Hello, World!\" Glance app with the minimum of fuss.</p>\n\n<p>This all assumes a fairly modern Debian / Ubuntu / Pop OS Linux.</p>\n\n<h2 id=\"prerequisites\"><a href=\"#prerequisites\">Prerequisites</a></h2>\n\n<p>This installs most of the tools you will need:</p>\n\n<pre><code class=\"language-bash\">sudo apt update\nsudo apt install -y git python3 python3-pip cmake build-essential\n</code></pre>\n\n<h2 id=\"stm32cubeclt\"><a href=\"#stm32cubeclt\">STM32CubeCLT</a></h2>\n\n<p>Visit <a href=\"https://www.st.com/en/development-tools/stm32cubeclt.html\">https://www.st.com/en/development-tools/stm32cubeclt.html</a> and download the Linux version of the tool. Inexplicably, you need to give your email address. I recommend giving a disposable one from <a href=\"https://10minutemail.com/\">https://10minutemail.com/</a></p>\n\n<p>After downloading, unzip the file.</p>\n\n<p>On the command line, <code>cd</code> into the directory containing the unzipped file.</p>\n\n<pre><code class=\"language-bash\">chmod +x stm32cubeclt_*.sh\nsudo ./stm32cubeclt_*.sh\n</code></pre>\n\n<p>You will need to agree to their licence. Just hit <code>y</code>.</p>\n\n<p>That installs everything in <code>/opt/st/stm32cubeclt_1.22.0/</code></p>\n\n<p>You will need to make that available to your PATH.</p>\n\n<pre><code class=\"language-bash\">export PATH=\"/opt/st/stm32cubeclt_1.22.0/GNU-tools-for-STM32/bin:$PATH\"\n</code></pre>\n\n<h2 id=\"clone-the-repo\"><a href=\"#clone-the-repo\">Clone the repo</a></h2>\n\n<p>You will need to copy <a href=\"https://github.com/UNAWatch/una-sdk/\">the Una SDK</a>. I keep my git repos in <code>~/git/</code> - you will need the location of the directory later.</p>\n\n<pre><code class=\"language-bash\">cd ~/git/\ngit clone git@github.com:UNAWatch/una-sdk.git\ncd una-sdk/\ngit submodule update --init ThirdParty/lvgl\n</code></pre>\n\n<p>Then update all the Python dependencies.</p>\n\n<pre><code class=\"language-bash\">export UNA_SDK=\"$PWD\"\npython3 -m pip install --upgrade pip\npython3 -m pip install -r \"$UNA_SDK/Utilities/Scripts/app_packer/requirements.txt\"\n</code></pre>\n\n<h2 id=\"check-it-has-all-worked\"><a href=\"#check-it-has-all-worked\">Check it has all worked</a></h2>\n\n<p>Paste this into your terminal. Check for any errors.</p>\n\n<pre><code class=\"language-bash\">which arm-none-eabi-gcc || true\nwhich cmake || true\nwhich make || true\nwhich python3 || true\npython3 -m pip --version\narm-none-eabi-gcc --version || true\ncmake --version || true\nmake --version || true\n</code></pre>\n\n<h2 id=\"build-the-demo-app\"><a href=\"#build-the-demo-app\">Build the demo app</a></h2>\n\n<p>If all of the above has worked, you should be able to build the demo app with:</p>\n\n<pre><code class=\"language-bash\">cd /tmp\nmkdir -p MyAlarm\ncp -r \"$UNA_SDK/Examples/Apps/Alarm/\"* MyAlarm\nmkdir -p MyAlarm/build\ncmake -G \"Unix Makefiles\" -S MyAlarm/Software/Apps/Alarm-CMake -B MyAlarm/build\ncmake --build MyAlarm/build\n</code></pre>\n\n<h2 id=\"make-it-permanent\"><a href=\"#make-it-permanent\">Make it permanent</a></h2>\n\n<p>Store the paths permanently. If you put cloned the Una SDK to somewhere other than <code>~/git/</code> you'll need to edit this next bit of code:</p>\n\n<pre><code class=\"language-bash\">echo 'export UNA_SDK=\"$HOME/git/una-sdk\"' &gt;&gt; ~/.bashrc\necho 'export PATH=\"/opt/st/stm32cubeclt_1.22.0/GNU-tools-for-STM32/bin:$PATH\"' &gt;&gt; ~/.bashrc\nsource ~/.bashrc\n</code></pre>\n\n<h2 id=\"build-a-glance-app\"><a href=\"#build-a-glance-app\">Build a Glance app</a></h2>\n\n<p>At the moment, the only way to build a GUI app is using the Windows only (Booooo!) TouchGFX<sup id=\"fnref:76095.windoze\"><a href=\"#fn:76095.windoze\" class=\"footnote-ref\" title=\"Just after I wrote this, the SDK was updated to allow LVGL which should work on Linux. There's no documentation for it yet that I could find.\" role=\"doc-noteref\">0</a></sup>. So, instead, we'll make a \"Hello World\" glance. All it will do is draw some text.</p>\n\n<p>The <code>GlanceFloors</code> demo app is the simplest one to copy.</p>\n\n<pre><code class=\"language-bash\">cd $UNA_SDK/Examples/Apps/\ncp -r  GlanceFloors/ GlanceHello\n</code></pre>\n\n<p>Use an IDE or a text editor to open up the new GlanceHello directory.</p>\n\n<ul>\n<li>Rename the directory <code>GlanceHello/Software/App/GlanceFloors-CMake/</code> to <code>GlanceHello/Software/App/GlanceHello-CMake/</code></li>\n<li>Edit the file <code>GlanceHello/Software/App/GlanceFloors-CMake/CMakeLists.txt</code> - specifically the <code># App configuration</code> section.</li>\n<li>Change the <code>APP_NAME</code> to <code>GlanceHello</code></li>\n<li>Change the <code>APP_USER_NAME</code> to <code>Hello</code></li>\n</ul>\n\n<p>For a demo, you can leave the <code>DEV_ID</code> and <code>APP_ID</code> as their default values.</p>\n\n<h2 id=\"minimum-viable-app\"><a href=\"#minimum-viable-app\">Minimum viable app</a></h2>\n\n<p>You will need to edit two more files.</p>\n\n<p>In <code>GlanceHello/Software/Libs/Header/Service.hpp</code> remove the following lines:</p>\n\n<pre><code class=\"language-CPP\">SDK::Sensor::Connection mSensorFloors;\nuint32_t                mFloorsValue;\nbool                    mDataReceived;\n</code></pre>\n\n<p>Then save the file.</p>\n\n<p>Edit <code>GlanceHello/Software/Libs/Source/Service.cpp</code> and replace its entire contents with this:</p>\n\n<pre><code class=\"language-CPP\">#include \"Service.hpp\"\n#include \"SDK/Messages/CommandMessages.hpp\"\n#include \"SDK/Messages/MessageGuard.hpp\"\n\nService::Service(SDK::Kernel &amp;kernel)\n    : mKernel(kernel)\n    , mGlanceUI()\n    , mGlanceTitle()\n    , mGlanceValue()\n{}\n\nService::~Service(){}\n\nvoid Service::run()\n{\n    while (true) {\n        SDK::MessageBase *msg;\n        if(!mKernel.comm.getMessage(msg)) {\n            continue;\n        }\n\n        switch (msg-&gt;getType()) {\n\n            case SDK::MessageType::EVENT_GLANCE_START:\n                if (configGui()) {\n                    createGuiControls();\n                } else {\n                    mKernel.comm.releaseMessage(msg);\n                    mKernel.sys.exit(0);\n                }\n                break;\n\n            case SDK::MessageType::COMMAND_APP_STOP:\n            case SDK::MessageType::EVENT_GLANCE_STOP:\n                mKernel.comm.releaseMessage(msg);\n                return;\n                break;\n\n            case SDK::MessageType::EVENT_GLANCE_TICK:\n                onGlanceTick();\n                break;\n\n            default:\n                break;\n        }\n\n        mKernel.comm.releaseMessage(msg);\n    }\n}\n\nvoid Service::onGlanceTick()\n{\n    if (mGlanceUI.isInvalid()) {\n        if (auto upd = SDK::make_msg&lt;SDK::Message::RequestGlanceUpdate&gt;(mKernel)) {\n            upd-&gt;name           = APP_NAME;\n            upd-&gt;controls       = mGlanceUI.data();\n            upd-&gt;controlsNumber = static_cast&lt;uint32_t&gt;(mGlanceUI.size());\n            upd.send(100);\n        }\n\n        mGlanceUI.setValid();\n}\n}\n\nbool Service::configGui()\n{\n    bool status = false;\n    if (auto gc = SDK::make_msg&lt;SDK::Message::RequestGlanceConfig&gt;(mKernel)) {\n        if (gc.send(100) &amp;&amp; gc.ok()) {\n            if (gc-&gt;maxControls &gt;= 3) {\n                mGlanceUI.setWidth(gc-&gt;width);\n                mGlanceUI.setHeight(gc-&gt;height);\n                status = true;\n            }\n        }\n    }\n    return status;\n}\n\nvoid Service::createGuiControls()\n{\n    mGlanceTitle = mGlanceUI.createText();\n    mGlanceTitle.pos({ kTitleX, kTitleY }, { kTitleW, kTitleH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_TEAL)\n        .setText(\"Hello, World!\")\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n    mGlanceValue = mGlanceUI.createText();\n    mGlanceValue.pos({ 0, kValueY }, { 200, kValueH })\n        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)\n        .color(GlanceColor_t::GLANCE_COLOR_WHITE)\n        .setText(\"I know C++\")\n        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);\n}\n</code></pre>\n\n<p>Save the file.</p>\n\n<h2 id=\"compile-it\"><a href=\"#compile-it\">Compile it</a></h2>\n\n<p>On the command line</p>\n\n<pre><code class=\"language-bash\">cd $UNA_SDK/Examples/Apps/GlanceHello/Software/App/GlanceHello-CMake\ncmake -G \"Unix Makefiles\" -S . -B build\ncmake --build build\n</code></pre>\n\n<p>It should finish with a success message of <code>[100%] Built target GlanceHelloApp</code></p>\n\n<h2 id=\"copying-it-to-your-watch\"><a href=\"#copying-it-to-your-watch\">Copying it to your watch</a></h2>\n\n<ul>\n<li>Plug your watch into your computer with a USB-C cable.</li>\n<li>On your computer, open the watch's filesystem.</li>\n<li>Open the <code>Apps</code> directory.</li>\n<li>Create a new directory.</li>\n<li>Name the new directory with the string of characters from the <code>APP_ID</code> which is set in <code>CMakeLists.txt</code></li>\n<li>The app is stored in <code>GlanceHello/Output/</code> and will be called <code>Hello_1.5.0-rc5.uapp</code> or similar.</li>\n<li>Copy the <code>.uapp</code> file from your computer to the directory you just created on the watch.</li>\n<li>Eject the watch.</li>\n<li>Unplug the cable.</li>\n<li>Restart the watch by pressing the \"Up\" button until you get to \"Power Off\", then press \"Select\", then press \"Select\" again.</li>\n<li>Turn the watch on by holding down \"Select\".</li>\n<li>Once the watch has fully restarted, press the \"Down\" button until you see the \"Hello\" glance.</li>\n</ul>\n\n<p>If it has worked, you should see something like this:</p>\n\n<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/09/cpp.webp\" alt=\"A watch displaying &quot;Hello World&quot; and &quot;I know C++&quot;. The latter if off centre because I don't know C++.\" width=\"1926\" height=\"2048\" class=\"aligncenter\">\n\n<p>Hurrah! Treat yourself to a beer \ud83c\udf7b</p>\n\n<div id=\"footnotes\" role=\"doc-endnotes\">\n<hr aria-label=\"Footnotes\">\n<ol start=\"0\">\n\n<li id=\"fn:76095.windoze\">\n<p>Just after I wrote this, the SDK was updated to allow <a href=\"https://lvgl.io/\">LVGL</a> which should work on Linux. There's no documentation for it yet that I could find.&nbsp;<a href=\"#fnref:76095.windoze\" class=\"footnote-backref\" role=\"doc-backlink\">\u21a9\ufe0e</a></p>\n</li>\n\n</ol>\n</div><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76095&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/0std8-hz104","guid":"https://shkspr.mobi/blog/?p=76095","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/09/cpp.webp","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1791244800,"rid":"vrwx5-w7v30","summary":"I don't know why hardware manufacturers find it so hard to write decent documentation. As I've said before you need to actually test your readme. The Una Watch development instructions are scattered across a bunch of different files. So here's how to go from zero to your first \"Hello, World!\" Glance app with the minimum of fuss. This all assumes a fairly modern Debian / Ubuntu / Pop OS Linux.","tags":["/etc/","C++","HowTo","Programming","UnaWatch"],"title":"Una Watch - SDK and Writing Your First App","updated_at":1792592734,"url":"https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/","version":"v1"},{"authors":[{"contributor_roles":[],"family":"Eden","given":"Terence","url":"https://orcid.org/0000-0002-9265-9069"}],"blog":{"authors":[{"name":"Terence Eden","url":"https://orcid.org/0000-0002-9265-9069"}],"community_id":"61ce553a-bafd-4aba-a952-d3bab5e85bcc","created":1788652800,"current_feed_url":null,"description":"Regular nonsense about tech and its effects \ud83d\ude43","doi":"https://doi.org/10.59350/shkspr","favicon":"https://rogue-scholar.org/api/communities/61ce553a-bafd-4aba-a952-d3bab5e85bcc/logo","feed_format":"application/atom+xml","feed_url":"https://shkspr.mobi/blog/feed/DOI","filter":"category:-1982","generator":"WordPress","home_page_url":"https://shkspr.mobi/blog","issn":"2753-1570","language":"eng","license":"https://creativecommons.org/licenses/by/4.0/legalcode","prefix":"10.59350","relative_url":null,"secure":true,"slug":"shkspr","status":"active","subfield":"1712","title":"Terence Eden's Blog","updated":1792570548,"use_api":true},"blog_name":"Terence Eden's Blog","blog_slug":"shkspr","content_html":"<img src=\"https://shkspr.mobi/blog/wp-content/uploads/2026/10/are-you-really-the-doctor.avif\" alt=\"Book cover featuring a pair of crossed plasters in a dark brown colour.\" width=\"200\" class=\"alignleft\">\n\n<p>A few months ago, I went to see <a href=\"https://www.bbc.co.uk/programmes/m002vbxx\">Dr Hutchinson record his stand-up comedy special for the BBC</a> - it's about his experiences working in the NHS as a Black doctor. As the title of the book implies, some of the public can be a mite suspicious that anyone with even a hint of melanin can rise beyond being a porter or, at a push, a nurse.</p>\n\n<p>Much like <a href=\"https://shkspr.mobi/blog/2019/01/book-review-this-is-going-to-hurt/\">Adam Kay's \"This Is Going To Hurt\"</a>, this is part biography, part rant about the NHS, and part humorous look at what it takes to be a modern doctor. Hutchinson's book has rather less gallows humour and rather more racism.</p>\n\n<p>The problems of racial inequality in the NHS aren't just focussed on patients; they're also reflected in the way staff are treated. Rather than just recounting all the abuse he's been subject to, the book takes a good look at the actual statistics. Of course, this makes for slightly grim reading at times, so the frequent injections of humour are most welcome.</p>\n\n<p>The UK quite often seems in the shadow of larger cultures. As the book points out:</p>\n\n<blockquote><p>This same issue is understudied in the UK \u2013 to the point where the absence of research is glaring. While we are clearly not just a little America, we aren't so different that we can be confident we're not replicating the same here.</p></blockquote>\n\n<p>Because of that, at times the book does feel a little bit like the \"<a href=\"https://knowyourmeme.com/memes/we-have-food-at-home\">we have [racism] at home</a>\" meme.</p>\n\n<p>There's an almost relentlessly cheery irreverence to some of the issues - but what else can you do? Sometimes it takes a good dose of comedy to cut through the bleakness. There's a slight streak of ageism within the book - especially when it comes to older doctors trying to use new technology. Although, to be fair, that was also my experience when working at NHSX. There are some people within the system who are desperate for it to remain preserved in aspic as even minor improvements are a threat to their position in the hierarchy.</p>\n\n<p>A minor grumble, the problem I have with books like this - and with <a href=\"https://shkspr.mobi/blog/2019/05/review-the-secret-barrister/\">The Secret Barrister</a> - is that a lot of the moaning about how hard it is to become a professional doctor / lawyer just generates a giant shrug from me. Every piece of medical media in the last 50 years talks about how bone-crunchingly hard it is. All I can think of is \"none of this should be coming as a surprise to you!\" Should it be easier (and less sleep depriving) to be a doctor? Sure! But given it isn't, perhaps dial down on the clich\u00e9s of how uniquely awful your <em>chosen</em> career is.</p>\n\n<p>The book isn't just a list of complaints, of course, everything is backed up with statistics (although unreferenced). Thankfully, the book ends with some practical suggestions of how the NHS can improve. It would certainly be interesting to read a version of this from 2046 to see how much has changed - given the improvements already bearing fruit, I'd hope that the UK health service would become much less racist over time.</p><img src=\"https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76754&HTTP_REFERER=DOI\" alt width=1 height=1 loading=eager>","doi":"https://doi.org/10.59350/bc4fr-qcx14","guid":"https://shkspr.mobi/blog/?p=76754","image":"https://shkspr.mobi/blog/wp-content/uploads/2026/10/are-you-really-the-doctor.avif","language":"en","license":"https://creativecommons.org/licenses/by/4.0/legalcode","published_at":1792195200,"rid":"mnchs-wqn69","summary":"A few months ago, I went to see Dr Hutchinson record his stand-up comedy special for the BBC - it's about his experiences working in the NHS as a Black doctor. As the title of the book implies, some of the public can be a mite suspicious that anyone with even a hint of melanin can rise beyond being a porter or, at a push, a nurse.","tags":["/etc/","Book Review","NHS","Racism"],"title":"Book Review: Are You *Really* The Doctor? by Matthew Hutchinson","updated_at":1792592730,"url":"https://shkspr.mobi/blog/2026/10/book-review-are-you-really-the-doctor-by-matthew-hutchinson/","version":"v1"}],"out_of":57841,"page":1,"per_page":10,"total-results":57841}
