<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>HandShakeTec</title><link>https://handshaketec.com/</link><description>Recent content on HandShakeTec</description><generator>Hugo</generator><language>en-GB</language><copyright>&amp;copy; 2026 Matt Oppenheim. All rights reserved.</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://handshaketec.com/index.xml" rel="self" type="application/rss+xml"/><item><title>HandShake T-Watch S3 configuration</title><link>https://handshaketec.com/watch_configuration/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://handshaketec.com/watch_configuration/</guid><description>&lt;!-- enable hypermedia by importing from unpkg.com -->
&lt;head>
&lt;script src="https://unpkg.com/htmx.org@2.0.2" crossorigin="anonymous">&lt;/script>
&lt;/head>
&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>HandShake configuration tool for the T-Watch S3.&lt;/p>
&lt;p>&lt;strong>Under test - something will need fixing!&lt;/strong>&lt;/p>

&lt;div class="session-box" id="session-box">
 &lt;h2>Your session name&lt;/h2>
 &lt;div class="session-name" id="session-name">…&lt;/div>
 &lt;div class="name-change">
 &lt;input type="text" id="new-god-name" placeholder="e.g. hermes" autocomplete="off">
 &lt;button type="button" onclick="changeSessionName()">Change&lt;/button>
 &lt;span id="name-status">&lt;/span>
 &lt;/div>
&lt;/div>

&lt;style>
 .session-box { background: #f0f4ff; border: 1px solid #b0c0e0; border-radius: 6px;
 padding: 1em 1.2em; margin-bottom: 1.5em; }
 .session-box h2 { margin: 0 0 0.4em; font-size: 1em; color: #444; }
 .session-name { font-size: 1.4em; font-weight: bold; color: #2244aa;
 text-transform: capitalize; }
 .name-change { display: flex; gap: 0.5em; margin-top: 0.7em; align-items: center; }
 .name-change input { padding: 0.3em 0.5em; border: 1px solid #aaa; border-radius: 4px;
 font-size: 0.95em; width: 160px; }
 .name-change button { padding: 0.3em 0.8em; border: none; border-radius: 4px;
 background: #3355cc; color: white; cursor: pointer; font-size: 0.95em; }
 .name-change button:hover { background: #2244aa; }
 #name-status { font-size: 0.9em; color: #555; }
 #name-status.error { color: #cc2222; }
&lt;/style>

&lt;script>
 
 fetch('/whoami')
 .then(r => r.json().then(data => ({ ok: r.ok, data })))
 .then(({ ok, data }) => {
 const el = document.getElementById('session-name');
 if (ok &amp;&amp; data.god_name) {
 el.textContent = data.god_name;
 } else if (data.error === 'server_full') {
 el.textContent = '(server full)';
 el.style.color = '#cc2222';
 document.getElementById('name-status').textContent =
 'All ' + data.max_users + ' sessions are in use — try again later.';
 } else {
 el.textContent = '(unavailable)';
 }
 })
 .catch(() => {
 document.getElementById('session-name').textContent = '(offline)';
 });

 function changeSessionName() {
 const input = document.getElementById('new-god-name');
 const status = document.getElementById('name-status');
 const name = input.value.trim().toLowerCase();

 if (!name) {
 status.textContent = 'Please enter a name.';
 status.className = 'error';
 return;
 }

 status.textContent = 'Changing...';
 status.className = '';

 const body = new URLSearchParams();
 body.append('god_name', name);

 fetch('/set_god_name', { method: 'POST', body: body })
 .then(r => r.text().then(text => ({ ok: r.ok, text })))
 .then(({ ok, text }) => {
 if (ok) {
 document.getElementById('session-name').textContent = text;
 status.textContent = 'Name changed.';
 status.className = '';
 input.value = '';
 } else {
 status.textContent = text;
 status.className = 'error';
 }
 })
 .catch(() => {
 status.textContent = 'Network error.';
 status.className = 'error';
 });
 }
&lt;/script>

&lt;h1 id="test-form-is-json-enabled">Gesture Selection&lt;/h1>
&lt;form hx-target="#gesture_response" hx-post="/submit_gesture" hx-swap="innerHTML" id="gesture_to_detect">
 &lt;label for="punch_gesture">punch gesture&lt;/label>
 &lt;input type="radio" id="option1" name="gesture_to_detect" value="punch_gesture" placeholder="punch" checked>&lt;br>
 &lt;label for="roll_gesture">roll gesture&lt;/label>
 &lt;input type="radio" id="option2" name="gesture_to_detect" value="roll_gesture" placeholder="roll">&lt;br>
 &lt;input type="submit" value="submit_gesture">
&lt;/form>
&lt;div id="gesture_response">&lt;/div>

&lt;h1 id="gesture-sensitivity-control">Gesture Sensitivity Control&lt;/h1>
&lt;form hx-target="#sensitivity_response" hx-post="/submit_sensitivity" hx-swap="innerHTML" id="sensitivity_form">
 &lt;label for="gesture_sensitivity">Gesture Sensitivity (580-900):&lt;/label>&lt;br>
 &lt;input type="range" id="gesture_sensitivity" name="gesture_sensitivity" min="580" max="900" value="740" step="10" oninput="this.nextElementSibling.value = this.value">
 &lt;output>740&lt;/output>&lt;br>&lt;br>
 &lt;input type="submit" value="submit_sensitivity">
&lt;/form>
&lt;div id="sensitivity_response">&lt;/div>

&lt;h1 id="esp-now-mac-address-configuration">ESP-NOW MAC Address Configuration&lt;/h1>
&lt;style>
.btn-active {
 background-color: #4CAF50 !important;
 color: white !important;
 border: 2px solid #45a049 !important;
}

.btn-flash {
 animation: flash-green 0.5s;
}

@keyframes flash-green {
 0%, 100% { background-color: initial; }
 50% { background-color: #4CAF50; color: white; }
}
&lt;/style>

&lt;button id="esp_now_button" style="padding: 8px 16px;">ESP-NOW target MAC&lt;/button>

&lt;div id="mac_address_section" style="display: none; margin-top: 20px;">
 &lt;form hx-target="#mac_response" hx-post="/submit_mac" hx-swap="innerHTML" hx-on::after-request="handleMacSubmit(event)" id="mac_form">
 &lt;label for="mac_address">MAC Address (6 pairs of hex values):&lt;/label>&lt;br>
 &lt;div style="display: flex; align-items: center; gap: 5px; margin-top: 10px;">
 &lt;input type="text" class="mac-input" id="mac1" name="mac1" maxlength="2" pattern="[0-9A-Fa-f]{2}" required style="width: 35px; text-align: center;">
 &lt;span>,&lt;/span>
 &lt;input type="text" class="mac-input" id="mac2" name="mac2" maxlength="2" pattern="[0-9A-Fa-f]{2}" required style="width: 35px; text-align: center;">
 &lt;span>,&lt;/span>
 &lt;input type="text" class="mac-input" id="mac3" name="mac3" maxlength="2" pattern="[0-9A-Fa-f]{2}" required style="width: 35px; text-align: center;">
 &lt;span>,&lt;/span>
 &lt;input type="text" class="mac-input" id="mac4" name="mac4" maxlength="2" pattern="[0-9A-Fa-f]{2}" required style="width: 35px; text-align: center;">
 &lt;span>,&lt;/span>
 &lt;input type="text" class="mac-input" id="mac5" name="mac5" maxlength="2" pattern="[0-9A-Fa-f]{2}" required style="width: 35px; text-align: center;">
 &lt;span>,&lt;/span>
 &lt;input type="text" class="mac-input" id="mac6" name="mac6" maxlength="2" pattern="[0-9A-Fa-f]{2}" required style="width: 35px; text-align: center;">
 &lt;/div>
 &lt;br>
 &lt;input type="submit" value="Submit MAC" id="submit_mac_button" style="padding: 8px 16px;">
 &lt;/form>
 &lt;div id="mac_response">&lt;/div>
&lt;/div>

&lt;script>
var espButton = document.getElementById('esp_now_button');
var macSection = document.getElementById('mac_address_section');

function toggleMacDisplay() {
 var section = document.getElementById('mac_address_section');
 var button = document.getElementById('esp_now_button');
 
 if (!section) {
 return;
 }
 
 if (section.style.display === 'none' || section.style.display === '') {
 section.style.display = 'block';
 button.classList.add('btn-active');
 
 
 fetch('/get_mac', {
 method: 'GET',
 headers: {
 'Content-Type': 'application/json',
 }
 })
 .then(function(response) { return response.json(); })
 .then(function(data) {
 if (data.mac_address) {
 var macParts = data.mac_address.split(',');
 if (macParts.length === 6) {
 macParts.forEach(function(part, index) {
 var inputField = document.getElementById('mac' + (index + 1));
 if (inputField) {
 inputField.value = part.toUpperCase();
 }
 });
 }
 }
 })
 .catch(function(error) {
 console.error('Error fetching MAC address:', error);
 });
 } else {
 section.style.display = 'none';
 button.classList.remove('btn-active');
 
 
 fetch('/delete_mac', {
 method: 'POST',
 headers: {
 'Content-Type': 'application/x-www-form-urlencoded',
 }
 });
 }
}

if (espButton) {
 espButton.addEventListener('click', toggleMacDisplay);
}

window.handleMacSubmit = function(event) {
 if (event.detail.successful) {
 var submitBtn = document.getElementById('submit_mac_button');
 if (submitBtn) {
 submitBtn.classList.add('btn-flash');
 setTimeout(function() {
 submitBtn.classList.remove('btn-flash');
 }, 500);
 }
 }
};
&lt;/script>

&lt;script>

document.addEventListener('DOMContentLoaded', function() {
 const macInputs = document.querySelectorAll('.mac-input');
 
 macInputs.forEach((input, index) => {
 input.addEventListener('input', function(e) {
 
 this.value = this.value.toUpperCase();
 
 
 if (this.value.length === 2 &amp;&amp; index &lt; macInputs.length - 1) {
 macInputs[index + 1].focus();
 }
 });
 
 
 input.addEventListener('keydown', function(e) {
 if (e.key === 'Backspace' &amp;&amp; this.value.length === 0 &amp;&amp; index > 0) {
 macInputs[index - 1].focus();
 }
 });
 });
});
&lt;/script>

&lt;h1>Generate Firmware&lt;/h1>

&lt;div id="generate-container">
 &lt;button 
 id="generate-btn"
 hx-post="/generate_firmware"
 hx-trigger="click"
 hx-swap="none"
 class="generate-button"
 onclick="this.classList.add('clicked'); document.getElementById('output-box').style.display='block'; document.getElementById('throbber').style.display='block'; document.getElementById('output-text').textContent='';"
 hx-on::after-request="if(event.detail.successful) { pollStatus(); }"
 >
 Generate Firmware
 &lt;/button>
 
 &lt;div id="throbber" style="display:none; margin-top:10px;">
 &lt;div class="spinner">&lt;/div>
 &lt;p>Building firmware, please wait...&lt;/p>
 &lt;/div>
 
 &lt;div id="output-box" style="display:none; margin-top:10px;">
 &lt;div class="output-container">
 &lt;h3>Build Output:&lt;/h3>
 &lt;pre id="output-text" class="output-text">&lt;/pre>
 &lt;button id="ok-button" class="ok-button" style="display:none;" onclick="document.getElementById('output-box').style.display='none'; document.getElementById('generate-btn').classList.remove('clicked');">OK&lt;/button>
 &lt;/div>
 &lt;/div>
&lt;/div>

&lt;style>
 .generate-button {
 padding: 10px 20px;
 font-size: 16px;
 background-color: #007bff;
 color: white;
 border: none;
 border-radius: 5px;
 cursor: pointer;
 transition: background-color 0.3s;
 }
 
 .generate-button:hover {
 background-color: #0056b3;
 }
 
 .generate-button.clicked {
 background-color: #28a745;
 }
 
 .spinner {
 border: 4px solid #f3f3f3;
 border-top: 4px solid #3498db;
 border-radius: 50%;
 width: 40px;
 height: 40px;
 animation: spin 1s linear infinite;
 }
 
 @keyframes spin {
 0% { transform: rotate(0deg); }
 100% { transform: rotate(360deg); }
 }
 
 .output-container {
 padding: 15px;
 background-color: #f8f9fa;
 border: 1px solid #dee2e6;
 border-radius: 5px;
 }
 
 .output-text {
 padding: 10px;
 background-color: #1e1e1e;
 color: #d4d4d4;
 border: 1px solid #333;
 border-radius: 3px;
 max-height: 400px;
 overflow-y: auto;
 font-family: 'Courier New', Courier, monospace;
 font-size: 12px;
 white-space: pre-wrap;
 word-wrap: break-word;
 }
 
 .ok-button {
 margin-top: 10px;
 padding: 8px 20px;
 font-size: 14px;
 background-color: #007bff;
 color: white;
 border: none;
 border-radius: 5px;
 cursor: pointer;
 }
 
 .ok-button:hover {
 background-color: #0056b3;
 }
 
 .success-indicator {
 color: #28a745;
 font-weight: bold;
 }
 
 .error-indicator {
 color: #dc3545;
 font-weight: bold;
 }
&lt;/style>

&lt;script>
 let pollInterval;
 
 function pollStatus() {
 pollInterval = setInterval(async () => {
 try {
 const response = await fetch('/generate_status');
 const data = await response.json();
 
 
 const outputText = document.getElementById('output-text');
 if (data.output) {
 outputText.textContent = data.output;
 
 outputText.scrollTop = outputText.scrollHeight;
 }
 
 if (!data.running) {
 clearInterval(pollInterval);
 document.getElementById('throbber').style.display = 'none';
 
 
 document.getElementById('ok-button').style.display = 'block';
 
 
 const statusIndicator = document.createElement('div');
 statusIndicator.style.marginTop = '10px';
 
 if (data.success) {
 statusIndicator.className = 'success-indicator';
 statusIndicator.textContent = '✓ Build completed successfully';
 
 
 const downloadContainer = document.getElementById('elf-check-container');
 if (downloadContainer) {
 htmx.trigger(downloadContainer, 'refreshDownload');
 }
 } else if (data.success === false) {
 statusIndicator.className = 'error-indicator';
 statusIndicator.textContent = '✗ Build failed';
 }
 
 
 const okButton = document.getElementById('ok-button');
 okButton.parentNode.insertBefore(statusIndicator, okButton);
 }
 } catch (error) {
 console.error('Error polling status:', error);
 clearInterval(pollInterval);
 document.getElementById('throbber').style.display = 'none';
 document.getElementById('output-text').textContent += '\n\nError: Failed to retrieve build status';
 document.getElementById('ok-button').style.display = 'block';
 }
 }, 1000); 
 }
&lt;/script>

&lt;h1>Download Firmware&lt;/h1>

&lt;div id="download-elf-container">
 
 &lt;div 
 id="elf-check-container"
 hx-get="/check_elf"
 hx-trigger="load, refreshDownload from:body"
 hx-swap="innerHTML"
 >
 &lt;p>Checking for firmware file...&lt;/p>
 &lt;/div>
&lt;/div>

&lt;style>
 .download-button {
 padding: 10px 20px;
 font-size: 16px;
 background-color: #28a745;
 color: white;
 border: none;
 border-radius: 5px;
 cursor: pointer;
 transition: background-color 0.3s;
 text-decoration: none;
 display: inline-block;
 }
 
 .download-button:hover {
 background-color: #218838;
 }
 
 .download-button:active {
 background-color: #1e7e34;
 }
 
 .file-info {
 margin-top: 10px;
 padding: 10px;
 background-color: #f8f9fa;
 border: 1px solid #dee2e6;
 border-radius: 5px;
 font-size: 14px;
 color: #495057;
 }
 
 .no-file-message {
 color: #6c757d;
 font-style: italic;
 }
&lt;/style>
</description></item><item><title>mattoppenheim</title><link>https://handshaketec.com/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://handshaketec.com/about/</guid><description>&lt;p>I re-purpose off the shelf hardware to create enabling technology. All of the project ideas come from the staff who work at specialist schools with the target end-users.&lt;/p>
&lt;p>I spend about half of the year working at sea in marine geophysical survey or offshore construction. During my time off, I maintain an Honorary Researcher position at InfoLab21, Lancaster University, where I work on the enabling technology projects.&lt;/p>
&lt;p>My GitHub page is &lt;a href="https://github.com/mattoppenheim">here&lt;/a>.&lt;/p>
&lt;p>email me at matt@(this page&amp;rsquo;s title).com&lt;/p></description></item><item><title>Watch Configuration Help</title><link>https://handshaketec.com/watch_configuration_help/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://handshaketec.com/watch_configuration_help/</guid><description>&lt;p>The &lt;strong>Watch Configuration&lt;/strong> page lets you choose what your T-Watch S3 does, then builds a
custom firmware file (the program that runs on the watch) for you to download and
put onto the device. You don&amp;rsquo;t need any programming knowledge — just work through
the page from top to bottom.&lt;/p>
&lt;h2 id="what-youll-need">What you&amp;rsquo;ll need&lt;/h2>
&lt;ul>
&lt;li>A LilyGo T-Watch S3.&lt;/li>
&lt;li>A USB cable that can carry data, not just charge. (Many cheap cables only charge — if
your computer doesn&amp;rsquo;t notice the watch when you plug it in, try another cable.)&lt;/li>
&lt;li>A computer running the &lt;strong>Chrome&lt;/strong> or &lt;strong>Edge&lt;/strong> web browser. The page you&amp;rsquo;ll use to put
the firmware onto the watch doesn&amp;rsquo;t work in Firefox or Safari.&lt;/li>
&lt;/ul>
&lt;h2 id="step-1--check-your-session-name">Step 1 — Check your session name&lt;/h2>
&lt;p>At the top of the page you&amp;rsquo;ll see your &lt;strong>session name&lt;/strong>.&lt;/p>
&lt;p>Everybody using the site at the same time gets their own name, so the firmware you build
doesn&amp;rsquo;t get mixed up with anyone else&amp;rsquo;s. By default it&amp;rsquo;s the name of a Greek god — there
are plenty of them, so it makes for an easy way to keep everyone apart!&lt;/p>
&lt;p>Your firmware file will have this name in its filename, which is how you&amp;rsquo;ll recognise it
later. If you&amp;rsquo;d like, type a name of your own (for example, the name of the person who&amp;rsquo;ll
use the watch) and click &lt;strong>Change&lt;/strong>. Otherwise, just leave the name you&amp;rsquo;ve been given.&lt;/p>
&lt;h2 id="step-2--choose-a-gesture">Step 2 — Choose a gesture&lt;/h2>
&lt;p>Under &lt;strong>Gesture Selection&lt;/strong>, pick the movement the watch should respond to — a &lt;strong>punch&lt;/strong>
or a &lt;strong>roll&lt;/strong> of the wrist. Choose one, then click &lt;strong>submit_gesture&lt;/strong>.&lt;/p>
&lt;h2 id="step-3--set-the-gesture-sensitivity">Step 3 — Set the gesture sensitivity&lt;/h2>
&lt;p>The &lt;strong>Gesture Sensitivity&lt;/strong> slider controls how strong a movement has to be before the
watch registers it. Start with the default and click &lt;strong>submit_sensitivity&lt;/strong>. You can
always come back, build again, and adjust this if you find the watch reacts too easily or
not easily enough.&lt;/p>
&lt;h2 id="step-4--set-an-esp-now-target-optional">Step 4 — Set an ESP-NOW target (optional)&lt;/h2>
&lt;p>This step is only needed if your watch has to send its signal wirelessly to another
HandShake device. If that applies to you, click the &lt;strong>ESP-NOW target MAC&lt;/strong> button to
reveal the boxes, type in the six pairs of characters that make up the other device&amp;rsquo;s
address, and click &lt;strong>Submit MAC&lt;/strong>.&lt;/p>
&lt;p>If you don&amp;rsquo;t know what this is, you can safely skip it.&lt;/p>
&lt;h2 id="step-5--generate-the-firmware">Step 5 — Generate the firmware&lt;/h2>
&lt;p>Each time you change one of the controls above, make sure you&amp;rsquo;ve clicked its &lt;strong>submit&lt;/strong>
button — this saves your choice in the background.&lt;/p>
&lt;p>When you&amp;rsquo;re happy with all your selections, click &lt;strong>Generate Firmware&lt;/strong>. The site reads
your choices and builds the firmware file. This usually takes a minute or two, and you&amp;rsquo;ll
see progress messages while it works, so please be patient.&lt;/p>
&lt;h2 id="step-6--download-the-firmware">Step 6 — Download the firmware&lt;/h2>
&lt;p>Once the build finishes, a &lt;strong>download&lt;/strong> option appears. Click it to save the firmware file
to your computer.&lt;/p>
&lt;p>Once it&amp;rsquo;s downloaded, the firmware is yours forever! You own it. No time limits, no
subscriptions. All yours.&lt;/p>
&lt;h2 id="putting-the-firmware-onto-your-watch">Putting the firmware onto your watch&lt;/h2>
&lt;p>I&amp;rsquo;m going to add a simple &amp;lsquo;one-click&amp;rsquo; upload page to make this easy. Until then, you can
use this firmware upload site:&lt;/p>
&lt;p>&lt;a href="https://thelastoutpostworkshop.github.io/ESPConnect/">ESPConnect firmware uploader&lt;/a>&lt;/p>
&lt;ol>
&lt;li>Plug the watch into your computer with a USB data cable.&lt;/li>
&lt;li>Click &lt;strong>Connect&lt;/strong> on the top toolbar, then choose the port the watch is connected to.&lt;/li>
&lt;li>Select &lt;strong>Flash Tools&lt;/strong> from the menu on the left.&lt;/li>
&lt;li>Leave all the default settings as they are — in particular, the &lt;strong>Start offset&lt;/strong> is
already correct at &lt;code>0x0&lt;/code>, so there&amp;rsquo;s no need to change it.&lt;/li>
&lt;li>Click the icon next to the &lt;strong>Firmware binary (.bin)&lt;/strong> box and choose the firmware file
you saved.&lt;/li>
&lt;li>Click &lt;strong>Flash Firmware&lt;/strong> just below it.&lt;/li>
&lt;/ol>
&lt;p>The new firmware will be written to the watch. You may need to disconnect the watch and
restart it for the new firmware to run, though sometimes it starts on its own.&lt;/p>
&lt;p>&lt;strong>Don&amp;rsquo;t panic if something goes wrong.&lt;/strong> If you accidentally flash the wrong firmware to
the watch, just reconnect it and flash the correct file again.&lt;/p></description></item></channel></rss>