<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/rss-style.xsl" type="text/xsl"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	     xmlns:dc="http://purl.org/dc/elements/1.1/"
	   xmlns:atom="http://www.w3.org/2005/Atom"
	     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	  xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>
<channel>
	<title>UnaWatch &#8211; Terence Eden’s Blog</title>
	<atom:link href="https://shkspr.mobi/blog/tag/unawatch/feed/" rel="self" type="application/rss+xml" />
	<link>https://shkspr.mobi/blog</link>
	<description>Regular nonsense about tech and its effects 🙃</description>
	<lastBuildDate>Sat, 17 Oct 2026 10:10:32 +0000</lastBuildDate>
	<language>en-GB</language>
	<copyright>© Terence Eden. 🄯 CC BY-SA. See https://shkspr.mobi/blog/copyright-and-copyleft/</copyright>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://shkspr.mobi/blog/wp-content/uploads/2023/07/cropped-avatar-32x32.jpeg</url>
	<title>UnaWatch &#8211; Terence Eden’s Blog</title>
	<link>https://shkspr.mobi/blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title><![CDATA[Una Watch TOTP - MFA on your wrist!]]></title>
		<link>https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/</link>
					<comments>https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/#comments</comments>
				<dc:creator><![CDATA[Terence Eden]]></dc:creator>
		<pubDate>Sun, 18 Oct 2026 11:34:08 +0000</pubDate>
		<category><![CDATA[c++]]></category><category><![CDATA[HowTo]]></category><category><![CDATA[UnaWatch]]></category><category><![CDATA[watch]]></category>		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=76104</guid>

					<description><![CDATA[Let&#039;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&#039;ve built a watch app which will display them. That&#039;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&#039;re clever, they can decompile the app and find the secret…]]></description>
										<content:encoded>&lt;p&gt;Let's get this out of the way first - this is a &lt;em&gt;terrible&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;!-- 
👋👋👋👋👋👋👋👋👋
Welcome to Comment Club! If this is your first time - hello! This content is only available to sophisticated nerds who read my HTML comments.

You 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.

Anyway, the three rules of Comment Club are…

1. You must not tell anyone about Comment Club - let them find out about it themselves.
2. 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.
3. You should drop an email to anyone whose comments you've read. Start by sending me a friendly message at comment.club@shkspr.mobi

Keep your eyes peeled for more comments in my other blog posts 😃

TTFN.
--&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2 id="what-it-looks-like"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/#what-it-looks-like"&gt;What It Looks Like&lt;/a&gt;&lt;/h2&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/MFA.webp" alt="My wristwatch showing &amp;quot;HSBC MFA Code 751646&amp;quot;. Don't worry, this isn't my real code. OR IS IT?!?!" width="1024" height="576" class="aligncenter"&gt;

&lt;h2 id="prior-art"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/#prior-art"&gt;Prior Art&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;I'm sure many other people have done this. Indeed, three years ago I built &lt;a href="https://shkspr.mobi/blog/2023/07/an-eink-wrist-mounted-totp-generator/"&gt;An eInk, Wrist-Mounted, TOTP Generator&lt;/a&gt;.&lt;/p&gt;

&lt;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"&gt;

&lt;h2 id="the-service"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/#the-service"&gt;The Service&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;This is in my &lt;code&gt;Service.cpp&lt;/code&gt; - 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;You will need to set your own secret in here.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-cpp"&gt;//  Standard includes.
#include "Service.hpp"
#include "SDK/Messages/CommandMessages.hpp"
#include "SDK/Messages/MessageGuard.hpp"

//  Needed for PureTOTP.
#include &amp;lt;iostream&amp;gt;
#include "TOTP.h"

#include &amp;lt;string&amp;gt;
#include &amp;lt;sstream&amp;gt;

//  What this Glance can do.
Service::Service(SDK::Kernel &amp;amp;kernel)
    : mKernel(kernel)
    , mGlanceUI()
    , mGlanceTitle()
    , mGlanceValue()
{}

void Service::createGuiControls()
{
    //  Set the main title in the Glance.
    mGlanceTitle = mGlanceUI.createText();
    mGlanceTitle.pos({ kTitleX, kTitleY }, { kTitleW, kTitleH })
        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)
        .color(GlanceColor_t::GLANCE_COLOR_TEAL)
        .setText("HSBC MFA Code")
        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);

    //  Set the default text for the value.
    mGlanceValue = mGlanceUI.createText();
    mGlanceValue.pos({ 20, kValueY }, { 200, kValueH })
        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)
        .color(GlanceColor_t::GLANCE_COLOR_YELLOW_DARK)
        .setText( "Waiting for update" )
        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);
}

void Service::glanceUpdate()
{
    //  Set the Base32 encoded secret (all uppercase).
    std::string secret = "QWERTYUIOP";
    //  Generate the code based on the Unix timestamp right now.
    const std::time_t t = std::time(0);
    std::string code = TOTP::GenerateTOTP( 6, 30, TOTP::HMAC_SHA1, secret, t );

    std::ostringstream ss;
    ss &amp;lt;&amp;lt; t;

    //  Convert the string to char* and display it as the value.
    mGlanceValue.print( code.c_str() ); 

    //  Show the Unix time to check the timezone is correct.
    // mGlanceTitle.print( ss.str().c_str() );
}

//  No service.
Service::~Service(){}

//  When this glance is opened.
void Service::run()
{
    while (true) {
        SDK::MessageBase *msg;
        if(!mKernel.comm.getMessage(msg)) {
            continue;
        }

        switch (msg-&amp;gt;getType()) {
            //  The Glance has started.
            case SDK::MessageType::EVENT_GLANCE_START:
                if (configGui()) {
                    createGuiControls();
                } else {
                    mKernel.comm.releaseMessage(msg);
                    mKernel.sys.exit(0);
                }
                break;
            //  The Glance has been stopped.
            case SDK::MessageType::COMMAND_APP_STOP:
            case SDK::MessageType::EVENT_GLANCE_STOP:
                mKernel.comm.releaseMessage(msg);
                return;
                break;

            //  Every second, perform an action.
            case SDK::MessageType::EVENT_GLANCE_TICK:
                onGlanceTick();
                break;

            default:
                break;
        }

        mKernel.comm.releaseMessage(msg);
    }
}

void Service::onGlanceTick()
{
    if (mGlanceUI.isInvalid()) {
        if (auto upd = SDK::make_msg&amp;lt;SDK::Message::RequestGlanceUpdate&amp;gt;(mKernel)) {
            upd-&amp;gt;name           = APP_NAME;
            upd-&amp;gt;controls       = mGlanceUI.data();
            upd-&amp;gt;controlsNumber = static_cast&amp;lt;uint32_t&amp;gt;(mGlanceUI.size());
            upd.send(100);
        }

        mGlanceUI.setValid();
    }
    glanceUpdate();
}

bool Service::configGui()
{
    bool status = false;
    if (auto gc = SDK::make_msg&amp;lt;SDK::Message::RequestGlanceConfig&amp;gt;(mKernel)) {
        if (gc.send(100) &amp;amp;&amp;amp; gc.ok()) {
            if (gc-&amp;gt;maxControls &amp;gt;= 3) {
                mGlanceUI.setWidth(gc-&amp;gt;width);
                mGlanceUI.setHeight(gc-&amp;gt;height);
                status = true;
            }
        }
    }
    return status;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="the-library"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/#the-library"&gt;The Library&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;There are dozens of C++ libraries for RFC 6238 generation. I ended up using &lt;a href="https://github.com/Einztein/PureTOTP"&gt;PureTOTP&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;I've tidied up the code and comments where I can. The original was MIT Licenced - so this is as well.&lt;/p&gt;

&lt;p&gt;There are two parts to this. First, the header&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-_"&gt;// TOTP.h
//
#pragma once
#include &amp;lt;string&amp;gt;
class TOTP
{
public:
    const static int HMAC_SHA1 = 20;    //  Length of HMAC.

    static std::string GenerateTOTP(const std::string&amp;amp; secret);
    static std::string GenerateTOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp;amp; secret, int specTime);

    TOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp;amp; secret): digit(digit), refreshSeconds(refreshSeconds), algorithm(algorithm), secret(secret) {}
    TOTP(const std::string&amp;amp; secret): digit(6), refreshSeconds(30), algorithm(HMAC_SHA1), secret(secret) {}

    std::string currentTOTP();

private:
    int digit, refreshSeconds, algorithm;

    std::string secret;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next is the standalone library which does the TOTP generation, &lt;code&gt;TOTP.cpp&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-cpp"&gt;// TOTP.cpp
//
#include "TOTP.h"
#include &amp;lt;vector&amp;gt;
#include &amp;lt;stack&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;sstream&amp;gt;
#include &amp;lt;iomanip&amp;gt;
#include &amp;lt;cstdint&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;array&amp;gt;
#include &amp;lt;stdexcept&amp;gt;
#include &amp;lt;algorithm&amp;gt;
using namespace std;

struct JEVP_MD
{
    enum Kind {
        SHA1
    };

    Kind kind;
    int  md_size;
    int  block_size;
};

static inline uint32_t rotl32(uint32_t x, uint32_t n) { return (x &amp;lt;&amp;lt; n) | (x &amp;gt;&amp;gt; (32 - n)); }
static inline uint32_t rotr32(uint32_t x, uint32_t n) { return (x &amp;gt;&amp;gt; n) | (x &amp;lt;&amp;lt; (32 - n)); }
static inline uint64_t rotr64(uint64_t x, uint64_t n) { return (x &amp;gt;&amp;gt; n) | (x &amp;lt;&amp;lt; (64 - n)); }

static uint32_t load_be32(const unsigned char* p)
{
    return (uint32_t(p[0]) &amp;lt;&amp;lt; 24) |
           (uint32_t(p[1]) &amp;lt;&amp;lt; 16) |
           (uint32_t(p[2]) &amp;lt;&amp;lt;  8) |
            uint32_t(p[3]);
}

static void store_be32(uint32_t v, unsigned char* p)
{
    p[0] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 24) &amp;amp; 0xFF);
    p[1] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 16) &amp;amp; 0xFF);
    p[2] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt;  8) &amp;amp; 0xFF);
    p[3] = static_cast&amp;lt;unsigned char&amp;gt;( v        &amp;amp; 0xFF);
}

static void store_be64(uint64_t v, unsigned char* p)
{
    p[0] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 56) &amp;amp; 0xFF);
    p[1] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 48) &amp;amp; 0xFF);
    p[2] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 40) &amp;amp; 0xFF);
    p[3] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 32) &amp;amp; 0xFF);
    p[4] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 24) &amp;amp; 0xFF);
    p[5] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt; 16) &amp;amp; 0xFF);
    p[6] = static_cast&amp;lt;unsigned char&amp;gt;((v &amp;gt;&amp;gt;  8) &amp;amp; 0xFF);
    p[7] = static_cast&amp;lt;unsigned char&amp;gt;( v        &amp;amp; 0xFF);
}

static vector&amp;lt;unsigned char&amp;gt; sha1_hash(const unsigned char* data, size_t len)
{
    vector&amp;lt;unsigned char&amp;gt; msg(data, data + len);
    uint64_t              bit_len = static_cast&amp;lt;uint64_t&amp;gt;(len) * 8;

    msg.push_back(0x80);
    while ((msg.size() % 64) != 56) msg.push_back(0x00);

    unsigned char lenbuf[8];
    store_be64(bit_len, lenbuf);
    msg.insert(msg.end(), lenbuf, lenbuf + 8);

    uint32_t h0 = 0x67452301;
    uint32_t h1 = 0xEFCDAB89;
    uint32_t h2 = 0x98BADCFE;
    uint32_t h3 = 0x10325476;
    uint32_t h4 = 0xC3D2E1F0;

    for (size_t chunk = 0; chunk &amp;lt; msg.size(); chunk += 64)
    {
        uint32_t w[80];
        for (int i = 0; i &amp;lt; 16; ++i)
            w[i] = load_be32(&amp;amp;msg[chunk + i * 4]);
        for (int i = 16; i &amp;lt; 80; ++i)
            w[i] = rotl32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);

        uint32_t a = h0, b = h1, c = h2, d = h3, e = h4;

        for (int i = 0; i &amp;lt; 80; ++i)
        {
            uint32_t f, k;
            if (i &amp;lt; 20)
            {
                f = (b &amp;amp; c) | ((~b) &amp;amp; d);
                k = 0x5A827999;
            }
            else if (i &amp;lt; 40)
            {
                f = b ^ c ^ d;
                k = 0x6ED9EBA1;
            }
            else if (i &amp;lt; 60)
            {
                f = (b &amp;amp; c) | (b &amp;amp; d) | (c &amp;amp; d);
                k = 0x8F1BBCDC;
            }
            else
            {
                f = b ^ c ^ d;
                k = 0xCA62C1D6;
            }

            uint32_t temp = rotl32(a, 5) + f + e + k + w[i];

            e = d;
            d = c;
            c = rotl32(b, 30);
            b = a;
            a = temp;
        }

        h0 += a;
        h1 += b;
        h2 += c;
        h3 += d;
        h4 += e;
    }

    vector&amp;lt;unsigned char&amp;gt; out(20);
    store_be32(h0, &amp;amp;out[ 0]);
    store_be32(h1, &amp;amp;out[ 4]);
    store_be32(h2, &amp;amp;out[ 8]);
    store_be32(h3, &amp;amp;out[12]);
    store_be32(h4, &amp;amp;out[16]);
    return out;
}

static const JEVP_MD g_sha1   = {JEVP_MD::SHA1, 20, 64};

const JEVP_MD* JEVP_sha1() { return &amp;amp;g_sha1; }

unsigned 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)
{
    if (!JEVP_MD || !key || key_len &amp;lt; 0 || !data)
        return nullptr;

    static thread_local unsigned char static_md[64];

    if (!md)
        md = static_md;

    vector&amp;lt;unsigned char&amp;gt; key_block(JEVP_MD-&amp;gt;block_size, 0);
    const unsigned char*  key_bytes = static_cast&amp;lt;const unsigned char*&amp;gt;(key);

    vector&amp;lt;unsigned char&amp;gt; real_key;
    if (key_len &amp;gt; JEVP_MD-&amp;gt;block_size)
    {
        real_key = sha1_hash(key_bytes, static_cast&amp;lt;size_t&amp;gt;(key_len));
        std::copy(real_key.begin(), real_key.end(), key_block.begin());
    }
    else
    {
        std::copy(key_bytes, key_bytes + key_len, key_block.begin());
    }

    vector&amp;lt;unsigned char&amp;gt; ipad(JEVP_MD-&amp;gt;block_size);
    vector&amp;lt;unsigned char&amp;gt; opad(JEVP_MD-&amp;gt;block_size);
    for (int i = 0; i &amp;lt; JEVP_MD-&amp;gt;block_size; ++i)
    {
        ipad[i] = static_cast&amp;lt;unsigned char&amp;gt;(key_block[i] ^ 0x36);
        opad[i] = static_cast&amp;lt;unsigned char&amp;gt;(key_block[i] ^ 0x5C);
    }

    vector&amp;lt;unsigned char&amp;gt; inner;
    inner.reserve(JEVP_MD-&amp;gt;block_size + data_len);
    inner.insert(inner.end(), ipad.begin(), ipad.end());
    inner.insert(inner.end(), data, data + data_len);

    vector&amp;lt;unsigned char&amp;gt; inner_hash = sha1_hash(inner.data(), inner.size());

    vector&amp;lt;unsigned char&amp;gt; outer;
    outer.reserve(JEVP_MD-&amp;gt;block_size + inner_hash.size());
    outer.insert(outer.end(), opad.begin(), opad.end());
    outer.insert(outer.end(), inner_hash.begin(), inner_hash.end());

    vector&amp;lt;unsigned char&amp;gt; final_hash = sha1_hash(outer.data(), outer.size());

    std::memcpy(md, final_hash.data(), final_hash.size());
    if (md_len) *md_len = static_cast&amp;lt;unsigned int&amp;gt;(final_hash.size());

    return md;
}

//  Base32 decoding table.
/* clang-format off */
const int base32_reverse_map[128] =
{
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 31
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 47
//      (49)   2    3    4    5    6    7  (56)
    255, 255,  26,  27,  28,  29,  30,  31, 255, 255, 255, 255, 255, 255, 255, 255, // 63
// (64)   A    B    C    D    E    F    G    H    I    J    K    L    M    N    O
    255,   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14, // 79
//   P    Q    R    S    T    U    V    W    X    Y    Z  (91)
    15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25, 255, 255, 255, 255, 255, // 95
// (96)   A    B    C    D    E    F    G    H    I    J    K    L    M    N    O  // lower case instead
    255,   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14, // 111
//   P    Q    R    S    T    U    V    W    X    Y    Z (123)                     // lower case instead
    15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25, 255, 255, 255, 255, 255  // 127
};
/* clang-format on */

//  Base32 decoding function.
vector&amp;lt;unsigned char&amp;gt; base32_decode(const string&amp;amp; base32)
{
    queue&amp;lt;int&amp;gt; q_rev;
    for (const char&amp;amp; c : base32)
    {
        if (c &amp;lt;= 49 || (c &amp;gt;= 56 &amp;amp;&amp;amp; c &amp;lt;= 64) || (c &amp;gt;= 91 &amp;amp;&amp;amp; c &amp;lt;= 96) || c &amp;gt;= 123)
        { //    Illegal lower case will be treat as upper case thanks to base32_reverse_map.
            if (c == '=') break;
        }
        else
        {
            q_rev.push(base32_reverse_map[(int)c]); //  Get integers of base32.
        }
    }
    queue&amp;lt;bool&amp;gt; q_bin;
    while (!q_rev.empty()) //   Transform the integers into bits and queue them up.
    {
        int now = q_rev.front(); // Get a integer from queue.
        q_rev.pop();
        stack&amp;lt;bool&amp;gt; sb;
        for (int i = 0; i &amp;lt; 5; i++) //  Push the bits of this integer into stack
        {                           //  5 bits into 1 group
            sb.push(now &amp;amp; 1);       //  Push the last bit of this integer into stack.
            now &amp;gt;&amp;gt;= 1;              //  Flush the last bit.
        }
        while (!sb.empty()) //  Flush the bits of this integer into binary queue.
        {
            q_bin.push(sb.top()); //    Pop bits from stack into binary queue.
            sb.pop();
        }
    }
    vector&amp;lt;unsigned char&amp;gt; uc;
    while (!q_bin.empty()) //   Transform the bits into bytes.
    {
        int thehex = 0;
        for (int i = 0; i &amp;lt; 8; i++) //  Read a byte from the bits queue.
        {
            if (q_bin.empty())
            {                     //    If the bits queue is empty now,
                thehex &amp;lt;&amp;lt;= 8 - i; //    set 0 on the last bits of a byte.
                break;
            }
            thehex &amp;lt;&amp;lt;= 1;                 //    Make the space for the new bit.
            thehex |= (int)q_bin.front(); //    Push this bit into byte.
            q_bin.pop();
        }
        uc.push_back((unsigned char)thehex);
    }

    if (uc.back() == 0) uc.pop_back();     //   TOTP may abandon last zero.
    if (uc.size() % 2 != 0) uc.pop_back(); //   TOTP may abandon odd res.
    return uc;
}

//  Get Unix Time.
time_t getUnixTime()
{
    return time(0);
}

//  Calculate the TOTP time signature.
time_t getTimeSign(int refreshPeriod, time_t specUnixTime)
{
    time_t outTime = specUnixTime / refreshPeriod;
    return outTime;
}

//  Transform time_t aka &amp;lt;long long&amp;gt; to bytes.
vector&amp;lt;unsigned char&amp;gt; llong2bytes(time_t tt)
{
    uint64_t value = static_cast&amp;lt;uint64_t&amp;gt;(tt);

    vector&amp;lt;unsigned char&amp;gt; bytes(8);

    for (int i = 7; i &amp;gt;= 0; --i)
    {
        bytes[i] = static_cast&amp;lt;unsigned char&amp;gt;(value &amp;amp; 0xFF);
        value &amp;gt;&amp;gt;= 8;
    }

    return bytes;
}

//  Fast Exponentiation algorithm.
unsigned fastpow(unsigned base, unsigned index)
{
    if (index == 0) return 1;
    unsigned t = 1;
    while (index != 0)
    {
        if (index &amp;amp; 1) t *= base;
        index &amp;gt;&amp;gt;= 1;
        base *= base;
    }
    return t;
}

//  Calculate TOTP according to RFC standard.
std::string hash_to_TOTP(const unsigned char* hmac, unsigned int length, int digit)
{
    int offset = hmac[length - 1] &amp;amp; 0x0F;

    uint32_t bin_code = ((hmac[offset]     &amp;amp; 0x7F) &amp;lt;&amp;lt; 24) |
                        ((hmac[offset + 1] &amp;amp; 0xFF) &amp;lt;&amp;lt; 16) |
                        ((hmac[offset + 2] &amp;amp; 0xFF) &amp;lt;&amp;lt; 8)  |
                        ( hmac[offset + 3] &amp;amp; 0xFF);

    uint32_t mod = 1;
    for (int i = 0; i &amp;lt; digit; ++i) mod *= 10;

    uint32_t otp = bin_code % mod;

    std::ostringstream oss;
    oss &amp;lt;&amp;lt; std::setw(digit) &amp;lt;&amp;lt; std::setfill('0') &amp;lt;&amp;lt; otp;
    return oss.str();
}

std::string TOTP::GenerateTOTP(const std::string&amp;amp; secret)
{
    return GenerateTOTP(6, 30, HMAC_SHA1, secret, getUnixTime());
}

std::string TOTP::GenerateTOTP(int digit, int refreshSeconds, int algorithm, const std::string&amp;amp; secret, int specTime)
{
    vector&amp;lt;unsigned char&amp;gt; h_msg = llong2bytes(getTimeSign(refreshSeconds, specTime)); //    Get current time signature and transform time_t aka &amp;lt;long long&amp;gt; to bytes.
    vector&amp;lt;unsigned char&amp;gt; h_key = base32_decode(secret); // Transform base32 encoded secret key to decoded bytes.

    unsigned char* out_hmac = new unsigned char[algorithm]; //  Perform openssl function to calculate HMAC-SHA1 hash.
    unsigned int   out_hmac_length;
    const JEVP_MD* emd;
    emd = JEVP_sha1();
    JHMAC(emd, h_key.data(), (int)h_key.size(), h_msg.data(), (int)h_msg.size(), out_hmac, &amp;amp;out_hmac_length);

    string fine = hash_to_TOTP(out_hmac, out_hmac_length, digit);
    delete[] out_hmac;

    return fine; // Calculate TOTP according to RFC standard.
}

std::string TOTP::currentTOTP()
{
    return GenerateTOTP(digit, refreshSeconds, algorithm, secret, getUnixTime());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76104&amp;amp;HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager"&gt;</content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/10/una-watch-totp-mfa-on-your-wrist/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Una Watch - SDK and Writing Your First App]]></title>
		<link>https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/</link>
					<comments>https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#respond</comments>
				<dc:creator><![CDATA[Terence Eden]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 11:34:25 +0000</pubDate>
		<category><![CDATA[c++]]></category><category><![CDATA[HowTo]]></category><category><![CDATA[programming]]></category><category><![CDATA[UnaWatch]]></category>		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=76095</guid>

					<description><![CDATA[I don&#039;t know why hardware manufacturers find it so hard to write decent documentation. As I&#039;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&#039;s how to go from zero to your first &#34;Hello, World!&#34; Glance app with the minimum of fuss.  This all assumes a fairly modern Debian / Ubuntu / Pop OS Linux.  …]]></description>
										<content:encoded>&lt;p&gt;I don't know why hardware manufacturers find it so hard to write decent documentation. As I've said before &lt;a href="https://shkspr.mobi/blog/2025/10/how-to-actually-test-your-readme/"&gt;you need to &lt;em&gt;actually&lt;/em&gt; test your readme&lt;/a&gt;.  The Una Watch development instructions are scattered across a bunch of different files.&lt;/p&gt;

&lt;p&gt;So here's how to go from zero to your first "Hello, World!" Glance app with the minimum of fuss.&lt;/p&gt;

&lt;p&gt;This all assumes a fairly modern Debian / Ubuntu / Pop OS Linux.&lt;/p&gt;

&lt;h2 id="prerequisites"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#prerequisites"&gt;Prerequisites&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;This installs most of the tools you will need:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;sudo apt update
sudo apt install -y git python3 python3-pip cmake build-essential
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="stm32cubeclt"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#stm32cubeclt"&gt;STM32CubeCLT&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;Visit &lt;a href="https://www.st.com/en/development-tools/stm32cubeclt.html"&gt;https://www.st.com/en/development-tools/stm32cubeclt.html&lt;/a&gt; and download the Linux version of the tool. Inexplicably, you need to give your email address. I recommend giving a disposable one from &lt;a href="https://10minutemail.com/"&gt;https://10minutemail.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After downloading, unzip the file.&lt;/p&gt;

&lt;p&gt;On the command line, &lt;code&gt;cd&lt;/code&gt; into the directory containing the unzipped file.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;chmod +x stm32cubeclt_*.sh
sudo ./stm32cubeclt_*.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You will need to agree to their licence. Just hit &lt;code&gt;y&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That installs everything in &lt;code&gt;/opt/st/stm32cubeclt_1.22.0/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You will need to make that available to your PATH.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;export PATH="/opt/st/stm32cubeclt_1.22.0/GNU-tools-for-STM32/bin:$PATH"
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="clone-the-repo"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#clone-the-repo"&gt;Clone the repo&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;You will need to copy &lt;a href="https://github.com/UNAWatch/una-sdk/"&gt;the Una SDK&lt;/a&gt;. I keep my git repos in &lt;code&gt;~/git/&lt;/code&gt; - you will need the location of the directory later.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;cd ~/git/
git clone git@github.com:UNAWatch/una-sdk.git
cd una-sdk/
git submodule update --init ThirdParty/lvgl
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then update all the Python dependencies.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;export UNA_SDK="$PWD"
python3 -m pip install --upgrade pip
python3 -m pip install -r "$UNA_SDK/Utilities/Scripts/app_packer/requirements.txt"
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="check-it-has-all-worked"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#check-it-has-all-worked"&gt;Check it has all worked&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;Paste this into your terminal. Check for any errors.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;which arm-none-eabi-gcc || true
which cmake || true
which make || true
which python3 || true
python3 -m pip --version
arm-none-eabi-gcc --version || true
cmake --version || true
make --version || true
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="build-the-demo-app"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#build-the-demo-app"&gt;Build the demo app&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;If all of the above has worked, you should be able to build the demo app with:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;cd /tmp
mkdir -p MyAlarm
cp -r "$UNA_SDK/Examples/Apps/Alarm/"* MyAlarm
mkdir -p MyAlarm/build
cmake -G "Unix Makefiles" -S MyAlarm/Software/Apps/Alarm-CMake -B MyAlarm/build
cmake --build MyAlarm/build
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="make-it-permanent"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#make-it-permanent"&gt;Make it permanent&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;Store the paths permanently. If you put cloned the Una SDK to somewhere other than &lt;code&gt;~/git/&lt;/code&gt; you'll need to edit this next bit of code:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;echo 'export UNA_SDK="$HOME/git/una-sdk"' &amp;gt;&amp;gt; ~/.bashrc
echo 'export PATH="/opt/st/stm32cubeclt_1.22.0/GNU-tools-for-STM32/bin:$PATH"' &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="build-a-glance-app"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#build-a-glance-app"&gt;Build a Glance app&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;At the moment, the only way to build a GUI app is using the Windows only (Booooo!) TouchGFX&lt;sup id="fnref:windoze"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#fn: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"&gt;0&lt;/a&gt;&lt;/sup&gt;. So, instead, we'll make a "Hello World" glance. All it will do is draw some text.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;GlanceFloors&lt;/code&gt; demo app is the simplest one to copy.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;cd $UNA_SDK/Examples/Apps/
cp -r  GlanceFloors/ GlanceHello
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Use an IDE or a text editor to open up the new GlanceHello directory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rename the directory &lt;code&gt;GlanceHello/Software/App/GlanceFloors-CMake/&lt;/code&gt; to &lt;code&gt;GlanceHello/Software/App/GlanceHello-CMake/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Edit the file &lt;code&gt;GlanceHello/Software/App/GlanceFloors-CMake/CMakeLists.txt&lt;/code&gt; - specifically the &lt;code&gt;# App configuration&lt;/code&gt; section.&lt;/li&gt;
&lt;li&gt;Change the &lt;code&gt;APP_NAME&lt;/code&gt; to &lt;code&gt;GlanceHello&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Change the &lt;code&gt;APP_USER_NAME&lt;/code&gt; to &lt;code&gt;Hello&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a demo, you can leave the &lt;code&gt;DEV_ID&lt;/code&gt; and &lt;code&gt;APP_ID&lt;/code&gt; as their default values.&lt;/p&gt;

&lt;h2 id="minimum-viable-app"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#minimum-viable-app"&gt;Minimum viable app&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;You will need to edit two more files.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;GlanceHello/Software/Libs/Header/Service.hpp&lt;/code&gt; remove the following lines:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-CPP"&gt;SDK::Sensor::Connection mSensorFloors;
uint32_t                mFloorsValue;
bool                    mDataReceived;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then save the file.&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;GlanceHello/Software/Libs/Source/Service.cpp&lt;/code&gt; and replace its entire contents with this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-CPP"&gt;#include "Service.hpp"
#include "SDK/Messages/CommandMessages.hpp"
#include "SDK/Messages/MessageGuard.hpp"

Service::Service(SDK::Kernel &amp;amp;kernel)
    : mKernel(kernel)
    , mGlanceUI()
    , mGlanceTitle()
    , mGlanceValue()
{}

Service::~Service(){}

void Service::run()
{
    while (true) {
        SDK::MessageBase *msg;
        if(!mKernel.comm.getMessage(msg)) {
            continue;
        }

        switch (msg-&amp;gt;getType()) {

            case SDK::MessageType::EVENT_GLANCE_START:
                if (configGui()) {
                    createGuiControls();
                } else {
                    mKernel.comm.releaseMessage(msg);
                    mKernel.sys.exit(0);
                }
                break;

            case SDK::MessageType::COMMAND_APP_STOP:
            case SDK::MessageType::EVENT_GLANCE_STOP:
                mKernel.comm.releaseMessage(msg);
                return;
                break;

            case SDK::MessageType::EVENT_GLANCE_TICK:
                onGlanceTick();
                break;

            default:
                break;
        }

        mKernel.comm.releaseMessage(msg);
    }
}

void Service::onGlanceTick()
{
    if (mGlanceUI.isInvalid()) {
        if (auto upd = SDK::make_msg&amp;lt;SDK::Message::RequestGlanceUpdate&amp;gt;(mKernel)) {
            upd-&amp;gt;name           = APP_NAME;
            upd-&amp;gt;controls       = mGlanceUI.data();
            upd-&amp;gt;controlsNumber = static_cast&amp;lt;uint32_t&amp;gt;(mGlanceUI.size());
            upd.send(100);
        }

        mGlanceUI.setValid();
}
}

bool Service::configGui()
{
    bool status = false;
    if (auto gc = SDK::make_msg&amp;lt;SDK::Message::RequestGlanceConfig&amp;gt;(mKernel)) {
        if (gc.send(100) &amp;amp;&amp;amp; gc.ok()) {
            if (gc-&amp;gt;maxControls &amp;gt;= 3) {
                mGlanceUI.setWidth(gc-&amp;gt;width);
                mGlanceUI.setHeight(gc-&amp;gt;height);
                status = true;
            }
        }
    }
    return status;
}

void Service::createGuiControls()
{
    mGlanceTitle = mGlanceUI.createText();
    mGlanceTitle.pos({ kTitleX, kTitleY }, { kTitleW, kTitleH })
        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)
        .color(GlanceColor_t::GLANCE_COLOR_TEAL)
        .setText("Hello, World!")
        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);
    mGlanceValue = mGlanceUI.createText();
    mGlanceValue.pos({ 0, kValueY }, { 200, kValueH })
        .font(GlanceFont_t::GLANCE_FONT_POPPINS_SEMIBOLD_20)
        .color(GlanceColor_t::GLANCE_COLOR_WHITE)
        .setText("I know C++")
        .alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Save the file.&lt;/p&gt;

&lt;h2 id="compile-it"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#compile-it"&gt;Compile it&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;On the command line&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;cd $UNA_SDK/Examples/Apps/GlanceHello/Software/App/GlanceHello-CMake
cmake -G "Unix Makefiles" -S . -B build
cmake --build build
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It should finish with a success message of &lt;code&gt;[100%] Built target GlanceHelloApp&lt;/code&gt;&lt;/p&gt;

&lt;h2 id="copying-it-to-your-watch"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#copying-it-to-your-watch"&gt;Copying it to your watch&lt;/a&gt;&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Plug your watch into your computer with a USB-C cable.&lt;/li&gt;
&lt;li&gt;On your computer, open the watch's filesystem.&lt;/li&gt;
&lt;li&gt;Open the &lt;code&gt;Apps&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Create a new directory.&lt;/li&gt;
&lt;li&gt;Name the new directory with the string of characters from the &lt;code&gt;APP_ID&lt;/code&gt; which is set in &lt;code&gt;CMakeLists.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The app is stored in &lt;code&gt;GlanceHello/Output/&lt;/code&gt; and will be called &lt;code&gt;Hello_1.5.0-rc5.uapp&lt;/code&gt; or similar.&lt;/li&gt;
&lt;li&gt;Copy the &lt;code&gt;.uapp&lt;/code&gt; file from your computer to the directory you just created on the watch.&lt;/li&gt;
&lt;li&gt;Eject the watch.&lt;/li&gt;
&lt;li&gt;Unplug the cable.&lt;/li&gt;
&lt;li&gt;Restart the watch by pressing the "Up" button until you get to "Power Off", then press "Select", then press "Select" again.&lt;/li&gt;
&lt;li&gt;Turn the watch on by holding down "Select".&lt;/li&gt;
&lt;li&gt;Once the watch has fully restarted, press the "Down" button until you see the "Hello" glance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it has worked, you should see something like this:&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/cpp.webp" alt="A watch displaying &amp;quot;Hello World&amp;quot; and &amp;quot;I know C++&amp;quot;. The latter if off centre because I don't know C++." width="1926" height="2048" class="aligncenter"&gt;

&lt;p&gt;Hurrah! Treat yourself to a beer 🍻&lt;/p&gt;

&lt;div id="footnotes" role="doc-endnotes"&gt;
&lt;hr aria-label="Footnotes"&gt;
&lt;ol start="0"&gt;

&lt;li id="fn:windoze"&gt;
&lt;p&gt;Just after I wrote this, the SDK was updated to allow &lt;a href="https://lvgl.io/"&gt;LVGL&lt;/a&gt; which should work on Linux. There's no documentation for it yet that I could find.&amp;nbsp;&lt;a href="https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/#fnref:windoze" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;
&lt;/div&gt;
&lt;img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76095&amp;amp;HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager"&gt;</content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/10/una-watch-sdk-and-writing-your-first-app/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Gadget Review: Una Watch ★★★★☆]]></title>
		<link>https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/</link>
					<comments>https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#comments</comments>
				<dc:creator><![CDATA[Terence Eden]]></dc:creator>
		<pubDate>Fri, 02 Oct 2026 11:34:48 +0000</pubDate>
		<category><![CDATA[gadget]]></category><category><![CDATA[Review]]></category><category><![CDATA[UnaWatch]]></category><category><![CDATA[watch]]></category>		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=76031</guid>

					<description><![CDATA[I&#039;ve never been a huge fan of smart watches. My £16 smartwatch is basically fine, but the OS is closed source and there&#039;s no way to add new functionality.  Previously I had the eInk Watchy which was a pain to use and really poorly designed.  Even back in 2014 I was bemoaning the design compromises in the MyKronoz ZeWatch Smart Watch.  So why did I pick up the Una Watch?  Firstly, the Una Watch is …]]></description>
										<content:encoded>&lt;p&gt;I've never been a huge fan of smart watches. My &lt;a href="https://shkspr.mobi/blog/2025/08/i-bought-a-16-smartwatch-just-because-it-used-usb-c/"&gt;£16 smartwatch&lt;/a&gt; is basically fine, but the OS is closed source and there's no way to add new functionality.  Previously I had the &lt;a href="https://shkspr.mobi/blog/2023/06/review-watchy-an-eink-watch-full-of-interesting-compromises/"&gt;eInk Watchy&lt;/a&gt; which was a pain to use and really poorly designed.  Even back in 2014 I was bemoaning the design compromises in the &lt;a href="https://shkspr.mobi/blog/2014/11/disassembling-the-mykronoz-zewatch-smart-watch/"&gt;MyKronoz ZeWatch Smart Watch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So why did I pick up the Una Watch?&lt;/p&gt;

&lt;p&gt;Firstly, the Una Watch is designed in Scotland &lt;del&gt;from girders&lt;/del&gt;, and it's always nice to support local businesses.&lt;/p&gt;

&lt;p&gt;Secondly, as a &lt;a href="https://shkspr.mobi/blog/2026/07/im-a-usb-c-maximalist/"&gt;USB-C Maximalist&lt;/a&gt; I want gadgets which can plug in to the same cables as all my other toys. No magnetic pucks here!&lt;/p&gt;

&lt;p&gt;Thirdly, it is (almost) &lt;a href="https://unawatch.com/pages/open-source"&gt;completely open source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, it is repairable. You can easily unscrew it to replace the components. As my cheap smartwatch's dial has died after 12 months of use, that's a pretty compelling proposition!&lt;/p&gt;

&lt;p&gt;Let's put it through its paces!&lt;/p&gt;

&lt;h2 id="first-impressions"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#first-impressions"&gt;First Impressions&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;I bought mine second hand (yay for sustainability) and it arrived with a flat battery. The first charge from 0-100% took a little over an hour. My USB-C power monitor showed it taking in about 5V and 0.17 amps.&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/Charging.webp" alt="Power monitor showing 0.84W." width="1024" height="576" class="aligncenter"&gt;

&lt;p&gt;It happily charged from a PD plug, but didn't get any faster than about 0.84W. Basically, I can fully charge it on most public transport in London.&lt;/p&gt;

&lt;p&gt;The time seemed accurate, there were options to play about with, the vibrations for notifications were easy to feel. There is an option to make it beep with every button press - I turned that off sharpish!&lt;/p&gt;

&lt;h2 id="disclaimer"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#disclaimer"&gt;Disclaimer&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;I am &lt;em&gt;not&lt;/em&gt; &lt;a href="https://shkspr.mobi/blog/2026/09/are-you-a-smartwatch-power-user/"&gt;a smartwatch power user&lt;/a&gt;. I'm not using this to minutely track all my exercise or calculate if my heart is going to explode.  I don't need cm level precision of my GPS. I didn't sync this with Strava or anything else.&lt;/p&gt;

&lt;h2 id="apps"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#apps"&gt;Apps&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;The official Android app (which, sadly, isn't Open Source) worked fine on GrapheneOS. It found the watch, updated its GPS almanac, and let me browse the app store &amp;amp; install apps. Obviously early days, but there are a variety of community developed apps to play with.&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/apps.webp" alt="List of apps." width="504" height="728" class="aligncenter"&gt;

&lt;p&gt;Annoyingly the watch needs to be restarted after every app is installed - but that only take a handful of seconds.&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/Restart-watch.webp" alt="Message telling me the watch needs to restart." width="504" height="640" class="aligncenter"&gt;

&lt;p&gt;There are some &lt;em&gt;strange&lt;/em&gt; error messages.&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/error-message.webp" alt="birthday must be a valid ISO 8601 date string country must be a valid ISO31661 Alpha2 code." width="504" height="426" class="aligncenter"&gt;

&lt;p&gt;That isn't the sort of message which should be shown to users.&lt;/p&gt;

&lt;p&gt;But, on the plus side, you can install Doom!&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/Doom.webp" alt="App store listing showing Doom on the watch." width="504" height="550" class="aligncenter"&gt;

&lt;h2 id="the-screen"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#the-screen"&gt;The Screen&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;Oddly for a modern smartwatch, the screen stays on &lt;em&gt;all the time!&lt;/em&gt; But this isn't some power-hungry OLED, nor is it static eInk. Instead it is a &lt;a href="https://www.andersdx.com/memory-in-pixel-displays/"&gt;memory in pixel&lt;/a&gt; display - black background with orange, blue, and white pixels.  The backlight remains off most of the time and is easy enough to see in daylight. It is &lt;em&gt;slightly&lt;/em&gt; reflective - but not too bad.&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/Reflective.webp" alt="Watch showing notifications, there is a bit of a reflection." width="1024" height="576" class="aligncenter"&gt;

&lt;p&gt;Note the &lt;code&gt;??&lt;/code&gt; on the notifications - more on that later.&lt;/p&gt;

&lt;p&gt;Annoyingly, there's no "raise wrist to light" option. You have to interact with the watch to get the screen on. The accelerometer should allow this functionality - so perhaps it just needs to be activated in the firmware? It is bright enough to see in the dark, but not so bright it will dazzle you or people nearby.&lt;/p&gt;

&lt;p&gt;There's no touchscreen - instead there are four buttons around the face. Up, down, select, back. I did find myself repeatedly jabbing at the screen to no avail.&lt;/p&gt;

&lt;p&gt;So, to light it, press the back button or hold one of the other buttons.&lt;/p&gt;

&lt;p&gt;The colour scheme is pleasant enough. I miss having a full colour display so I can see a photo of my wife whenever I glance at the screen. But the low power usage can't be argued with.&lt;/p&gt;

&lt;h2 id="notifications"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#notifications"&gt;Notifications&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;I couldn't get notifications working at first. The app just refused to let me toggle them on. Eventually I found an app in the app-store which claimed to enable them. That didn't work either.&lt;/p&gt;

&lt;p&gt;Unpairing, repairing, and reinstalling the app made them spring to life.&lt;/p&gt;

&lt;p&gt;There's no notification history. Once you've clicked to read it, that's it. Gone forever. Considering this has 4GB storage, that's an odd decision.&lt;/p&gt;

&lt;p&gt;Some of the notifications were slightly corrupt - showing question marks in place of (I assume) esoteric Unicode.&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/09/Question-Mark-notification.webp" alt="A notification on screen with a question mark before the user's name." width="1024" height="768" class="aligncenter"&gt;

&lt;p&gt;There's no way to customise the vibrate pattern - so everything "feels" the same on your wrist.&lt;/p&gt;

&lt;p&gt;At the moment, the Una Watch sends &lt;em&gt;every&lt;/em&gt; notification to your phone. You can't tell it to ignore certain WhatsApp groups, or only allow text messages from your spouse.  The only way to get fine-grained notifications is with a third-party app like…&lt;/p&gt;

&lt;h2 id="gadgetbridge"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#gadgetbridge"&gt;Gadgetbridge&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;You're not tied to the official app. &lt;a href="https://gadgetbridge.org/gadgets/wearables/una/"&gt;Gadgetbridge support is excellent&lt;/a&gt;. There are a few things missing (you can't install apps or set alarms) - but if you want to measure your heart rate, send notifications, etc you'll be fine.&lt;/p&gt;

&lt;h2 id="linux-compatibility"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#linux-compatibility"&gt;Linux Compatibility&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;The Una Watch plugs in to USB-C and shows up as 3.5GB of exFAT formatted storage.&lt;/p&gt;

&lt;img src="https://shkspr.mobi/blog/wp-content/uploads/2026/10/Una-Filesystem.webp" alt="Filesystem view showing various JSON files." width="500" height="649" class="aligncenter"&gt;

&lt;p&gt;You can manually edit the JSON files if you like. I think you can copy off your workout data. Or you can just use it as portable storage.&lt;/p&gt;

&lt;p&gt;Under &lt;code&gt;lsusb&lt;/code&gt; it describes itself as &lt;code&gt;0483:52a4 STMicroelectronics UNA Watch&lt;/code&gt;&lt;/p&gt;

&lt;h2 id="battery-life"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#battery-life"&gt;Battery Life&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;After a full day of use the battery was at around 95% - that was with a bit of GPS, several notifications, heart rate monitoring, step counting, and a bunch of fiddling. With more GPS use, that's going to be heavier on the battery.&lt;/p&gt;

&lt;p&gt;But the joy of USB-C is that I can thwack in the same cable as I use for all my other gadgets. I can even plug it into my phone and leach a bit of power from there.&lt;/p&gt;

&lt;h2 id="development"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#development"&gt;Development&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;The watch comes will a full &lt;a href="https://github.com/UNAWatch/una-sdk"&gt;Open Source SDK&lt;/a&gt; including lots of assets. There are several tutorials and a friendly community board.&lt;/p&gt;

&lt;p&gt;Of course, everything has to be done in C++ - an accurs'd language which I learned in the last century and wish I'd forgotten.&lt;/p&gt;

&lt;p&gt;Annoyingly, the &lt;a href="https://github.com/UNAWatch/una-sdk/blob/main/Docs/sdk-setup.md"&gt;TouchGFX GUI designer&lt;/a&gt; only works in Windows.&lt;/p&gt;

&lt;p&gt;I'm going to try to build my own watch faces and a few niche apps.&lt;/p&gt;

&lt;h2 id="downsides"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#downsides"&gt;Downsides&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;There are a few things this watch &lt;em&gt;doesn't&lt;/em&gt; do - some of these may be deal-breakers for you, but weren't for me.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No payments. There's no tap-to-pay, NFC, or anything like that.&lt;/li&gt;
&lt;li&gt;No microphone. You cannot speak into your Una Watch or take calls on it.&lt;/li&gt;
&lt;li&gt;No speaker. There's a little buzzer which can make squeaks and squawks - but you won't be playing your music through it.&lt;/li&gt;
&lt;li&gt;While the apps and SDK are fully open, the firmware isn't (yet).&lt;/li&gt;
&lt;li&gt;Can't reply to notifications.&lt;/li&gt;
&lt;li&gt;No maps or directions (yet).&lt;/li&gt;
&lt;li&gt;Step counter only shows the full day - no hour-by-hour view.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these things can and will be fixed in software. Others are limitations of the hardware.&lt;/p&gt;

&lt;h2 id="final-thoughts"&gt;&lt;a href="https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/#final-thoughts"&gt;Final Thoughts&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;The Una Watch has dropped in price to £180. I grabbed mine 2nd hand from eBay for £120. At either price, it's decent value &lt;em&gt;if&lt;/em&gt; you're happy to play with alpha / beta quality technology.&lt;/p&gt;

&lt;p&gt;If you're a serious athlete, you'll probably want a more expensive and polished experience. If you are tied into the Apple or Google ecosystems, you'll probably want one of their watches.&lt;/p&gt;

&lt;p&gt;If you like tinkering, want to experiment with new technology, or simply want to support a British company trying to build something open - then this is the watch for you. Yes, there are some rough edges, but I fundamentally believe that technology should be Open Source, repairable, and give control to its users.&lt;/p&gt;
&lt;img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=76031&amp;amp;HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager"&gt;</content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/10/gadget-review-una-watch/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
	</channel>
</rss>
