Lots of changes for V3.0:

This commit is contained in:
Neale Pickett 2022-05-21 20:14:03 -06:00
parent 86691b88a9
commit 00d8223f35
12 changed files with 755 additions and 215 deletions

217
README.md
View File

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

16
case/adapter-case.scad Normal file
View File

@ -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]);
}
}

BIN
case/case.stl Normal file

Binary file not shown.

521
case/paddles.scad Normal file
View File

@ -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();

BIN
case/seeed_xaio_case.stl Normal file

Binary file not shown.

78
doc/advanced-install.md Normal file
View File

@ -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

26
doc/easy-install.md Normal file
View File

@ -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).

24
doc/tech-notes.md Normal file
View File

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

BIN
doc/vail-adapter-v2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

11
touchbounce.cpp Normal file
View File

@ -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;
}

16
touchbounce.h Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include <Adafruit_FreeTouch.h>
#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;
};

View File

@ -5,27 +5,23 @@
// MIDIUSB - Version: Latest
#include <MIDIUSB.h>
#include <Keyboard.h>
#include <Adafruit_FreeTouch.h>
#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);