Magic Switch: Deel een Magic Keyboard en Trackpad tussen twee Macs (Gratis)

Ik ben onlangs teruggekeerd naar Polen na ongeveer zeven maanden in Taiwan te hebben gewoond, waar ik heb gefietst, Chinees heb geleerd en ervaring heb opgedaan met dingen die ik nooit had gedacht in mijn leven te doen. Bij mijn terugkeer besloot ik dat het tijd was om toe te geven dat ik niet langer slechts een toerist ben, en ik wilde mijn (huur)appartement er leuk uit laten zien. Ik hou veel van interieurontwerp, maar in de afgelopen vijf jaar dat ik op mijn huidige plek woon, kon ik me — ondanks de lage kosten — niet brengen om te investeren in iets anders dan de essentiële zaken die ik regelmatig zou gebruiken of nodig had. Ik zou uit een rugzak kunnen leven; dat heb ik maandenlang gedaan. Ik kwam in Polen aan met een halve koffer. Ik heb niets anders nodig dan het hoognodige om te leven. De meeste van mijn bezittingen zijn niet waardevol, maar de sentimentele waarde maakt ze onbetaalbaar. Het is niet zo dat mijn woning niet mooi was; sterker nog, van de vele mensen die ik via Couchsurfing heb gehost, gaven ze allemaal aan dat ze de manier waarop ik de woning had ingericht en de spullen had gekozen, mooi vonden. Het waren grotendeels basisbehoeften.

Dus ik kwam terug in Polen en heb mijn appartement volledig vernieuwd. Taiwan inspireerde me enorm; ik verhuisde elke week of twee, en de plekken waar ik verbleef, de plekken die ik verkende en de galerieën, studio's en musea die ik bezocht, lieten hun sporen na. In essentie ben ik een minimalist, maar het plezier in het leven en het gemak hebben uiteindelijk de overhand gekregen: misschien word ik gewoon oud. Als ik hier binnenkort wordt weggejaagd, zal ik verslagen zijn, want mijn harde werk om kleuren te kiezen die matchen, materialen die passen en moeilijk te vinden ontwerpen en items, passen waarschijnlijk simpelweg niet in de volgende plek waar ik woon; de tegels op de keukenmuur zullen anders zijn, het meubilair zal een ander palet hebben, of de gordijnen zullen een andere kleur hebben. Vervelend.

Ik besloot voor het eerst in mijn leven een echt bureau te kopen. Normaal gesproken werk ik in bed of lig ik ergens anders. Aan een bureau zitten is over het algemeen een pijnlijke ervaring voor mij en dat is altijd al zo geweest — zitten in het algemeen is extreem pijnlijk; na ongeveer 5 minuten begint mijn rug te spasmeren op een manier die ik nergens anders (regelmatig) heb gevoeld.

Ik heb waarschijnlijk het Ehlers-Danlos syndroom, wat, naast mijn extreme flexibiliteit, waarschijnlijk heeft geleid tot rugproblemen waar geen arts, chirurg, fysiotherapeut of "helpende vrienden" mee konden helpen. De enige keer dat ik een vergelijking vond voor hoe die pijn voelt, was in Orwells 1984:

"[..] zijn lichaam werd uit vorm getrokken, de gewrichten werden langzaam uit elkaar getrokken [..]"

Dus, ik heb mezelf een zit-sta bureau cadeau gedaan!

Ja, de blogpost van vandaag is een mix van storytelling en technische details. Heb geduld met me, we hebben allemaal onze demonen.

Waarom je geen Magic Keyboard kunt delen tussen twee Macs

Naast het bureau heb ik ook een monitor gekocht, wat betekende dat het tijd was voor een toetsenbord. Ik koos voor de gratis optie: een Apple Magic Keyboard en Magic Trackpad. Deze twee zijn Bluetooth-apparaten die een initiële koppeling via USB-C vereisen.

Blijkbaar heeft Apple, in al hun "it just works"-wijsheid, ervoor gezorgd dat deze randapparaten niet gelijktijdig met meerdere apparaten kunnen worden verbonden. In tegenstelling tot Apple's AirPods, die je met meerdere apparaten tegelijk kunt verbinden en eenvoudig kunt overschakelen naar elk apparaat wanneer je ze wilt gebruiken, weigeren het Magic Keyboard en de Magic Trackpad simpelweg om met meerdere apparaten tegelijk gekoppeld te zijn. Ze met meer dan één computer gebruiken is extreem pijnlijk. Sterker nog: je kunt de randapparaten niet gewoon ontkoppelen van het apparaat dat ze gebruikt en ze verbinden met het andere apparaat; nee, je moet ze via USB aan het andere apparaat koppelen om een volledige her-koppeling uit te voeren. In feite zijn de randapparaten erg "plakkerig".

Je kunt twee apparaten hebben die gekoppeld zijn aan één randapparaat, maar succes met het krijgen van het randapparaat om verbinding te maken met het tweede apparaat: je kunt het eerste apparaat uitschakelen of Bluetooth verbreken; het werkt gewoon niet.

Dit is een bekend probleem. Er is in werkelijkheid een manier om de randapparaten tussen apparaten te delen, maar dat vereist dat je op beide apparaten bent ingelogd met hetzelfde Apple-account. Je gaat naar Instellingen -> Scherm -> Geavanceerd -> Koppelen aan Mac of iPad, en op die manier kan het werken. Dat past niet bij mijn gebruiksscenario, omdat ik me niet wil (en in sommige gevallen niet kan) aanmelden bij mijn Apple-account.

Blue-switch

Er is een betaald product in de Apple Store dat claimt hierbij te helpen. Ik vond open-source software genaamd blue-switch, die een zeer eenvoudige menu-bar UI had waarmee een randapparaat van het ene systeem kon worden losgekoppeld en met een ander kon worden verbonden. In essentie werkte het als volgt:

  1. Via mDNS onderhandelen twee apparaten een verbinding met elkaar.
  2. Beide apparaten kunnen verschillende commando's verzenden/ontvangen, bijvoorbeeld: "ONTKOPEL VAN RANDAPPARAAT AA:BB:CC..".
  3. Het apparaat zal vervolgens een verbinding met het randapparaat forceren.

In feite was het een hack rond het ontwerp van de Magic-apparaten; je forceert de "re-pair dance" zodat deze met één klik gebeurt, in plaats van een USB-kabel te gebruiken voor de her-koppeling en te worstelen met de Bluetooth-instellingen.

Het probleem met blue-switch was dat het slechts een zeer eenvoudig bewijs van concept (proof of concept) was, iedereen op het lokale netwerk vertrouwde en de UX nagenoeg afwezig was (en zelfs geen mogelijkheid had om fouten af te handelen). Volgens Claude deed de originele app het volgende:

  • Ontdekking via mDNS/Bonjour — twee Macs die de app draaien vinden elkaar op het lokale netnetwerk en wisselen routeringsinformatie uit (host + poort).
  • Een plaintext TCP-controlekanaal — zodra ze elkaar hebben gevonden, opent elke Mac een NWConnection naar de andere en verzendt korte string-commando's: CONNECTALL, UNREGISTERALL, NOTIFICATION, SYNCPERIPHERALS, HEALTHCHECK, plus OPSUCCESS / OPFAILED bevestigingen.
  • De kern van de Bluetooth-truc — bij ontvangst van CONNECTALL gebruikt een Mac IOBluetooth om de Magic-randapparaten geforceerd aan zichzelf te koppelen/verbinden; bij UNREGISTERALL laat hij ze los.
  • Een basis menu-bar UI — een statusitem met een lijst van geregistreerde randapparaten en de andere Mac, en een instellingenvenster met een paar tabbladen.

Zonder Claude had ik geen manier gehad om te weten hoe dit werkte (behalve de code lezen en hopen dat ik de macOS Bluetooth-glue begrijp). Mensen behandelen deze LLM-coding agents als slaven, maar duidelijk ben ik een slaaf van de LLM.

Dat was dus alles wat blue-switch was. Het had geen authenticatie, encryptie, geen idee van welk apparaat de andere Mac daadwerkelijk was, en geen herstel bij fouten (wat soms onvermijdelijk lijkt). Samen met mijn meester, Claude, heb ik een veiligere en veerkrachtigere applicatie "vibe-coded", met een proper UI/UX en een algoritme om deze apparaten nooit "uit het oog te verliezen".

Magic Switch: een gratis alternatief

Ik heb het Magic Switch genoemd. De broncode is beschikbaar op GitHub, evenals een immutable build, gebouwd met GitHub runners.

Beveiliging

Voor de beveiliging heb ik gekozen voor:

  • Een wederzijds geauthenticeerd, verzegeld kanaal, versleuteld met een 12-tekens lange koppelingscode die buiten bandbreedte (out-of-band) tussen de twee Macs wordt gedeeld.
  • Met de koppelingscode stellen we een PSK (Pre-Shared Key) in via PBKDF2-HMAC-SHA256 (met 600k iteraties), en slaan we deze op in de macOS Keychain.
  • Er is ook een per-verbinding handshake: elke zijde verzendt een 32-byte nonce, en beide apparaten leiden richtingsspecifieke sessiesleutels af met HKDF. Om het nog leuker te maken, bewijzen we het bezit van de sleutel met een HMAC over het transcript (met client/server-roltags om te voorkomen dat een pakket simpelweg wordt gereflecteerd, om zo een server-naar-server aanval te voorkomen). Claude was vriendelijk genoeg om me te vertellen dat de functie die ik oorspronkelijk had bedacht niet constant-time-safe was (alsof dat uitmaakt, lol).
  • Berichten tussen apparaten zijn geformatteerd als ChaCha20-Poly1305 sealed boxes met monotone counter nonces, zodat replay-aanvallen niet kunnen plaatsvinden.
  • Omdat we 12-tekens lange koppelingscodes gebruiken, beperken we het aantal cryptografische fouten tot 5 per 60 seconden: als die limiet wordt bereikt, blokkeren we het apparaat voor 15 minuten.
  • De verbindingen zelf zijn verhard: 30s idle / 5-min totale budgetten, per-verbinding status (waarmee de racy shared ConnectionManager is vervangen), en IP-canonicalisatie zodat een aanvaller zijn budget niet kan verdubbelen door af te wisselen tussen IPv4/IPv6.

Men kan zich afvragen waarom ik al deze moeite heb gedaan voor een eenvoudige LAN-verbinding die de andere client simpelweg vertelt: "verbind met dit Bluetooth-apparaat; ontkoppel van dat Bluetooth-apparaat". In werkelijkheid is het omdat ik dingen wilde leren. En plezier wilde hebben.

Het overdrachtsproces soepeler maken

Wat betreft de feitelijke functionele wijzigingen, heb ik een reeks nieuwe zaken toegevoegd om de app en de overdracht soepeler te maken, zodat het gewoon werkt.

Naast het aanvragen van een randapparaat is het mogelijk om randapparaten naar het andere apparaat te sturen. Voordat er enige Bluetooth-status wordt gewijzigd, pingt het systeem de andere Mac via het beveiligde kanaal om te bevestigen dat deze de overdracht daadwerkelijk zal accepteren (het openstaan van TCP is niet genoeg, omdat het systeem in typische macOS-stijl half in slaap kan zijn). Als de overdracht halverwege mislukt, worden de randapparaten lokaal opnieuw verbonden, zodat we nooit eindigen met randapparaten die op geen enkel systeem zijn verbonden.

De koppelingsflow komt overeen met die van andere macOS-systemen (zoals AirPlay?). In principe genereert één systeem een 12-tekens lange koppelingscode, wat een gedeelde vingerafdruk (fingerprint) genereert die op beide systemen wordt getoond ter bevestiging.

Om vóór de authenticatie met het andere systeem te kunnen communiceren, pinnen beide Macs de vingerafdruk van de peer de eerste keer dat ze deze zien. Een latere, afwijkende vingerafdruk wordt geweigerd en gemarkeerd als een "Identity Mismatch". Hiermee wordt dus ook een vorm van impersonatie van een ander systeem (authenticatie, niet encryptie!) opgemerkt.

De originele app stuurde al je randapparaten naar het andere apparaat met één druk op de knop. Ik heb het mogelijk gemaakt om een enkel apparaat te sturen, aan te vragen of te ontvangen.

Koppelen

Het koppelen van twee Macs werkt veel zoals het koppelen van de meeste Apple-producten: één genereert een code, de ander typt deze in, en vervolgens tonen beide een overeenkomende vingerafdruk die je visueel kunt controleren.

  1. Genereer een 12-tekens lange code op één Mac.
  2. Voer deze in op de andere Mac.
  3. Zodra ze gekoppeld zijn, tonen beide Macs een overeenkomende vingerafdruk — een mismatch betekent dat er iets mis is.

Slaapstand, clamshell-modus en andere irritaties

De echte problemen ontstaan wanneer we dingen werkend proberen te krijgen bij slaapstand, clamshell-modus (klep dicht maar verbonden met een monitor) en andere irritante netwerkzaken. Ik heb enkele nuttige configureerbare functies bedacht:

  • Bij het in slaap vallen van het systeem proberen we de randapparaten over te dragen aan óf 1) de andere Mac, óf 2) "de wereld". Met "de wereld" bedoel ik dat we ze simpelweg ontkoppelen en verder niets doen.
  • Deze Magic-apparaten raken vaak "vast" zodra de radio in slaap valt en verbinden pas opnieuw nadat je ze hebt herstart (power-cycle). Ik heb het programma zo gemaakt dat het let op alles wat op deze Mac was voordat hij in slaap viel, en dit onmiddellijk opnieuw verbindt zodra het weer verschijnt (bijv. na een power-cycle); maar dit gebeurt alleen na het bevragen van de andere Mac om te bevestigen dat deze ze niet gebruikt (zodat de twee Macs niet om het apparaat vechten).

Wie is nu eigenlijk de eigenaar van een randapparaat?

Achter al deze overdracht-zaken staat in feite één vraag: "wie is nu eigenlijk de eigenaar van een randapparaat?". Dit wordt nergens in de applicatie "opgeslagen". In plaats daarvan beschouwen we "het bezitten van een randapparaat" simpelweg als het hebben van een actieve Bluetooth-verbinding ermee. De app stelt dit elke keer vast door:

  1. Lokaal aan Bluetooth te vragen.
  2. Aan het verbonden andere systeem te vragen: "Houd jij dit nu vast?".

Van daaruit wordt bepaald wie het randapparaat krijgt op basis van een paar scenario's:

  • Klikken op een Mac (verplaats alles): Er wordt gekeken naar wat deze Mac vasthoudt. Als we ze allemaal hebben, zijn we de verzender — ping het andere systeem om te controleren of het ze accepteert, ontkoppel lokaal, en vertel het andere systeem ze te pakken (met rollback bij fouten). Als we er geen hebben, zijn we de ontvanger — vertel de andere Mac eerst om los te laten, en verbind dan lokaal. Als het een mix is (sommige hier, sommige daar), weigert het programma en vraagt het je dit eerst handmatig op te lossen in plaats van te gokken.
  • Klikken op een enkel randapparaat: Dezelfde logica, maar dan alleen voor één apparaat — op deze Mac: verzenden; niet op deze Mac: aanvragen; tijdens een verbinding: negeer de klik.
  • Automatisch iets terugpakken (na een drop, wake, of power-cycle): Dit is een vervelende edge case waarbij twee Macs over een apparaat kunnen gaan vechten. Voor het opnieuw verbinden vraagt het programma aan de andere Mac of deze het daadwerkelijk gebruikt — zo ja, dan laten we het met rust; zo nee (of als er geen antwoord komt), dan proberen we het te pakken. Deze route vertelt de andere Mac dus niet om te ontkoppelen, maar pakt alleen iets op dat niemand vasthoudt.

Over het algemeen is er meer logica, vooral over:

  • Geen enkel apparaat mag "nergens" landen. Als een overdracht om een bepaalde reden mislukt, probeert de app terug te keren naar de staat waarin hij begon (dus: herconnecteer wat zojuist is ontkoppeld).
  • "Onbereikbaar" betekent verschillende dingen per richting: als ik een randapparaat aanvraag en de andere Mac antwoordt niet, ga ik ervan uit dat hij het al heeft losgelaten (een Mac met deze app die van het netwerk valt, heeft de randapparaten namelijk automatisch losgelaten) en pak ik het apparaat. De app zal nooit geforceerd proberen een randapparaat weg te trekken van een Mac die wakker is en antwoordt.

De gebruikersinterface

Voor de UI heb ik een veel gepolijstere weergave en een menu-bar popdown gemaakt.

Het menu-bar popdown-menu

In het popdown-menu kun je op een Mac klikken om alles te verplaatsen, of op een enkel randapparaat om alleen dat apparaat te verplaatsen; een vinkje betekent dat het op deze Mac staat.

Het icoon dat voor het popdown-menu wordt gebruikt, hangt af van de status:

  • Driehoek: Bluetooth is uit of niet gekoppeld aan randapparaten.
  • Op/neer-transferpijlen: Het systeem is respectievelijk een randapparaat aan het verzenden of ontvangen.

Het popdown-menu controleert elke 24 uur op updates (met een rollende check elk uur totdat de check slaagt). Het update niet automatisch.

Achter het menu zit een klein instellingenvenster met een tabblad per taak:

  • Peripheral tab: registreer een Magic-apparaat, stuur het terug of stop met het beheren ervan.
  • Device tab: ping de andere Mac, deel randapparaten ermee, of vergeet de Mac.
  • Other tab: start op bij login, licentie-informatie en de handmatige update-check.

Het popdown-menu in de menu-bar was het moeilijkste om precies zo te krijgen als ik wilde. Ik had de volgende eisen:

  • Wanneer je het popdown-menu opent, mag het bewegen van de muis over het scherm het menu niet minimaliseren (de enige manier om het menu te sluiten is door iets aan te klikken).
  • Het bewegen van de muis terwijl het menu open is, mag de menu-bar niet laten verdwijnen wanneer een applicatie in volledig scherm wordt gebruikt.
  • Alle knoppen en belangrijke tekst moeten nuttige hover/tooltip-hints hebben.
  • Het openen van de instellingen vanuit het popdown-menu moet het venster op het bureaublad openen en je automatisch naar dat scherm verplaatsen.
  • Het indrukken van Cmd-Q (exit) mag de applicatie niet afsluiten, maar alleen de instellingen (of het instellingenmenu).
  • Het popdown-menu moet automatisch updaten, zelfs als het al open is, wanneer er informatie wijzigt.
  • Alle iconen moeten uitgelijnd zijn, zonder dat er een enkele pixel afwijkt, op een logische en gecompartimenteerde manier.

Ik stel me voor dat niemand anders dit ooit echt zal gebruiken: ik zou het zeker niet aanraden! Desalniettemin was het leuk om te maken.