diff --git a/README.md b/README.md index fce603b..344d842 100644 --- a/README.md +++ b/README.md @@ -4,188 +4,36 @@ Email: neale@woozle.org License: MIT --- -# Project: USB Morse Adapter +# Vail Adapter: Morse Code Key/Paddle to USB -![Vail adapter, assembled and connected](https://lh3.googleusercontent.com/pw/ACtC-3d9xbLxL23QeLm-3gy3-Yt0VHE3IlQ-qyMDqTfdF6Bo7fHkkokACdIs68pmXevu14VzrrCeKj1JmRUiekUNiZe9J9rYIh_pTagvCbKSzpY8Ynp1m6cF4G_jTvtiU5eRtoNCsmU5OLy2SR9kYcCDYSt-AA=s1471-no) - -This translates Morse key inputs into USB events, -either MIDI or MIDI+Keyboard, -so you can use a computer. - -It is fully compatible with [VBand](https://hamradio.solutions/vband/) -and anything else that uses the VBand adapter. - -I use this with an Internet morse code repeater I wrote, -available at https://vail.woozle.org/. - -This project requires an Arduino that can send USB. -The following should work: - -### Verified Working - -* Seeeduino Xiao **(recommended)** -* Arduino Micro -* KeeYees Pro Micro - -### Unverified but should work - -Any USB-capable Arduino should be fine, including: - -* Arduino Leonardo -* Arduino Zero -* Adafruit Trinket M0 -* Adafruit GEMMA M0 -* Adafruit Feather M0 - -It may also be possible to get this working on a DigiSpark. -I'm trying to convince Michele Giugliano to try :) - -=== Similar projects - -* Vail user Michele Giugliano's [MorsePaddle2USB](https://github.com/mgiugliano/MorsePaddle2USB), - which runs on a DigiSpark (attiny85). It only sends keystrokes, so you must keep the Vail - window focused: you can't switch to other apps and still transimit. -* Ham Radio Solutions sells a [USB Paddle Interface](https://hamradio.solutions/vband/) - which appears to be very similar to Michele's project. You must keep the Vail window focused. - -## Step 1: Installation - -#### The Easy Way - -1. Get a Seeeduino XIAO -2. Download the most recent xiao firmware from -[releases](https://github.com/nealey/vail-adapter/releases) -3. [Enter bootloader mode](https://wiki.seeedstudio.com/Seeeduino-XIAO/#enter-bootloader-mode) -4. Copy the firmware onto the XIAO - -A longer write-up is [on the wiki](https://github.com/nealey/vail-adapter/wiki/Flashing-firmware). - -#### The Other Way - -Install the MidiUSB and Keyboard libraries. -These are highly popular libraries, -and there is much better documentation elsewhere on the Internet. -The code should then build in the Arduino IDE. - -## Step 2: Assmble the circuit - -### For the impatient: - -[Photos of assembled circuit](https://github.com/nealey/vail-adapter/wiki) - -### For the inquisitive: - -Morse code keyers are very simple devices, -they just connect two wires together. -You could use a button if you wanted to, -or even touch wires together. - -The only real complication here is that some browsers -need to get keyboard events instead of musical instrument events. - -The Vail adapter boots into a mode that sends both keyboard events -and MIDI messages. -If it receives a MIDI key release event -on channel 0 -for note C0, -it will disable keyboard events. - -Vail sends this "disable keyboard" MIDI event, so as soon as you -load up Vail, the keyboard events are disabled, and your adapter -will no longer interfere with your typing. -If your browser doesn't support MIDI, -the disable command can't be sent, -and it keeps on sending keystrokes. - -### Wire up your input device - -Hook a straight key into ground on one side, -pin 10 on the other. -It's okay to leave pin 10 disconnected if you don't have a straight key. - -Hook an iambic paddle in ground in the middle, -pins 11 and 12 on the outside posts. -It's okay to leave pins 11 and 12 disconnected if you don't have a paddle. - -=== Using a headphone jack - -If you prefer, you can wire a headphone jack up to GND, 11, and 12. -GND should be the sleeve, 11 the ring, and 12 the tip. - - o --- 12 - |_| --- 11 - | | --- GND - | | - -Make sure any straight key you plug in has a TS adapter (mono plug): -this will short pin 11 to ground and signal to the Arduino to -go into straight key mode. - -If you change from an iambic key to a straight key, -you'll have to reset the adapter by unplugging it from the computer. - -### Optional: buzzer - -If you connect a buzzer or speaker to pin 7 on one leg, -and ground on the other, -the adapter will beep when you press the straight key. - -This will help a lot if there is a noticeable delay between when you press the key -and when your computer starts making a local beeping sound. - -If you feel like no matter what you do, -you're always getting DAH with your straight key, -you should try this. - -## Step 3: Load the code - -Upload the code contained in this sketch on to your board. - -## Step 4: Test it out - -Make sure it's plugged in to your computer's USB port. - -If you connected pin 9 to ground, -Open anything where you can type, -type in "hello", and hit the straight key. -You should see a comma after your hello. - -Now you can open https://vail.woozle.org/, -click the "KEY" button once to let the browser know it's okay to make sound, -and you should be able to wail away on your new USB keyer. - -## Step 5: Debugging - -If you plug in your straight key and it looks like DAH is being held down, -it means you need to switch the connections to pins 11 and 12. - -If you plug in your straight key and it looks like DIT is being held down, -it means you need to reset the Arduino to make it detect the key type again. +![Vail adapter, assembled and connected](doc/vail-adapter-v2.jpg) -# License +# Features -This project is released under an MIT License. +* Lets you key even if you move focus to another window +* Works with [Vail](https://vail.woozle.org/) +* Works with [VBand](https://hamradio.solutions/vband/) +* Optional sidetone generator for straight keying, which helps with latency +* Free firmware updates for life +* Can be wired up in about 5 minutes -Copyright © 2020 Neale Pickett -Copyright © 2013 thomasfredericks +[Vail Adapter benefits video](https://www.youtube.com/watch?v=XQ-mwdyLkOY) (4:46) -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +# Setting Up -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +* [Easy Setup](doc/easy-install.md) +* [Advanced Setup](doc/advanced-install.md) -The software is provided "as is", without warranty of any kind, express or -implied, including but not limited to the warranties of merchantability, fitness -for a particular purpose, and noninfringement. In no event shall the authors or -copyright holders be liable for any claim, damages, or other liability, whether -in an action of contract, tort or otherwise, arising from, out of, or in -connection with the software or the use or other dealings in the software. + +# Future Work + +Things I plan to add: + +* Local keyer logic for Ultimatic, Iambic, etc. +* Local keyer generating sidetones +* Vail site sends MIDI note events to the adapter, + so you don't need your computer speaker on to listen. # Contributing @@ -193,15 +41,16 @@ To contribute to this project please contact neale@woozle.org https://id.arduino.cc/neale -# BOM -In addition to a key, some hookup wires, and a USB cable, -you only need a USB-capable Arduino: see above. +# Similar projects -Since I don't know what might try to parse this section, -I'm calling for an Arduino Micro. But, really, many options -will work fine. - -| ID | Part name | Part number | Quantity | -| --- | ------- | ------ | ------ | -| A1 | Seeeduino XIAO | 102010328 | 1 | +* Vail user Michele Giugliano's + [MorsePaddle2USB](https://github.com/mgiugliano/MorsePaddle2USB), + which runs on a DigiSpark (attiny85). It only sends keystrokes, so you must keep the Vail + window focused: you can't switch to other apps and still transimit. +* Ham Radio Solutions sells a + [USB Paddle Interface](https://hamradio.solutions/vband/) + which appears to be very similar to Michele's project. You must keep the + Vail window focused. +* [CWKeyboard](https://github.com/kevintechie/CWKeyboard) looks almost + exactly the same as the VBand adapter. diff --git a/case/adapter-case.scad b/case/adapter-case.scad new file mode 100644 index 0000000..7f66abe --- /dev/null +++ b/case/adapter-case.scad @@ -0,0 +1,16 @@ +union() { + import("seeed_xaio_case.stl", convexity=3); + translate([-12.1, -2, 6]) cube([1.3, 4, 4]); // Plug up the component hole in the back + + // Fill in the curved sides in front + translate([-13.5, -10.485, 0]) cube([5, 1.30, 10]); + translate([-13.5, 9.185, 0]) cube([5, 1.30, 10]); + + // Big block for key + translate([-40, -10.485, 0]) cube([29, 10.485*2, 10]); + + difference() { + translate([-40, -10.485, 0]) cube([28, 10.485*2, 30]); + } + +} diff --git a/case/case.stl b/case/case.stl new file mode 100644 index 0000000..b1bd881 Binary files /dev/null and b/case/case.stl differ diff --git a/case/paddles.scad b/case/paddles.scad new file mode 100644 index 0000000..3fda4e9 --- /dev/null +++ b/case/paddles.scad @@ -0,0 +1,521 @@ + + +/* + >>> COMPACT 3D-PRINTED PADDLES <<< + Torbjørn Skauli, LA4ZCA (tskauli@gmail.com) + v2.0, December 2018 +Iambic paddles designed for 3D-printing. The design is simple, but provides precise movement with adjustable force and travel. Design features include a printed rocker hinge, force adjustment by a sliding spring, travel adjustment using a modified screw, ergonomic grip and general simplicity and precision. +Changes in v2.0: The paddles have been made narrower, and the base is thinner. There is an option to remove the bottom mounting holes. The base extends under the paddles to protect them from being pushed upwards. The cable exit is on the side, to fit mounting on the QCX transceiver. +TODO: +-Arms 1-2 mm higher, spring channel longer towards contacts? +Materials: +- 3 printed parts +- 2 screws M3x5mm, cylinder head, with washers for adjustment if needed +- 1 screw M4 x 18-20mm, cylinder head, with lockwasher and nut +- Compression spring, 6-8 mm in diameter +- Compression spring, 4-5 mm in diameter +- Cable with plug as required, up to 3.5 mm diameter +Note: Nickel plated brass screws have been found to give the most reliable contact operation. Dimensions of screws, springs and cable can be changed in the code. +Assembly: +- First, prepare the 3D-printed parts by removing support material in the arm spring well and in the ends of the cable holes. Also remove any protuding edges and bumps by gently filing the surfaces. +- Place the large spring so that it is held between the paddles approximately in the middle of the spring well. Also place the small spring in the holes at the hinge. Temporarily slide the two paddles in place. Check the spring force on the paddles and adjust as desired by either moving the spring along the well or by bending the spring to change its length. Make sure that the small spring keeps the arms in place at the hinges during use. +- Remove 6-8 cm of the outer isolation (if present) of the cable and 1 cm of the inner isolation of each wire. Insert the cable from the back through the diagonal hole, and temporarily pull it out from the side "window". Insert the cable back into the other hole and press the cable bend into the window so that the outer isolation ends in the interior wiring well. This forms a strain relief. +- Prepare a 18-20 mm M4 screw with cylindrical head by grinding the outer 5 mm to flatten two opposing sides. Preferably align the flattened screw end with the slot in the screw head. +- Enter two M3 screws with cylindrical heads into the paddle arms, with the heads facing inwards. Clamp the dot and dash wire ends under these screw heads. +- Enter the M4 screw from the bottom and clamp the ground wire underneath the lockwasher. Tighten the nut quite firmly, while allowing a small amount of adjustment of the screw angle to set the travel distance. +- Place the spring between the paddles and slide them in place. Adjust travel by rotating the M4 screw. If the travel is asymmetric, it may be necessary to correct the difference by placing a washer under the head of one of the M3 screws in the paddle arms. +*/ + +//*************** Rendering output control +mountholes=true;//Whether to have mounting holes in bottom +cacc=4; // accuracy of circles, multiplier for $fn. Use cacc=1 for dev/debug, =2-4 for final. +preview=0; // =0 for print layout, + // =1 or =2 for 1- or 2-arm assembly preview, + // =3 for base only + // =4 for arms print layout, + // =5 base+attachment preview + // =6 base modified for attachment to other cabinet + // =7 attachment for inclusion in other cabinet + // =8 rotation stopper for inclusion in other cabinet + +//**************** main parameters of the design +wxbase=24; // overall width, sets arm thickness etc. +lybasemin=40; // length excl. knobs (normally 40, made longer below for QCX) +hzarm=20; // height of main part of arm +wallt=2; // wall thikckness +wallmin=2; // min wall thickness under cable holes + +dfinger=30; // approx diameter of finger for curved knob. Also knob length. +lyknob=25; // total length of knob +rround=1; // radius of rounded edges on knob +txknobmin=2; // min. thickness of knob + +yhinge=6.5; // y position of hinge relative to back +dxwedge=4; // height of hinge wedge +ahinginn=50; // angles for inner and outer part of hinges +ahingout=80; +dstopper=1.5; // diameter of stopper on top of hinge that keeps arm down + +armsep=0.75; // arm separation from all walls +dyarmrests=2; // y width of resting and bounding surfaces for arms +minstroke=0.25; // minimum stroke length (at full dia of center screw) + +//**************** parameters for non-printed parts +dscrew1=4; // screw dia, also scales screw head height +dscrew2=3; // screw dia, also scales screw head height +hscrhrel=2/3; // screw head height and diam rel to diameter +dscrhrel=7/4; +dzcontact=0.1; // extra height of all screws +dzsprwell=-1; // height adjustment of spring well +dcable=4; // cable diameter +dspring=9.5; // spring diameter +dhingespring=5; // diameter of spring keeping hinge in place + +//*************** Parameters for cabinet attachment, needed to make integrated paddle +daxis=6; // Diameter of rotation axis tube +lyflange=22; // Length and height of flange on axis tube +hzflange=13; +atthick=1.5; +rotsnaph=1; // Height of snaps for paddle attachment rotation +rotstop=7; // Size of rotation stopper shelf + +//**************** parameters for rendering +gap=0.2; // gap for loose fit +tol=0.025; // general tolerance +nil=0.001; // Negligible distance, to correct rendering + + +lybase=lybasemin; // length of basee ex. knobs +echo("Length of base (mm):",lybase); +hzwall=hzarm+armsep; // total height of walls +hzbase=2*wallmin+dcable; // height of base under arms +ycontact=lybase-1*dscrew1; // position of contact screw + +wxarm=wxbase/2-wallt-armsep-hscrhrel*dscrew2-minstroke-dscrew1/2; // arm thickness is the remaining space after removing many contributions to total width +echo("Total height (mm):",hzwall+hzbase); +echo("Total width for QCX (mm):",wxbase+dcable); +echo("Max length of contact screws in arms (mm):",wxarm); +echo("Min length of contact screw in base (mm):",hzbase-hscrhrel*dscrew1+armsep+hzarm/2+dscrew2*dscrhrel/2); +echo("Min length to flatten contact screw in base (mm):",dscrew2*dscrhrel); +wxknob=wxarm+wallt+armsep+hscrhrel*dscrew2; +dxknob=hscrhrel*dscrew2; // x offset toward center rel to main arm +hzknob=hzarm; // height of knobs +lyarm=lybase-wallt-armsep/2; // knob spaced armsep/2 from base front + +y0sprwell=yhinge-wallt-armsep+dxwedge*tan(ahingout/2); // starting pos of spring channel +sprfloort=wallt; // thickness of floor underneath spring +sprlen=wxbase-2*(wallt+armsep+sprfloort); // length of spring + +dxtip=wxbase/4; // diameter of rounded tip with attachment hole +dytip=(wxbase-dxtip)/2; // length of tip ex rouned part +rtip=dxtip*sqrt(2)/2; // radius of tip +ycentip=lybase+dytip-rtip/sqrt(2); // center of rounded tip + +module teardropHole(lh,rh){ // Hole with 45-degree teardrop shape + rotate([90,0,0]) + rotate([0,0,45]) + union(){ + cylinder(h=lh,r1=rh,r2=rh,$fn=8*cacc); + cube([rh,rh,lh]); + }; +}; +module snap45(snaph,snapl){ // bumps to snap parts together, max angle 45 degrees +// snapl is length of bump, passed as parameter to allow tolerance +difference(){ + rsnap=snaph/(1-1/sqrt(2)); + translate([rsnap-snaph,-snapl/2,0]) + rotate([-90,0,0]) + cylinder(h=snapl,r=rsnap,$fn=5*cacc); + translate([nil,-snapl/2,-rsnap]) + cube([2*rsnap,snapl,2*rsnap]); +}; +}; // end snap +module wedge(a,wx,hz){ // Equilateral triangular block + //top angle a(deg), length hz and triangle height wx in x direction + linear_extrude(height=hz) + polygon([[0,0],[wx,wx*tan(a/2)],[wx,-wx*tan(a/2)]]); +}; +module cone45(dc){ // cone for stopper that keeps arm down, 45-degree slope + rotate([180,0,0]) + cylinder(d1=dc,d2=0,h=dc*1.0,$fn=8*cacc); +}; +module wedge_hinge(){ //wedge on wall for hinge, with cutout so that arm rests on top and bottom + // cutout for stopper cone + translate([0,0,hzwall]) + cone45(dstopper); + // wedge with cutout, centered on the edge between cone and armrest, + // to make arm better supported against up-down tilt + difference(){ + wedge(a=ahinginn,wx=dxwedge,hz=hzwall); + translate([-hzarm/2/sqrt(2),hzarm/2,hzarm/2+armsep-dstopper*0.75/2]) + rotate([90,0,0]) + cylinder(h=hzarm,d=hzarm,$fn=12*cacc); + }; +}; +module bump45(bumph){ // Spherical bump with at most 45 degree angle +rotate([0,180,0]) +difference(){ + amax=60; // max overhang angle on bump, not necessarily 45 degerees + rsnap=bumph/(1-cos(amax)); + translate([rsnap-bumph,0,0]) + sphere(r=rsnap,$fn=5*cacc); + translate([nil,-rsnap,-rsnap]) + cube([2*rsnap,2*rsnap,2*rsnap]); +}; +}; +module rotsnaps(snaph){ // Rotation snaps for paddle + rrot=(hzbase+hzwall)/2*sqrt(2)-snaph/(1-1/sqrt(2))-wallt; + for (a=[45:90:360]) + rotate([a,0,0]) + translate([0,rrot,0]) + bump45(snaph); +}; +module base_add(){ // parts of base that add to shape + // base plate + translate([-wxbase/2,0,0]) + cube([wxbase,lybase+lyknob-rround,hzbase]); + + //rounded front + translate([-wxbase/2+rround,lybase+lyknob-rround,0]) + minkowski(){ + cube([wxbase-2*rround,tol,hzbase]); + // rounding cy + cylinder(r=rround,h=tol,$fn=cacc*4); + }; + + // walls + translate([0,0,hzbase]) + difference(){ + translate([-wxbase/2,0,0]) + cube([wxbase,lybase,hzwall]); + translate([-wxbase/2+wallt,wallt,0]) + cube([wxbase-2*wallt,lybase,hzwall+tol]); + } + + //wedge 1 + translate([wxbase/2-wallt-dxwedge,yhinge,hzbase]) + wedge_hinge(); + //wedge 2 + translate([-(wxbase/2-wallt-dxwedge),yhinge,hzbase]) + rotate([0,0,180]) + wedge_hinge(); + + // bottom arm resting surface, height armsep above base top + translate([0,yhinge,hzbase]) + cube([wxbase-2*wallt-2*dxwedge-2,dyarmrests,2*armsep],center=true); + + // front arm lower resting surface, normally with 2*gap airgap + translate([-wxbase/2,lybase-dyarmrests,hzbase]) + cube([(wxbase-2*dscrew1)/2,dyarmrests,armsep-2*gap]); + translate([wxbase/2,lybase,hzbase]) + rotate([0,0,180]) + cube([(wxbase-2*dscrew1)/2,dyarmrests,armsep-2*gap]); + + // outer end stops, 2mm wide + translate([-(wxbase/2-wallt),lybase-dyarmrests,hzbase]) + cube([armsep,dyarmrests,hzwall]); + translate([(wxbase/2-wallt)-armsep,lybase-dyarmrests,hzbase]) + cube([armsep,dyarmrests,hzwall]); + + // extra column for center screw stability + translate([0,ycontact,hzbase]) + cylinder(d=dscrew1*dscrhrel*1.5,h=dscrew1/2+dzcontact,$fn=8*cacc); + + // QCX attachment: add 1x cable dia of wall thickness and rotation axis + if (preview>=5){ + + // Thicker wall + translate([wxbase/2,0,0]) + cube([dcable,lybase,hzbase+hzwall]); + + // Rotation stopper + translate([wxbase/2,lybase,0]) + cube([dcable,rotstop,hzbase]); + + // snaps for paddle rotation + translate([wxbase/2+dcable,lybase/2,(hzbase+hzwall)/2]) + rotsnaps(rotsnaph); + + }; + +}; +module base_sub(){ // parts of base that cut away from shape + + // center contact screw hole + translate([0,ycontact,0]) + cylinder(d=dscrew1,h=hzbase*9,$fn=8*cacc); + + // center contact screw head recess (20% enlarged) filled in by cylinder to avoid need for support + translate([0,ycontact,0]) + cylinder(d=dscrew1*dscrhrel*1.2,h=dscrew1*hscrhrel*1.2,$fn=8*cacc); + + // Front mounting screw hole with recess + if (mountholes && preview<5){ + translate([0,ycentip,0]) + cylinder(d=dscrew1+gap,h=hzbase,$fn=8*cacc); + translate([0,ycentip,wallt]) + cylinder(d=dscrew1*dscrhrel*1.2,h=hzbase,$fn=8*cacc); + }; + + // wire well + wellw=wxbase/2; // wire well width and length + translate([wellw/2,ycontact-2*dscrew1,wallt]) + rotate([0,0,180]) + cube([wellw,wellw,hzbase]); + + // Back mounting screw hole in well + if (mountholes && preview<5){ + translate([0,ycontact-2*dscrew1-wellw/2,0]) + cylinder(d=dscrew1+gap,h=hzbase,$fn=8*cacc); + }; + + //cable holes + if (preview<5) // Free standing paddle, no attachment + translate([-(wxbase/2-dcable/4),ycontact-2*dscrew1-wellw-dcable/2,dcable/2+wallmin]){ + // cable hole 1 + translate([0,-dcable/2,0]) + rotate([0,0,90]) + teardropHole(lh=wxbase*2,rh=dcable/2); + // cable hole 2 + rotate([0,0,45+90]) + teardropHole(lh=wxbase/sqrt(2)/2,rh=dcable/2); + //cable access opening + cube([dcable,2*dcable,dcable],center=true); + } + else{ // Cable routing for QCX + // cable hole from well + translate([0,lybase/2+dcable,dcable/2+wallmin]) + rotate([0,0,90]) + teardropHole(lh=wxbase,rh=dcable/2); + + //cable access opening, printable without support, using breakaway wall + for (i=[-1,1]) + translate([wxbase/2+0*wallt/2,lybase/2+i*(gap+dcable/2), wallmin]){ + cube([dcable,dcable,dcable]); + translate([0,dcable/2,1.5*dcable])rotate([0,90,0])wedge(90,dcable/2,dcable); + }; + + //vertical cable hole + translate([wxbase/2+dcable-(wallt+dcable)/2,lybase/2,wallmin]){ + hhole=hzbase+hzwall/2; + cylinder(d=dcable,h=hhole); // main hole + translate([0,0,hhole]) + cylinder(d1=dcable,d2=0,h=dcable); // tapered top to avoid support + }; + + // rotation axis + translate([wxbase/2-dcable,lybase/2,(hzbase+hzwall)/2]){ + // Axis hole + rotate([0,90,0]) + cylinder(d=daxis+gap,h=99,$fn=8*cacc); + // Rotation snap + }; + + // Room for square flange soldered onto rotation axis + translate([wxbase/2-wallt,(lybase-lyflange)/2,(hzbase+hzwall-hzflange)/2]){ + cube([dcable,lyflange,hzflange]); + translate([-tol,0,hzflange-tol]) + rotate([90,-135,180]) + wedge(90,dcable/sqrt(2),lyflange); + }; + + }; // End QCX attachment + +}; +module base(){ // complete base part + difference(){ + base_add(); + base_sub(); + }; +}; +module attachment(){ // attachment that can be included in QCX (or other) cabinet + difference(){ + union(){ + + // Attachment part of wall + translate([0,-2*rotstop,0]) + cube([atthick,2*rotstop+lybase,hzbase+hzwall]); + + }; // end union + + // Axis hole + translate([0,lybase/2,(hzbase+hzwall)/2]) + rotate([0,90,0]) + cylinder(d=daxis+gap,h=99,$fn=8*cacc); + + // snaps for paddle rotation, slightly tight to avoid play + translate([0,lybase/2,(hzbase+hzwall)/2]) + rotsnaps(rotsnaph-gap/3); + + }; // end difference +}; +module rotlimit(){ // Rotation stopper for attachment, to be included in cabinet + + limdim=wallt+dcable; // width of stopper cube, equal to stopper on paddle base + + // stopper knob + translate([-dcable,lybase/2-limdim/2,(hzbase+hzwall)/2-limdim/2])// move to hole + rotate([0*30,0,0]) // rotate if desired + translate([0,-lybase/2-limdim/2-rotstop,(hzbase+hzwall)/2-limdim/2]) + cube([dcable,limdim,limdim]); + +}; +module rotlimitEXPORT(){ // Rotation stopper for attachment, to be included in cabinet + translate([0,-lybase,-hzbase-hzwall]) + rotlimit(); +}; +module attachmentEXPORT(){ // Rotation stopper for attachment, to be included in cabinet + translate([0,-lybase,-hzbase-hzwall]) + attachment(); +}; +function attachHeightEXPORT()=hzbase+hzwall; // Export total height +module arm_add(){ // arm base shape, without knob + + // main arm + cube([wxarm,lyarm,hzarm]); + + // extra material for supporting hinge spring + translate([-dxknob,0,0]) + cube([dxknob,y0sprwell,hzarm]); + + // extra material near top of arm, for stiffness and appearance + hzarmextra=hzarm/2-dzcontact-(dscrew2*dscrhrel*1.3); + translate([-dxknob,0,hzarm-(hzarmextra-dxknob)]){ + cube([dxknob,lyarm,hzarmextra-dxknob]); + + // 45 deg underside of extra material, to avoid generation of support + translate([dxknob,0,0]) + rotate([90,45,180]) + wedge(90,dxknob/sqrt(2),lyarm); + } + +}; +module arm_sub(){ // arm shaping + // hinge groove + translate([wxarm-dxwedge+armsep,yhinge-wallt-armsep,0]) + wedge(a=ahingout,wx=dxwedge,hz=hzarm); + + // hinge stopper cutout + translate([wxarm-dxwedge+armsep,yhinge-wallt-armsep,hzarm]) + cone45(dstopper+2*gap); + + // tension spring channel + translate([0,y0sprwell,(hzarm-dspring)/2+dzsprwell]){ + cube([wxarm-sprfloort,ycontact-y0sprwell-2.5*dscrew1,dspring]); + translate([-tol,0,dspring-tol]) + rotate([90,-135,180]) + wedge(90,(wxarm-sprfloort)/sqrt(2),ycontact-y0sprwell-2.5*dscrew1); + }; + + // contact screw hole + translate([-tol,ycontact-wallt-armsep,hzarm/2+dzcontact]) + rotate([0,0,90]) + teardropHole(lh=wxarm*2,rh=dscrew2/2-gap); + + // hole for spring keeping hinge in place + translate([-dxknob-tol,yhinge-wallt-armsep,hzarm/2]) + rotate([0,0,90]) + teardropHole(lh=dxknob+wxarm-dxwedge+armsep-sprfloort+tol,rh=dhingespring/2); + + // extra space for center screw column + translate([-(wxbase/2-wallt-armsep-wxarm),ycontact-wallt-armsep,0]) + cylinder(d=dscrew1*dscrhrel*1.5+2*minstroke+dscrew1,h=dscrew1/2+dzcontact+dscrew1*hscrhrel*1.25,$fn=8*cacc); + + + }; + +module knob_curved(){ // finger-curved and rounded knob + intersection(){ // cutting to outer shape + minkowski(){ // rounding of edges + + // un-rounded knob shrunk by rounding radius + difference(){ // shaping finger rest + + // knob, to be shaped by subtraction + translate([-dxknob,lyarm,0]) + cube([wxknob-rround,lyknob-rround,hzknob-rround]); + + // shaping of knob + translate([-dxknob+txknobmin+dfinger/2,lyarm+0*wallt+dfinger/2,dfinger/2+0*wallt]) + minkowski(){ + cube([tol,wxbase,wxbase]); + sphere(r=dfinger/2,$fn=8*cacc); + }; + }; + // rounding sphere + sphere(r=rround,$fn=cacc*4); + }; + // outer bound of knob + translate([-dxknob,lyarm,0]) + cube([wxknob,dfinger,hzknob]); + }; + +}; +module arm(){ // Generate final shape according to preview and cacc settings + difference(){ + union(){ + knob_curved(); + arm_add(); + }; + arm_sub(); + }; +}; +module build_all(){ + + if(preview==1){ + base(); + translate([wxbase/2-wallt-armsep-wxarm,wallt+armsep,hzbase+armsep]) + arm(); + } + else if (preview==2){ + base(); + translate([wxbase/2-wallt-armsep-wxarm,wallt+armsep,hzbase+armsep]) + arm(); + + translate([-(wxbase/2-wallt-armsep-wxarm),wallt+armsep,hzbase+armsep]) + scale([-1,1,1]) + arm(); + } + else if (preview==3){ + base(); + } + else if (preview==4){ + translate([wxbase/2+wxknob+1,0,0]) + scale([-1,1,1]) + arm(); + + translate([wxbase/2+2*wxknob+2,0,0]) + arm(); + } + else if (preview==5){ + base(); + translate([wxbase/2+dcable+10,0,0]){ // spacing for rendering, for preview of fit + attachment(); + rotlimit(); + }; + } + else if (preview==6){ // Base with additions for attachment + base(); + } + else if (preview==7){ // Attachment for inclusion in cabinet + attachmentEXPORT(); + } + else if (preview==8){ // Rotation stopper for inclusion in cabinet + rotlimitEXPORT(); + } + else if (preview==9){ // Rotation stopper for inclusion in cabinet + attachmentEXPORT(); + rotlimitEXPORT(); + } + else{ + base(); + translate([wxbase/2+wxknob+1,0,0]) + scale([-1,1,1]) + arm(); + + translate([wxbase/2+2*wxknob+2,0,0]) + arm(); + + }; +}; +build_all(); diff --git a/case/seeed_xaio_case.stl b/case/seeed_xaio_case.stl new file mode 100644 index 0000000..c0ea012 Binary files /dev/null and b/case/seeed_xaio_case.stl differ diff --git a/doc/advanced-install.md b/doc/advanced-install.md new file mode 100644 index 0000000..0d140e9 --- /dev/null +++ b/doc/advanced-install.md @@ -0,0 +1,78 @@ +# Advanced Install + +In the Arduino IDE, edit [vail-adapter.ino](../vail-adapter.ino) with the pins +you want to use on your device. + +You will need the MidiUSB and Keyboard libraries installed. +You can do this through the Library manager. + +Then compile and upload the sketch. + + +## Works with no source code changes + +* Seeeduino Xiao +* Adafruit Qt Py + +## Known to work with source code changes + +* Arduino Micro +* KeeYees Pro Micro +* Arduino Leonardo +* Arduino Zero +* Adafruit Trinket M0 +* Adafruit GEMMA M0 +* Adafruit Feather M0 + + +# Advanced Wiring + +![XIAO Pinout](https://files.seeedstudio.com/wiki/Seeeduino-XIAO/img/Seeeduino-XIAO-pinout-1.jpg) + +* GND: Ground +* D2: Dit +* D1: Dah +* D0: Straight Key +* D10: Speaker or Passive piezo buzzer +* A6: Capacative Dit +* A7: Capacative Dah +* A8: Capacative Straight Key + + +## Using a headphone jack + +You can wire a headphone jack up to GND, D1, and D2. +GND should be the sleeve, D1 the ring, and D2 the tip. + + o --- D2 (tip, dit) + |_| --- D1 (ring, dah) + | | --- GND (sleeve) + | | + +# Sidetone generator + +If you connect a buzzer or speaker to pin 10 on one leg, +and ground on the other, +the adapter will beep when you press the straight key. + +This will help a lot if there is a noticeable delay between when you press the key +and when your computer starts making a local beeping sound. + +If you feel like no matter what you do, +you're always getting DAH with your straight key, +you should try this. + + +# Capacative Touch + +The adapter works as a capacative touch sensor, +like a touch lamp. + +You might wire these pins to screws or conductive pads. +These can be used instead of, or in additon to, the normal pins D0, D1, and D2. + +You do not need a ground wire with capacative touch! + +* Pin A6: Dit capacative touch +* Pin A7: Dah capacative touch +* Pin A8: Straight key capacative touch diff --git a/doc/easy-install.md b/doc/easy-install.md new file mode 100644 index 0000000..5202c37 --- /dev/null +++ b/doc/easy-install.md @@ -0,0 +1,26 @@ +# Easy Installation + +1. Get a Seeeduino XIAO +2. Download the most recent xiao firmware from +[releases](https://github.com/nealey/vail-adapter/releases) +3. [Enter bootloader mode](https://wiki.seeedstudio.com/Seeeduino-XIAO/#enter-bootloader-mode) +4. Copy the firmware onto the XIAO + +[Wideo walkthrough of firmware upload](https://www.youtube.com/watch?v=IgOdkUe5SMY) (3:07) + +If you would like to use a different microcontroller, +see the [advanced install instructions](advanced-install.md). + +# Wiring + +![XIAO Pinout](https://files.seeedstudio.com/wiki/Seeeduino-XIAO/img/Seeeduino-XIAO-pinout-1.jpg) + +* GND: Ground (usually in the middle of the paddle) +* Pin D2: Dit (usually left paddle) +* Pin D1: Dah (usually right paddle) + +If you'd like to wire up a TRS (headphone) jack; +or take advantage of some of the adapters other features, +such as dedicated straight key, sidetone generation, +or capacative touch, +see the [advanced install instructions](advanced-install.md). diff --git a/doc/tech-notes.md b/doc/tech-notes.md new file mode 100644 index 0000000..5dc262d --- /dev/null +++ b/doc/tech-notes.md @@ -0,0 +1,24 @@ +# MIDI Negotiation + +Morse code keyers are very simple devices, +they just connect two wires together. +You could use a button if you wanted to, +or even touch wires together. + +The only real complication here is that some browsers +need to get keyboard events instead of musical instrument events. + +The Vail adapter boots into a mode that sends both keyboard events +and MIDI messages. +If it receives a MIDI key release event +on channel 0 +for note C0, +it will disable keyboard events. + +Vail sends this "disable keyboard" MIDI event, so as soon as you +load up Vail, the keyboard events are disabled, and your adapter +will no longer interfere with your typing. +If your browser doesn't support MIDI, +the disable command can't be sent, +and it keeps on sending keystrokes. + diff --git a/doc/vail-adapter-v2.jpg b/doc/vail-adapter-v2.jpg new file mode 100644 index 0000000..027a3fc Binary files /dev/null and b/doc/vail-adapter-v2.jpg differ diff --git a/touchbounce.cpp b/touchbounce.cpp new file mode 100644 index 0000000..8051175 --- /dev/null +++ b/touchbounce.cpp @@ -0,0 +1,11 @@ +#include "touchbounce.h" + +void TouchBounce::attach(int pin) { + this->qt = Adafruit_FreeTouch(pin); + this->qt.begin(); +} + +bool TouchBounce::readCurrentState() { + int val = this->qt.measure(); + return val < QT_THRESHOLD; +} diff --git a/touchbounce.h b/touchbounce.h new file mode 100644 index 0000000..8c4fc28 --- /dev/null +++ b/touchbounce.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "bounce2.h" + +#define QT_THRESHOLD 850 + +class TouchBounce: public Bounce { +public: + // attach a touch pin + void attach(int pin); + +protected: + bool readCurrentState(); + Adafruit_FreeTouch qt; +}; diff --git a/vail-adapter.ino b/vail-adapter.ino index c4b0d3f..496ef4f 100644 --- a/vail-adapter.ino +++ b/vail-adapter.ino @@ -5,27 +5,23 @@ // MIDIUSB - Version: Latest #include #include +#include #include "bounce2.h" +#include "touchbounce.h" -#ifdef ARDUINO_SEEED_XIAO_M0 -# define DIT_PIN 2 -# define DAH_PIN 1 -# define KEY_PIN 0 -# define PIEZO 7 -# define LED_ON false -#else -# define DIT_PIN 12 -# define DAH_PIN 11 -# define KEY_PIN 10 -# define PIEZO 7 -# define LED_ON true -#endif +#define DIT_PIN 2 +#define DAH_PIN 1 +#define KEY_PIN 0 +#define QT_DIT_PIN A6 +#define QT_DAH_PIN A7 +#define QT_KEY_PIN A8 +#define PIEZO 10 +#define LED_ON false // Xiao inverts this logic for some reason #define LED_OFF (!LED_ON) -#define STRAIGHT_KEY ',' #define DIT_KEY KEY_LEFT_CTRL #define DAH_KEY KEY_RIGHT_CTRL -#define TONE 660 +#define TONE 550 #define MILLISECOND 1 #define SECOND (1 * MILLISECOND) @@ -36,6 +32,9 @@ uint16_t iambicDelay = 80 * MILLISECOND; Bounce dit = Bounce(); Bounce dah = Bounce(); Bounce key = Bounce(); +TouchBounce qt_dit = TouchBounce(); +TouchBounce qt_dah = TouchBounce(); +TouchBounce qt_key = TouchBounce(); void setup() { pinMode(LED_BUILTIN, OUTPUT); @@ -43,7 +42,10 @@ void setup() { dit.attach(DIT_PIN, INPUT_PULLUP); dah.attach(DAH_PIN, INPUT_PULLUP); key.attach(KEY_PIN, INPUT_PULLUP); - + qt_dit.attach(QT_DIT_PIN); + qt_dah.attach(QT_DAH_PIN); + qt_key.attach(QT_KEY_PIN); + Keyboard.begin(); // To auto-sense a straight key in a TRRS jack, @@ -114,18 +116,13 @@ void loop() { setLED(); // Monitor straight key pin - if (key.update()) { - midiKey(key.fell(), 0); - if (key.fell()) { + if (key.update() || qt_key.update()) { + bool fell = key.fell() || qt_key.fell(); + midiKey(fell, 0); + if (fell) { tone(PIEZO, TONE); - if (keyboard) { - Keyboard.press(STRAIGHT_KEY); - } } else { noTone(PIEZO); - if (keyboard) { - Keyboard.release(STRAIGHT_KEY); - } } } @@ -135,10 +132,11 @@ void loop() { return; } - if (dit.update()) { - midiKey(dit.fell(), 1); + if (dit.update() || qt_dit.update()) { + bool fell = dit.fell() || qt_dit.fell(); + midiKey(fell, 1); if (keyboard) { - if (dit.fell()) { + if (fell) { Keyboard.press(DIT_KEY); } else { Keyboard.release(DIT_KEY); @@ -147,11 +145,12 @@ void loop() { } // Monitor dah pin - if (dah.update()) { - midiKey(dah.fell(), 2); + if (dah.update() || qt_dah.update()) { + bool fell = dah.fell() || qt_dah.fell(); + midiKey(fell, 2); if (keyboard) { - if (dah.fell()) { + if (fell) { Keyboard.press(DAH_KEY); } else { Keyboard.release(DAH_KEY);