{"version":"https://jsonfeed.org/version/1.1","title":"HandShakeTec","description":"Recent content on HandShakeTec","home_page_url":"https://handshaketec.com/","feed_url":"https://handshaketec.com/index.json","language":"en-GB","icon":"https://handshaketec.com/logo.png","favicon":"https://handshaketec.com/logo.png","authors":[{"name":"Matt Oppenheim","url":"https://mattoppenheim.com"}],"items":[{"title":"Watch Flasher","date_published":"2026-07-06T00:00:00Z","date_modified":"2026-07-06T00:00:00Z","id":"https://handshaketec.com/watch_flasher/","url":"https://handshaketec.com/watch_flasher/","content_html":"\u003ch1 id=\"flash-firmware-to-your-lilygo-t-watch-s3\"\u003eFlash Firmware to Your Lilygo T-Watch S3\u003c/h1\u003e\n\u003cp\u003eThis page allows you to flash custom firmware that you\u0026rsquo;ve downloaded from the Watch Configuration page directly to your Lilygo T-Watch S3 smartwatch.\u003c/p\u003e\n\u003ch2 id=\"how-to-use\"\u003eHow to Use\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eDownload firmware\u003c/strong\u003e from the \u003ca href=\"/watch_configuration/\"\u003eWatch Configuration\u003c/a\u003e page\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConnect\u003c/strong\u003e your T-Watch S3 to your computer via USB cable\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSelect the firmware file\u003c/strong\u003e you downloaded using the file picker below\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eClick Flash\u003c/strong\u003e once the device is detected and verified\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWait\u003c/strong\u003e for the flashing process to complete (~30 seconds)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVerify\u003c/strong\u003e your watch has the new firmware\u003c/li\u003e\n\u003c/ol\u003e\n\n\u003cstyle\u003e\n  .flasher-container {\n    max-width: 600px;\n    margin: 2rem auto;\n    padding: 1rem;\n    border: 1px solid #ccc;\n    border-radius: 8px;\n    background-color: #f9f9f9;\n  }\n\n  @media (prefers-color-scheme: dark) {\n    .flasher-container {\n      background-color: #222;\n      border-color: #555;\n    }\n  }\n\n  .flasher-section {\n    margin: 1.5rem 0;\n    padding: 1rem;\n    border-left: 4px solid #2196F3;\n  }\n\n  .flasher-section h3 {\n    margin-top: 0;\n    color: #2196F3;\n  }\n\n  .file-input-wrapper {\n    margin: 1rem 0;\n  }\n\n  .file-input-wrapper input[type=\"file\"] {\n    padding: 0.5rem;\n    cursor: pointer;\n  }\n\n  .status-message {\n    padding: 0.75rem;\n    margin: 1rem 0;\n    border-radius: 4px;\n    display: none;\n  }\n\n  .status-message.show {\n    display: block;\n  }\n\n  .status-message.info {\n    background-color: #e3f2fd;\n    color: #01579b;\n    border-left: 4px solid #2196F3;\n  }\n\n  .status-message.success {\n    background-color: #e8f5e9;\n    color: #1b5e20;\n    border-left: 4px solid #4caf50;\n  }\n\n  .status-message.error {\n    background-color: #ffebee;\n    color: #b71c1c;\n    border-left: 4px solid #f44336;\n  }\n\n  .status-message.warning {\n    background-color: #fff3e0;\n    color: #e65100;\n    border-left: 4px solid #ff9800;\n  }\n\n  @media (prefers-color-scheme: dark) {\n    .status-message.info {\n      background-color: #1a237e;\n      color: #90caf9;\n    }\n    .status-message.success {\n      background-color: #1b5e20;\n      color: #c8e6c9;\n    }\n    .status-message.error {\n      background-color: #b71c1c;\n      color: #ffcdd2;\n    }\n    .status-message.warning {\n      background-color: #e65100;\n      color: #ffe0b2;\n    }\n  }\n\n  .button-group {\n    display: flex;\n    gap: 1rem;\n    margin: 1rem 0;\n    justify-content: center;\n  }\n\n  .btn {\n    padding: 0.75rem 1.5rem;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n    font-size: 1rem;\n    transition: background-color 0.3s;\n  }\n\n  .btn-connect {\n    background-color: #90EE90;\n    color: #000;\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n\n  .btn-connect.enabled {\n    background-color: #28a745;\n    color: #fff;\n    opacity: 1;\n    cursor: pointer;\n  }\n\n  .btn-flash {\n    background-color: #90EE90;\n    color: #000;\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n\n  .btn-flash.enabled {\n    background-color: #28a745;\n    color: #fff;\n    opacity: 1;\n    cursor: pointer;\n  }\n\n  .btn-disconnect {\n    background-color: #dc3545;\n    color: #fff;\n    display: none;\n  }\n\n  .btn-disconnect.show {\n    display: inline-block;\n  }\n\n  .progress-container {\n    display: none;\n    margin: 1rem 0;\n  }\n\n  .progress-container.show {\n    display: block;\n  }\n\n  .progress-bar {\n    width: 100%;\n    height: 24px;\n    border-radius: 4px;\n    background-color: #e0e0e0;\n    overflow: hidden;\n  }\n\n  @media (prefers-color-scheme: dark) {\n    .progress-bar {\n      background-color: #444;\n    }\n  }\n\n  .progress-fill {\n    height: 100%;\n    background-color: #4caf50;\n    width: 0%;\n    transition: width 0.1s;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: #fff;\n    font-weight: bold;\n    font-size: 0.9rem;\n  }\n\n  .spinner {\n    display: inline-block;\n    width: 20px;\n    height: 20px;\n    border: 3px solid #f3f3f3;\n    border-top: 3px solid #2196F3;\n    border-radius: 50%;\n    animation: spin 1s linear infinite;\n  }\n\n  @keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(360deg); }\n  }\n\n  .device-info {\n    background-color: #f0f0f0;\n    padding: 1rem;\n    border-radius: 4px;\n    margin: 1rem 0;\n    display: none;\n    font-family: monospace;\n    font-size: 0.9rem;\n  }\n\n  .device-info.show {\n    display: block;\n  }\n\n  @media (prefers-color-scheme: dark) {\n    .device-info {\n      background-color: #333;\n      color: #0f0;\n    }\n  }\n\n  .device-info-row {\n    display: flex;\n    justify-content: space-between;\n    padding: 0.25rem 0;\n  }\n\n  .device-info-label {\n    font-weight: bold;\n  }\n\u003c/style\u003e\n\n\u003cdiv class=\"flasher-container\" id=\"flasher-app\"\u003e\n  \u003c!-- File Selection Section --\u003e\n  \u003cdiv class=\"flasher-section\"\u003e\n    \u003ch3\u003e1. Select Firmware File\u003c/h3\u003e\n    \u003cp\u003eChoose the .bin firmware file you downloaded from Watch Configuration.\u003c/p\u003e\n    \u003cdiv class=\"file-input-wrapper\"\u003e\n      \u003cinput type=\"file\" id=\"firmware-file\" accept=\".bin\" /\u003e\n    \u003c/div\u003e\n    \u003cdiv id=\"file-status\" class=\"status-message\"\u003e\u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Serial Port Detection Section --\u003e\n  \u003cdiv class=\"flasher-section\"\u003e\n    \u003ch3\u003e2. Detect Device\u003c/h3\u003e\n    \u003cp\u003eClick to detect and connect to your T-Watch S3 via USB.\u003c/p\u003e\n    \u003cdiv class=\"button-group\"\u003e\n      \u003cbutton class=\"btn btn-connect\" id=\"btn-connect\"\u003eDetect \u0026 Connect\u003c/button\u003e\n      \u003cbutton class=\"btn btn-disconnect\" id=\"btn-disconnect\"\u003eDisconnect\u003c/button\u003e\n    \u003c/div\u003e\n    \u003cdiv id=\"connect-status\" class=\"status-message\"\u003e\u003c/div\u003e\n    \u003cdiv id=\"device-info\" class=\"device-info\"\u003e\n      \u003cdiv class=\"device-info-row\"\u003e\n        \u003cspan class=\"device-info-label\"\u003ePort:\u003c/span\u003e\n        \u003cspan id=\"port-name\"\u003e-\u003c/span\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"device-info-row\"\u003e\n        \u003cspan class=\"device-info-label\"\u003eChip:\u003c/span\u003e\n        \u003cspan id=\"chip-name\"\u003e-\u003c/span\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"device-info-row\"\u003e\n        \u003cspan class=\"device-info-label\"\u003eVariant:\u003c/span\u003e\n        \u003cspan id=\"chip-variant\"\u003e-\u003c/span\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"device-info-row\"\u003e\n        \u003cspan class=\"device-info-label\"\u003eMAC Address:\u003c/span\u003e\n        \u003cspan id=\"mac-address\"\u003e-\u003c/span\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Flashing Section --\u003e\n  \u003cdiv class=\"flasher-section\"\u003e\n    \u003ch3\u003e3. Flash Firmware\u003c/h3\u003e\n    \u003cp\u003eClick to write the selected firmware to your device.\u003c/p\u003e\n    \u003cdiv class=\"button-group\"\u003e\n      \u003cbutton class=\"btn btn-flash\" id=\"btn-flash\"\u003eFlash Firmware\u003c/button\u003e\n    \u003c/div\u003e\n    \n    \u003c!-- Progress Bar --\u003e\n    \u003cdiv id=\"progress-container\" class=\"progress-container\"\u003e\n      \u003cp\u003eFlashing firmware... This takes about 30 seconds.\u003c/p\u003e\n      \u003cdiv class=\"progress-bar\"\u003e\n        \u003cdiv class=\"progress-fill\" id=\"progress-fill\"\u003e0%\u003c/div\u003e\n      \u003c/div\u003e\n      \u003cp id=\"progress-message\"\u003eInitializing...\u003c/p\u003e\n    \u003c/div\u003e\n\n    \u003cdiv id=\"flash-status\" class=\"status-message\"\u003e\u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Instructions Section --\u003e\n  \u003cdiv class=\"flasher-section\" style=\"border-left-color: #666;\"\u003e\n    \u003ch3\u003eTroubleshooting\u003c/h3\u003e\n    \u003cul\u003e\n      \u003cli\u003e\u003cstrong\u003eDevice not detected?\u003c/strong\u003e Make sure your T-Watch S3 is connected via USB and powered on.\u003c/li\u003e\n      \u003cli\u003e\u003cstrong\u003eWrong chip detected?\u003c/strong\u003e The device must be an ESP32-S3 variant QFN56. Check your device.\u003c/li\u003e\n      \u003cli\u003e\u003cstrong\u003eFlashing failed?\u003c/strong\u003e Try disconnecting and reconnecting your device.\u003c/li\u003e\n      \u003cli\u003e\u003cstrong\u003eStill having issues?\u003c/strong\u003e Check the browser console (F12) for error messages.\u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\n// Inline all functionality\nlet port = null;\n\n// Check if Web Serial API is supported\nif (!('serial' in navigator)) {\n  document.addEventListener('DOMContentLoaded', () =\u003e {\n    document.getElementById('connect-status').innerHTML = '\u003cdiv class=\"status-message show error\"\u003e❌ Web Serial API not supported. Please use Chrome, Edge, or Opera (version 79+)\u003c/div\u003e';\n  });\n}\n\n// File validation\nfunction validateFile(file) {\n  const MIN_SIZE = 256 * 1024;\n  const MAX_SIZE = 16 * 1024 * 1024;\n\n  if (!file.name.endsWith('.bin')) {\n    return { valid: false, message: '❌ File must be a .bin file' };\n  }\n  if (file.size \u003c MIN_SIZE) {\n    return { valid: false, message: `❌ File too small (${formatBytes(file.size)}). Minimum is ${formatBytes(MIN_SIZE)}` };\n  }\n  if (file.size \u003e MAX_SIZE) {\n    return { valid: false, message: `❌ File too large (${formatBytes(file.size)}). Maximum is ${formatBytes(MAX_SIZE)}` };\n  }\n  return { valid: true, message: `✅ Valid firmware file (${formatBytes(file.size)})` };\n}\n\nfunction formatBytes(bytes) {\n  if (bytes \u003c 1024) return bytes + ' B';\n  if (bytes \u003c 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';\n  return (bytes / (1024 * 1024)).toFixed(2) + ' MB';\n}\n\n// UI helpers\nfunction showStatus(elementId, message, type = 'info') {\n  const el = document.getElementById(elementId);\n  el.textContent = message;\n  el.className = 'status-message show ' + type;\n}\n\nfunction setButtonEnabled(btnId, enabled) {\n  const btn = document.getElementById(btnId);\n  if (enabled) {\n    btn.classList.add('enabled');\n    btn.disabled = false;\n  } else {\n    btn.classList.remove('enabled');\n    btn.disabled = true;\n  }\n}\n\nfunction setButtonVisible(btnId, visible) {\n  const btn = document.getElementById(btnId);\n  if (visible) {\n    btn.classList.add('show');\n  } else {\n    btn.classList.remove('show');\n  }\n}\n\nfunction checkReadyToFlash() {\n  const fileValid = document.getElementById('firmware-file').files.length \u003e 0;\n  const deviceConnected = port !== null;\n  return fileValid \u0026\u0026 deviceConnected;\n}\n\n// Event listeners\ndocument.addEventListener('DOMContentLoaded', () =\u003e {\n  // File selection\n  document.getElementById('firmware-file').addEventListener('change', (e) =\u003e {\n    if (e.target.files[0]) {\n      const validation = validateFile(e.target.files[0]);\n      showStatus('file-status', validation.message, validation.valid ? 'success' : 'error');\n      setButtonEnabled('btn-flash', checkReadyToFlash());\n    }\n  });\n\n  // Connect button\n  document.getElementById('btn-connect').addEventListener('click', async () =\u003e {\n    try {\n      setButtonEnabled('btn-connect', false);\n      document.getElementById('btn-connect').textContent = '🔄 Connecting...';\n      \n      // Request port - no filters to show all available ports\n      // This is especially important on Linux where USB permissions may vary\n      port = await navigator.serial.requestPort();\n\n      // Open port\n      await port.open({ baudRate: 115200 });\n      \n      console.log('Port opened:', port.getInfo());\n      \n      showStatus('connect-status', '✅ Connected to T-Watch S3', 'success');\n      setButtonEnabled('btn-connect', false);\n      setButtonVisible('btn-disconnect', true);\n      setButtonEnabled('btn-flash', checkReadyToFlash());\n      \n      // Show device info\n      document.getElementById('device-info').classList.add('show');\n      document.getElementById('chip-name').textContent = 'ESP32-S3';\n      document.getElementById('chip-variant').textContent = 'QFN56';\n      \n      // Read MAC address\n      console.log('Reading MAC address...');\n      const mac = await readMacAddress();\n      document.getElementById('mac-address').textContent = mac;\n      console.log('MAC address:', mac);\n\n    } catch (error) {\n      console.error('Connection error:', error);\n      showStatus('connect-status', `❌ ${error.message}`, 'error');\n      port = null;\n      setButtonEnabled('btn-connect', true);\n      document.getElementById('btn-connect').textContent = 'Detect \u0026 Connect';\n    }\n  });\n\n  // Disconnect button\n  document.getElementById('btn-disconnect').addEventListener('click', async () =\u003e {\n    if (port) {\n      try {\n        await port.close();\n      } catch (e) {\n        console.error('Close error:', e);\n      }\n    }\n    port = null;\n    setButtonEnabled('btn-connect', true);\n    setButtonVisible('btn-disconnect', false);\n    setButtonEnabled('btn-flash', false);\n    document.getElementById('device-info').classList.remove('show');\n    showStatus('connect-status', '⚪ Disconnected', 'info');\n  });\n\n  // Simple helper for MAC address (just show it's connected)\n  async function readMacAddress() {\n    return 'Connected to device';\n  }\n\n  // Flash button - uploads to Flask backend for flashing\n  document.getElementById('btn-flash').addEventListener('click', async () =\u003e {\n    if (!port) {\n      showStatus('flash-status', '❌ No device connected', 'error');\n      return;\n    }\n\n    try {\n      const file = document.getElementById('firmware-file').files[0];\n      if (!file) {\n        showStatus('flash-status', '❌ No file selected', 'error');\n        return;\n      }\n\n      setButtonEnabled('btn-flash', false);\n      document.getElementById('btn-flash').textContent = '⏳ Flashing...';\n      document.getElementById('progress-container').classList.add('show');\n\n      // Get port name from port info\n      const portInfo = port.getInfo();\n      const portPath = portInfo.path || '/dev/ttyUSB0';\n      \n      console.log('Uploading firmware to server for flashing...');\n      console.log('Port:', portPath);\n\n      // Upload file to Flask endpoint\n      const formData = new FormData();\n      formData.append('firmware', file);\n      formData.append('port', portPath);\n\n      const response = await fetch('/flash_watch', {\n        method: 'POST',\n        body: formData\n      });\n\n      const result = await response.json();\n\n      if (result.success) {\n        showStatus('flash-status', '✅ ' + result.message, 'success');\n        document.getElementById('progress-fill').style.width = '100%';\n        document.getElementById('progress-fill').textContent = '100%';\n      } else {\n        showStatus('flash-status', `❌ Flash failed: ${result.error}`, 'error');\n      }\n\n      document.getElementById('progress-container').classList.remove('show');\n\n    } catch (error) {\n      console.error('Flash error:', error);\n      showStatus('flash-status', `❌ Flash failed: ${error.message}`, 'error');\n    } finally {\n      setButtonEnabled('btn-flash', checkReadyToFlash());\n      document.getElementById('btn-flash').textContent = 'Flash Firmware';\n    }\n  });\n\n  // Initial button states\n  setButtonEnabled('btn-connect', true);\n  setButtonVisible('btn-disconnect', false);\n  setButtonEnabled('btn-flash', false);\n});\n\u003c/script\u003e\n\n\n"},{"title":"HandShake T-Watch S3 configuration","date_published":"2026-06-19T00:00:00Z","date_modified":"2026-06-19T00:00:00Z","id":"https://handshaketec.com/watch_configuration/","url":"https://handshaketec.com/watch_configuration/","content_html":"\u003c!-- enable hypermedia by importing from unpkg.com --\u003e\n\u003chead\u003e\n\u003cscript src=\"https://unpkg.com/htmx.org@2.0.2\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003ch1 id=\"introduction\"\u003eIntroduction\u003c/h1\u003e\n\u003cp\u003eHandShake configuration tool for the T-Watch S3.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUnder test - something will need fixing!\u003c/strong\u003e\u003c/p\u003e\n\n\u003cdiv class=\"session-box\" id=\"session-box\"\u003e\n  \u003ch2\u003eYour session name\u003c/h2\u003e\n  \u003cdiv class=\"session-name\" id=\"session-name\"\u003e…\u003c/div\u003e\n  \u003cdiv class=\"name-change\"\u003e\n    \u003cinput type=\"text\" id=\"new-god-name\" placeholder=\"e.g. hermes\" autocomplete=\"off\"\u003e\n    \u003cbutton type=\"button\" onclick=\"changeSessionName()\"\u003eChange\u003c/button\u003e\n    \u003cspan id=\"name-status\"\u003e\u003c/span\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cstyle\u003e\n  .session-box { background: #f0f4ff; border: 1px solid #b0c0e0; border-radius: 6px;\n                 padding: 1em 1.2em; margin-bottom: 1.5em; }\n  .session-box h2 { margin: 0 0 0.4em; font-size: 1em; color: #444; }\n  .session-name { font-size: 1.4em; font-weight: bold; color: #2244aa;\n                  text-transform: capitalize; }\n  .name-change { display: flex; gap: 0.5em; margin-top: 0.7em; align-items: center; }\n  .name-change input { padding: 0.3em 0.5em; border: 1px solid #aaa; border-radius: 4px;\n                       font-size: 0.95em; width: 160px; }\n  .name-change button { padding: 0.3em 0.8em; border: none; border-radius: 4px;\n                        background: #3355cc; color: white; cursor: pointer; font-size: 0.95em; }\n  .name-change button:hover { background: #2244aa; }\n  #name-status { font-size: 0.9em; color: #555; }\n  #name-status.error { color: #cc2222; }\n\u003c/style\u003e\n\n\u003cscript\u003e\n  \n  fetch('/whoami')\n    .then(r =\u003e r.json().then(data =\u003e ({ ok: r.ok, data })))\n    .then(({ ok, data }) =\u003e {\n      const el = document.getElementById('session-name');\n      if (ok \u0026\u0026 data.god_name) {\n        el.textContent = data.god_name;\n      } else if (data.error === 'server_full') {\n        el.textContent = '(server full)';\n        el.style.color = '#cc2222';\n        document.getElementById('name-status').textContent =\n          'All ' + data.max_users + ' sessions are in use — try again later.';\n      } else {\n        el.textContent = '(unavailable)';\n      }\n    })\n    .catch(() =\u003e {\n      document.getElementById('session-name').textContent = '(offline)';\n    });\n\n  function changeSessionName() {\n    const input = document.getElementById('new-god-name');\n    const status = document.getElementById('name-status');\n    const name = input.value.trim().toLowerCase();\n\n    if (!name) {\n      status.textContent = 'Please enter a name.';\n      status.className = 'error';\n      return;\n    }\n\n    status.textContent = 'Changing...';\n    status.className = '';\n\n    const body = new URLSearchParams();\n    body.append('god_name', name);\n\n    fetch('/set_god_name', { method: 'POST', body: body })\n      .then(r =\u003e r.text().then(text =\u003e ({ ok: r.ok, text })))\n      .then(({ ok, text }) =\u003e {\n        if (ok) {\n          document.getElementById('session-name').textContent = text;\n          status.textContent = 'Name changed.';\n          status.className = '';\n          input.value = '';\n        } else {\n          status.textContent = text;\n          status.className = 'error';\n        }\n      })\n      .catch(() =\u003e {\n        status.textContent = 'Network error.';\n        status.className = 'error';\n      });\n  }\n\u003c/script\u003e\n\n\u003ch1 id=\"test-form-is-json-enabled\"\u003eGesture Selection\u003c/h1\u003e\n\u003cform hx-target=\"#gesture_response\" hx-post=\"/submit_gesture\" hx-swap=\"innerHTML\" id=\"gesture_to_detect\"\u003e\n    \u003clabel for=\"punch_gesture\"\u003epunch gesture\u003c/label\u003e\n    \u003cinput type=\"radio\" id=\"option1\" name=\"gesture_to_detect\" value=\"punch_gesture\" placeholder=\"punch\" checked\u003e\u003cbr\u003e\n    \u003clabel for=\"roll_gesture\"\u003eroll gesture\u003c/label\u003e\n    \u003cinput type=\"radio\" id=\"option2\" name=\"gesture_to_detect\" value=\"roll_gesture\" placeholder=\"roll\"\u003e\u003cbr\u003e\n    \u003cinput type=\"submit\" value=\"submit_gesture\"\u003e\n\u003c/form\u003e\n\u003cdiv id=\"gesture_response\"\u003e\u003c/div\u003e\n\n\u003ch1 id=\"gesture-sensitivity-control\"\u003eGesture Sensitivity Control\u003c/h1\u003e\n\u003cform hx-target=\"#sensitivity_response\" hx-post=\"/submit_sensitivity\" hx-swap=\"innerHTML\" id=\"sensitivity_form\"\u003e\n    \u003clabel for=\"gesture_sensitivity\"\u003eGesture Sensitivity (580-900):\u003c/label\u003e\u003cbr\u003e\n    \u003cinput type=\"range\" id=\"gesture_sensitivity\" name=\"gesture_sensitivity\" min=\"580\" max=\"900\" value=\"740\" step=\"10\" oninput=\"this.nextElementSibling.value = this.value\"\u003e\n    \u003coutput\u003e740\u003c/output\u003e\u003cbr\u003e\u003cbr\u003e\n    \u003cinput type=\"submit\" value=\"submit_sensitivity\"\u003e\n\u003c/form\u003e\n\u003cdiv id=\"sensitivity_response\"\u003e\u003c/div\u003e\n\n\u003ch1 id=\"esp-now-mac-address-configuration\"\u003eESP-NOW MAC Address Configuration\u003c/h1\u003e\n\u003cstyle\u003e\n.btn-active {\n    background-color: #4CAF50 !important;\n    color: white !important;\n    border: 2px solid #45a049 !important;\n}\n\n.btn-flash {\n    animation: flash-green 0.5s;\n}\n\n@keyframes flash-green {\n    0%, 100% { background-color: initial; }\n    50% { background-color: #4CAF50; color: white; }\n}\n\u003c/style\u003e\n\n\u003cbutton id=\"esp_now_button\" style=\"padding: 8px 16px;\"\u003eESP-NOW target MAC\u003c/button\u003e\n\n\u003cdiv id=\"mac_address_section\" style=\"display: none; margin-top: 20px;\"\u003e\n    \u003cform hx-target=\"#mac_response\" hx-post=\"/submit_mac\" hx-swap=\"innerHTML\" hx-on::after-request=\"handleMacSubmit(event)\" id=\"mac_form\"\u003e\n        \u003clabel for=\"mac_address\"\u003eMAC Address (6 pairs of hex values):\u003c/label\u003e\u003cbr\u003e\n        \u003cdiv style=\"display: flex; align-items: center; gap: 5px; margin-top: 10px;\"\u003e\n            \u003cinput type=\"text\" class=\"mac-input\" id=\"mac1\" name=\"mac1\" maxlength=\"2\" pattern=\"[0-9A-Fa-f]{2}\" required style=\"width: 35px; text-align: center;\"\u003e\n            \u003cspan\u003e,\u003c/span\u003e\n            \u003cinput type=\"text\" class=\"mac-input\" id=\"mac2\" name=\"mac2\" maxlength=\"2\" pattern=\"[0-9A-Fa-f]{2}\" required style=\"width: 35px; text-align: center;\"\u003e\n            \u003cspan\u003e,\u003c/span\u003e\n            \u003cinput type=\"text\" class=\"mac-input\" id=\"mac3\" name=\"mac3\" maxlength=\"2\" pattern=\"[0-9A-Fa-f]{2}\" required style=\"width: 35px; text-align: center;\"\u003e\n            \u003cspan\u003e,\u003c/span\u003e\n            \u003cinput type=\"text\" class=\"mac-input\" id=\"mac4\" name=\"mac4\" maxlength=\"2\" pattern=\"[0-9A-Fa-f]{2}\" required style=\"width: 35px; text-align: center;\"\u003e\n            \u003cspan\u003e,\u003c/span\u003e\n            \u003cinput type=\"text\" class=\"mac-input\" id=\"mac5\" name=\"mac5\" maxlength=\"2\" pattern=\"[0-9A-Fa-f]{2}\" required style=\"width: 35px; text-align: center;\"\u003e\n            \u003cspan\u003e,\u003c/span\u003e\n            \u003cinput type=\"text\" class=\"mac-input\" id=\"mac6\" name=\"mac6\" maxlength=\"2\" pattern=\"[0-9A-Fa-f]{2}\" required style=\"width: 35px; text-align: center;\"\u003e\n        \u003c/div\u003e\n        \u003cbr\u003e\n        \u003cinput type=\"submit\" value=\"Submit MAC\" id=\"submit_mac_button\" style=\"padding: 8px 16px;\"\u003e\n    \u003c/form\u003e\n    \u003cdiv id=\"mac_response\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\nvar espButton = document.getElementById('esp_now_button');\nvar macSection = document.getElementById('mac_address_section');\n\nfunction toggleMacDisplay() {\n    var section = document.getElementById('mac_address_section');\n    var button = document.getElementById('esp_now_button');\n    \n    if (!section) {\n        return;\n    }\n    \n    if (section.style.display === 'none' || section.style.display === '') {\n        section.style.display = 'block';\n        button.classList.add('btn-active');\n        \n        \n        fetch('/get_mac', {\n            method: 'GET',\n            headers: {\n                'Content-Type': 'application/json',\n            }\n        })\n        .then(function(response) { return response.json(); })\n        .then(function(data) {\n            if (data.mac_address) {\n                var macParts = data.mac_address.split(',');\n                if (macParts.length === 6) {\n                    macParts.forEach(function(part, index) {\n                        var inputField = document.getElementById('mac' + (index + 1));\n                        if (inputField) {\n                            inputField.value = part.toUpperCase();\n                        }\n                    });\n                }\n            }\n        })\n        .catch(function(error) {\n            console.error('Error fetching MAC address:', error);\n        });\n    } else {\n        section.style.display = 'none';\n        button.classList.remove('btn-active');\n        \n        \n        fetch('/delete_mac', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            }\n        });\n    }\n}\n\nif (espButton) {\n    espButton.addEventListener('click', toggleMacDisplay);\n}\n\nwindow.handleMacSubmit = function(event) {\n    if (event.detail.successful) {\n        var submitBtn = document.getElementById('submit_mac_button');\n        if (submitBtn) {\n            submitBtn.classList.add('btn-flash');\n            setTimeout(function() {\n                submitBtn.classList.remove('btn-flash');\n            }, 500);\n        }\n    }\n};\n\u003c/script\u003e\n\n\u003cscript\u003e\n\ndocument.addEventListener('DOMContentLoaded', function() {\n    const macInputs = document.querySelectorAll('.mac-input');\n    \n    macInputs.forEach((input, index) =\u003e {\n        input.addEventListener('input', function(e) {\n            \n            this.value = this.value.toUpperCase();\n            \n            \n            if (this.value.length === 2 \u0026\u0026 index \u003c macInputs.length - 1) {\n                macInputs[index + 1].focus();\n            }\n        });\n        \n        \n        input.addEventListener('keydown', function(e) {\n            if (e.key === 'Backspace' \u0026\u0026 this.value.length === 0 \u0026\u0026 index \u003e 0) {\n                macInputs[index - 1].focus();\n            }\n        });\n    });\n});\n\u003c/script\u003e\n\n\u003ch1\u003eGenerate Firmware\u003c/h1\u003e\n\n\u003cdiv id=\"generate-container\"\u003e\n  \u003cbutton \n    id=\"generate-btn\"\n    hx-post=\"/generate_firmware\"\n    hx-trigger=\"click\"\n    hx-swap=\"none\"\n    class=\"generate-button\"\n    onclick=\"this.classList.add('clicked'); document.getElementById('output-box').style.display='block'; document.getElementById('throbber').style.display='block'; document.getElementById('output-text').textContent='';\"\n    hx-on::after-request=\"if(event.detail.successful) { pollStatus(); }\"\n  \u003e\n    Generate Firmware\n  \u003c/button\u003e\n  \n  \u003cdiv id=\"throbber\" style=\"display:none; margin-top:10px;\"\u003e\n    \u003cdiv class=\"spinner\"\u003e\u003c/div\u003e\n    \u003cp\u003eBuilding firmware, please wait...\u003c/p\u003e\n  \u003c/div\u003e\n  \n  \u003cdiv id=\"output-box\" style=\"display:none; margin-top:10px;\"\u003e\n    \u003cdiv class=\"output-container\"\u003e\n      \u003ch3\u003eBuild Output:\u003c/h3\u003e\n      \u003cpre id=\"output-text\" class=\"output-text\"\u003e\u003c/pre\u003e\n      \u003cbutton id=\"ok-button\" class=\"ok-button\" style=\"display:none;\" onclick=\"document.getElementById('output-box').style.display='none'; document.getElementById('generate-btn').classList.remove('clicked');\"\u003eOK\u003c/button\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cstyle\u003e\n  .generate-button {\n    padding: 10px 20px;\n    font-size: 16px;\n    background-color: #007bff;\n    color: white;\n    border: none;\n    border-radius: 5px;\n    cursor: pointer;\n    transition: background-color 0.3s;\n  }\n  \n  .generate-button:hover {\n    background-color: #0056b3;\n  }\n  \n  .generate-button.clicked {\n    background-color: #28a745;\n  }\n  \n  .spinner {\n    border: 4px solid #f3f3f3;\n    border-top: 4px solid #3498db;\n    border-radius: 50%;\n    width: 40px;\n    height: 40px;\n    animation: spin 1s linear infinite;\n  }\n  \n  @keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(360deg); }\n  }\n  \n  .output-container {\n    padding: 15px;\n    background-color: #f8f9fa;\n    border: 1px solid #dee2e6;\n    border-radius: 5px;\n  }\n  \n  .output-text {\n    padding: 10px;\n    background-color: #1e1e1e;\n    color: #d4d4d4;\n    border: 1px solid #333;\n    border-radius: 3px;\n    max-height: 400px;\n    overflow-y: auto;\n    font-family: 'Courier New', Courier, monospace;\n    font-size: 12px;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n  }\n  \n  .ok-button {\n    margin-top: 10px;\n    padding: 8px 20px;\n    font-size: 14px;\n    background-color: #007bff;\n    color: white;\n    border: none;\n    border-radius: 5px;\n    cursor: pointer;\n  }\n  \n  .ok-button:hover {\n    background-color: #0056b3;\n  }\n  \n  .success-indicator {\n    color: #28a745;\n    font-weight: bold;\n  }\n  \n  .error-indicator {\n    color: #dc3545;\n    font-weight: bold;\n  }\n\u003c/style\u003e\n\n\u003cscript\u003e\n  let pollInterval;\n  \n  function pollStatus() {\n    pollInterval = setInterval(async () =\u003e {\n      try {\n        const response = await fetch('/generate_status');\n        const data = await response.json();\n        \n        \n        const outputText = document.getElementById('output-text');\n        if (data.output) {\n          outputText.textContent = data.output;\n          \n          outputText.scrollTop = outputText.scrollHeight;\n        }\n        \n        if (!data.running) {\n          clearInterval(pollInterval);\n          document.getElementById('throbber').style.display = 'none';\n          \n          \n          document.getElementById('ok-button').style.display = 'block';\n          \n          \n          const statusIndicator = document.createElement('div');\n          statusIndicator.style.marginTop = '10px';\n          \n          if (data.success) {\n            statusIndicator.className = 'success-indicator';\n            statusIndicator.textContent = '✓ Build completed successfully';\n            \n            \n            const downloadContainer = document.getElementById('elf-check-container');\n            if (downloadContainer) {\n              htmx.trigger(downloadContainer, 'refreshDownload');\n            }\n          } else if (data.success === false) {\n            statusIndicator.className = 'error-indicator';\n            statusIndicator.textContent = '✗ Build failed';\n          }\n          \n          \n          const okButton = document.getElementById('ok-button');\n          okButton.parentNode.insertBefore(statusIndicator, okButton);\n        }\n      } catch (error) {\n        console.error('Error polling status:', error);\n        clearInterval(pollInterval);\n        document.getElementById('throbber').style.display = 'none';\n        document.getElementById('output-text').textContent += '\\n\\nError: Failed to retrieve build status';\n        document.getElementById('ok-button').style.display = 'block';\n      }\n    }, 1000); \n  }\n\u003c/script\u003e\n\n\u003ch1\u003eDownload Firmware\u003c/h1\u003e\n\n\u003cdiv id=\"download-elf-container\"\u003e\n  \n  \u003cdiv \n    id=\"elf-check-container\"\n    hx-get=\"/check_elf\"\n    hx-trigger=\"load, refreshDownload from:body\"\n    hx-swap=\"innerHTML\"\n  \u003e\n    \u003cp\u003eChecking for firmware file...\u003c/p\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cstyle\u003e\n  .download-button {\n    padding: 10px 20px;\n    font-size: 16px;\n    background-color: #28a745;\n    color: white;\n    border: none;\n    border-radius: 5px;\n    cursor: pointer;\n    transition: background-color 0.3s;\n    text-decoration: none;\n    display: inline-block;\n  }\n  \n  .download-button:hover {\n    background-color: #218838;\n  }\n  \n  .download-button:active {\n    background-color: #1e7e34;\n  }\n  \n  .file-info {\n    margin-top: 10px;\n    padding: 10px;\n    background-color: #f8f9fa;\n    border: 1px solid #dee2e6;\n    border-radius: 5px;\n    font-size: 14px;\n    color: #495057;\n  }\n  \n  .no-file-message {\n    color: #6c757d;\n    font-style: italic;\n  }\n\u003c/style\u003e\n\n"},{"title":"mattoppenheim","date_published":"0001-01-01T00:00:00Z","date_modified":"0001-01-01T00:00:00Z","id":"https://handshaketec.com/about/","url":"https://handshaketec.com/about/","content_html":"\u003cp\u003eI 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.\u003c/p\u003e\n\u003cp\u003eI 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.\u003c/p\u003e\n\u003cp\u003eMy GitHub page is \u003ca href=\"https://github.com/mattoppenheim\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eemail me at matt@(this page\u0026rsquo;s title).com\u003c/p\u003e\n"},{"title":"Watch Configuration Help","date_published":"0001-01-01T00:00:00Z","date_modified":"0001-01-01T00:00:00Z","id":"https://handshaketec.com/watch_configuration_help/","url":"https://handshaketec.com/watch_configuration_help/","content_html":"\u003cp\u003eThe \u003cstrong\u003eWatch Configuration\u003c/strong\u003e page lets you choose what your T-Watch S3 does, then builds a\ncustom firmware file (the program that runs on the watch) for you to download and\nput onto the device. You don\u0026rsquo;t need any programming knowledge — just work through\nthe page from top to bottom.\u003c/p\u003e\n\u003ch2 id=\"what-youll-need\"\u003eWhat you\u0026rsquo;ll need\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA LilyGo T-Watch S3.\u003c/li\u003e\n\u003cli\u003eA USB cable that can carry data, not just charge. (Many cheap cables only charge — if\nyour computer doesn\u0026rsquo;t notice the watch when you plug it in, try another cable.)\u003c/li\u003e\n\u003cli\u003eA computer running the \u003cstrong\u003eChrome\u003c/strong\u003e or \u003cstrong\u003eEdge\u003c/strong\u003e web browser. The page you\u0026rsquo;ll use to put\nthe firmware onto the watch doesn\u0026rsquo;t work in Firefox or Safari.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"step-1--check-your-session-name\"\u003eStep 1 — Check your session name\u003c/h2\u003e\n\u003cp\u003eAt the top of the page you\u0026rsquo;ll see your \u003cstrong\u003esession name\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eEverybody using the site at the same time gets their own name, so the firmware you build\ndoesn\u0026rsquo;t get mixed up with anyone else\u0026rsquo;s. By default it\u0026rsquo;s the name of a Greek god — there\nare plenty of them, so it makes for an easy way to keep everyone apart!\u003c/p\u003e\n\u003cp\u003eYour firmware file will have this name in its filename, which is how you\u0026rsquo;ll recognise it\nlater. If you\u0026rsquo;d like, type a name of your own (for example, the name of the person who\u0026rsquo;ll\nuse the watch) and click \u003cstrong\u003eChange\u003c/strong\u003e. Otherwise, just leave the name you\u0026rsquo;ve been given.\u003c/p\u003e\n\u003ch2 id=\"step-2--choose-a-gesture\"\u003eStep 2 — Choose a gesture\u003c/h2\u003e\n\u003cp\u003eUnder \u003cstrong\u003eGesture Selection\u003c/strong\u003e, pick the movement the watch should respond to — a \u003cstrong\u003epunch\u003c/strong\u003e\nor a \u003cstrong\u003eroll\u003c/strong\u003e of the wrist. Choose one, then click \u003cstrong\u003esubmit_gesture\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"step-3--set-the-gesture-sensitivity\"\u003eStep 3 — Set the gesture sensitivity\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003eGesture Sensitivity\u003c/strong\u003e slider controls how strong a movement has to be before the\nwatch registers it. Start with the default and click \u003cstrong\u003esubmit_sensitivity\u003c/strong\u003e. You can\nalways come back, build again, and adjust this if you find the watch reacts too easily or\nnot easily enough.\u003c/p\u003e\n\u003ch2 id=\"step-4--set-an-esp-now-target-optional\"\u003eStep 4 — Set an ESP-NOW target (optional)\u003c/h2\u003e\n\u003cp\u003eThis step is only needed if your watch has to send its signal wirelessly to another\nHandShake device. If that applies to you, click the \u003cstrong\u003eESP-NOW target MAC\u003c/strong\u003e button to\nreveal the boxes, type in the six pairs of characters that make up the other device\u0026rsquo;s\naddress, and click \u003cstrong\u003eSubmit MAC\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eIf you don\u0026rsquo;t know what this is, you can safely skip it.\u003c/p\u003e\n\u003ch2 id=\"step-5--generate-the-firmware\"\u003eStep 5 — Generate the firmware\u003c/h2\u003e\n\u003cp\u003eEach time you change one of the controls above, make sure you\u0026rsquo;ve clicked its \u003cstrong\u003esubmit\u003c/strong\u003e\nbutton — this saves your choice in the background.\u003c/p\u003e\n\u003cp\u003eWhen you\u0026rsquo;re happy with all your selections, click \u003cstrong\u003eGenerate Firmware\u003c/strong\u003e. The site reads\nyour choices and builds the firmware file. This usually takes a minute or two, and you\u0026rsquo;ll\nsee progress messages while it works, so please be patient.\u003c/p\u003e\n\u003ch2 id=\"step-6--download-the-firmware\"\u003eStep 6 — Download the firmware\u003c/h2\u003e\n\u003cp\u003eOnce the build finishes, a \u003cstrong\u003edownload\u003c/strong\u003e option appears. Click it to save the firmware file\nto your computer.\u003c/p\u003e\n\u003cp\u003eOnce it\u0026rsquo;s downloaded, the firmware is yours forever! You own it. No time limits, no\nsubscriptions. All yours.\u003c/p\u003e\n\u003ch2 id=\"putting-the-firmware-onto-your-watch\"\u003ePutting the firmware onto your watch\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;m going to add a simple \u0026lsquo;one-click\u0026rsquo; upload page to make this easy. Until then, you can\nuse this firmware upload site:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://thelastoutpostworkshop.github.io/ESPConnect/\"\u003eESPConnect firmware uploader\u003c/a\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ePlug the watch into your computer with a USB data cable.\u003c/li\u003e\n\u003cli\u003eClick \u003cstrong\u003eConnect\u003c/strong\u003e on the top toolbar, then choose the port the watch is connected to.\u003c/li\u003e\n\u003cli\u003eSelect \u003cstrong\u003eFlash Tools\u003c/strong\u003e from the menu on the left.\u003c/li\u003e\n\u003cli\u003eLeave all the default settings as they are — in particular, the \u003cstrong\u003eStart offset\u003c/strong\u003e is\nalready correct at \u003ccode\u003e0x0\u003c/code\u003e, so there\u0026rsquo;s no need to change it.\u003c/li\u003e\n\u003cli\u003eClick the icon next to the \u003cstrong\u003eFirmware binary (.bin)\u003c/strong\u003e box and choose the firmware file\nyou saved.\u003c/li\u003e\n\u003cli\u003eClick \u003cstrong\u003eFlash Firmware\u003c/strong\u003e just below it.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe new firmware will be written to the watch. You may need to disconnect the watch and\nrestart it for the new firmware to run, though sometimes it starts on its own.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDon\u0026rsquo;t panic if something goes wrong.\u003c/strong\u003e If you accidentally flash the wrong firmware to\nthe watch, just reconnect it and flash the correct file again.\u003c/p\u003e\n"}]}