Morse Code is not dead. Here is my project to use 2 BBC micro:bits and a laptop to send Morse Code in both directions whilst also decoding Morse Code sent from both ends using button A or a proper key.
Step 1 upload the code using the Python Programmer On line.
Remember to change 1 line of code to determine what end is connected to the laptop (Master). The USB port of the Remote or Slave end is not used, however it may be used for a variety of power connections of course.
LED 1 is USB Tx LED 5 is USB Rx. LED 21 is WiFi Tx and Led 25 is WiFi Rx. LED 11 is Master end, LED 15 is Slave. The 9 LEDs in the middle are the Morse Code Display.
Here is a short example of the sender using button A, the message windows to send pre made messages and the 2 decoder windows in operation.
Here is the code for the micro:bit . V2.2 and above is required.
# main_v854_relay.py – Morse Keyer v8.54 + HL-52 relay (active-low)
# * v8.54: relay polarity fixed for HL-52 active-low modules
# - RELAY_ACTIVE_LOW = True -> write 0 to energise relay
# - RELAY_ACTIVE_LOW = False -> write 1 to energise relay
# - P16 starts de-energised at boot
# * Everything else identical to v8.53 (radio, TDMA, keying, USB, WDT)
from microbit import *
import radio, music, utime, gc, machine
# ============================================================
# ROLE: True = master unit (USB output enabled)
# False = remote unit (UART silent)
MASTER_MODE = True
# RELAY POLARITY:
# HL-52 V1.0 relay board -> True (active-LOW: 0 = relay ON)
# Active-HIGH relay board -> False (1 = relay ON)
RELAY_ACTIVE_LOW = True
# ============================================================
# ---------- configuration ----------
KP = pin8
DBN = 5
RELAY = pin16
def relay_set(on):
"""Drive the relay pin respecting the configured polarity."""
if RELAY_ACTIVE_LOW:
RELAY.write_digital(0 if on else 1)
else:
RELAY.write_digital(1 if on else 0)
# Relay de-energised at boot (prevents the "always-on" symptom)
relay_set(False)
RG = 7
RC = 7
RDR = radio.RATE_1MBIT
RP = 7
FM = 30
SM = 5
FPU = 200
SB = 115200
AMT = 10000
LMT = 30000
MFT = 3
WRT = 120000
LBM = 300
TXM = 300
RXM = 300
PIM = 2000
CLT = 3000
COB = 9
CLB = 4
WPM = 20
FF = 1.0
DM = 1200 // WPM
DAM = 3 * DM
EGM = DM
CGM = int(3 * DM * FF)
WGM = int(7 * DM * FF)
EBI = 10
MEB = 1
MSC = 256
MASTER_LOCAL_TONE = 800
SLAVE_LOCAL_TONE = 600
REMOTE_TONE = 1000
MT = {
'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.',
'F': '..-.', 'G': '--.', 'H': '....', 'I': '..', 'J': '.---',
'K': '-.-', 'L': '.-..', 'M': '--', 'N': '-.', 'O': '---',
'P': '.--.', 'Q': '--.-', 'R': '.-.', 'S': '...', 'T': '-',
'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-', 'Y': '-.--',
'Z': '--..',
'0': '-----', '1': '.----', '2': '..---', '3': '...--',
'4': '....-', '5': '.....', '6': '-....', '7': '--...',
'8': '---..', '9': '----.',
' ': ' ',
'.': '.-.-.-', ',': '--..--', '?': '..--..', '/': '-..-.',
'=': '-...-', '+': '.-.-.', '-': '-....-', '@': '.--.-.',
'!': '-.-.--', '&': '.-...', '(': '-.--.', ')': '-.--.-',
':': '---...', ';': '-.-.-.', "'": '.----.', '"': '.-..-.',
'_': '..--.-', '$': '...-..-'
}
KP.set_pull(KP.PULL_UP)
# ---------- global state ----------
R = 'M' if MASTER_MODE else 'S'
MFS = 0
SFB = 0
STT = 0
PK = 0
SK = 0
EL = 0
LEL = -1
RK = 0
LR = -1
KR = 0
KST = 0
SD = False
ST = []
SI = 0
SNC = 0
SST = 0
LES = 0
BT = 0
LRR = 0
LPS = 0
LUR = 0
LUT = 0
LRX = 0
LTX = 0
LMA = 0
LHS = 0
RHI = 1000
HLD = FM * 10
HTX = False
HRX = False
HLO = 0
LUB = 0
UBO = False
UBT = 0
TXO = 0
RXO = 0
MCO = False
RCO = False
MC = False
MCS = 0
MCB = 500
MCR = 1000
_pending_words = []
_word_done_flag = False
_waiting_for_gap = False
_next_word_time = 0
_pending_word_next = ""
_seed = 0
# ---------- v8.53 crash protection ----------
UART_ENABLED = MASTER_MODE
RADIO_FORCE_REINIT_MS = 120000
RADIO_SILENCE_LIMIT = 300000
RADIO_DEAD_LIMIT = 60000
MEM_CHECK_MS = 30000
MEM_THRESHOLD = 4096
last_radio_reinit = 0
last_audio_freq = 0
# ---------- nRF51 hardware watchdog ----------
WDT_START = 0x40010000
WDT_STOP = 0x40010004
WDT_CRV = 0x40010504
WDT_RREN = 0x40010508
WDT_RR0 = 0x40010600
def setup_wdt(timeout_sec=8):
try:
machine.mem32[WDT_STOP] = 1
utime.sleep_ms(1)
machine.mem32[WDT_CRV] = 32768 * timeout_sec
machine.mem32[WDT_RREN] = 0x01
machine.mem32[WDT_START] = 1
return True
except:
return False
WDT_READY = setup_wdt(8)
def feed_wdt():
if WDT_READY:
machine.mem32[WDT_RR0] = 0x6E524635
def safe_radio_send(msg):
try:
radio.send(msg)
return True
except:
rri()
return False
def rri():
global R, MFS, SFB, STT, MC, LAST_RADIO_ACT, last_radio_reinit
radio.off()
utime.sleep_ms(50)
radio.on()
utime.sleep_ms(50)
radio.config(group=RG, channel=RC, power=RP, data_rate=RDR)
MFS = 0; SFB = 0; STT = 0; MC = False
LAST_RADIO_ACT = running_time()
last_radio_reinit = running_time()
if UART_ENABLED:
dbg("Reinit")
# ---------- helpers ----------
def pr(m):
global _seed
_seed = (_seed * 1103515245 + 12345) & 0x7fffffff
return _seed % m
def ul():
global HTX, HRX, HLO, UBO, UBT, MCO, RCO, TXO, RXO
n = running_time()
display.set_pixel(0,0,9 if (n-LUR<LBM) else 0)
display.set_pixel(4,0,9 if ((n-LUT<LBM) or UBO) else 0)
if R=='M':
b=COB if MCO else CLB
display.set_pixel(0,2,b)
display.set_pixel(4,2,0)
elif R=='S':
b=COB if RCO else CLB
display.set_pixel(4,2,b)
display.set_pixel(0,2,0)
else:
display.set_pixel(0,2,0)
display.set_pixel(4,2,0)
rx_on = (n<RXO) or HRX
display.set_pixel(0,4,9 if rx_on else 0)
tx_on = (n<TXO) or HTX
display.set_pixel(4,4,9 if tx_on else 0)
if HTX and n>=HLO: HTX=False
if HRX and n>=HLO: HRX=False
if UBO and n>=UBT: UBO=False
active = EL or RK
for y in range(1,4):
for x in range(1,4):
display.set_pixel(x,y,9 if active else 0)
# ---- v8.54: polarity-aware relay drive ----
relay_set(bool(active))
def uw(d):
global LUT
if UART_ENABLED:
uart.write(d)
LUT = running_time()
def dbg(m):
# Debug messages are NOT sent in v8.5x to keep USB traffic light.
pass
def ss(w,f=1.0):
global DM,DAM,EGM,CGM,WGM,WPM,FF
WPM=w
FF=f
DM=1200//WPM
DAM=3*DM
EGM=DM
CGM=int(3*DM*f)
WGM=int(7*DM*f)
def me(t):
return [MT.get(c,'') for c in t.upper()]
def btl_single_word(w):
tm = []
mw = me(w)
for ci, c in enumerate(mw):
for i, s in enumerate(c):
tm.append((DM if s == '.' else DAM, 1))
if i < len(c) - 1:
tm.append((EGM, 0))
if ci < len(mw) - 1:
tm.append((CGM, 0))
if tm and tm[-1][1] == 0:
tm.pop()
return tm
def ssnd_word(w):
global SD,ST,SI,SNC,SK,SST
tm = btl_single_word(w)
if not tm:
return False
ST = tm
SI = 0
SK = tm[0][1]
SNC = running_time() + tm[0][0]
SST = running_time()
SD = True
return True
def abt():
global SD,SK,ST,SI,SNC,_pending_words,_word_done_flag
global _waiting_for_gap, _pending_word_next
SD=False
SK=0
ST=[]
SI=0
SNC=0
_pending_words = []
_word_done_flag = False
_waiting_for_gap = False
_pending_word_next = ""
def usd():
global SD,SI,SNC,SK,_word_done_flag
if not SD: return
n=running_time()
if n-SST>120000: abt(); return
if n>=SNC:
SI+=1
if SI>=len(ST):
SD=False
SK=0
_word_done_flag = True
return
SK=ST[SI][1]
SNC+=ST[SI][0]
if SNC<n: SNC=n+ST[SI][0]
def send_ev(et, st, ts):
if UART_ENABLED:
uart.write("E," + et + str(st) + "," + str(ts) + "\n")
LUT = running_time()
def upk():
global PK,KR,KST
raw=1 if (KP.read_digital()==0 or button_a.is_pressed()) else 0
n=running_time()
if raw!=KR: KR=raw; KST=n
if n-KST>=DBN and PK!=raw: PK=raw
cbuf=""
def pcmd(c):
global LUR,_pending_words,_word_done_flag,_waiting_for_gap,_pending_word_next
c=c.strip()
if not c: return
LUR=running_time()
if c=='X': abt(); return
if c=='RST': dbg("USB RST"); reset()
if 'T' in c:
p=c.split('T',1)
sp=p[0]
txt=p[1] if len(p)>1 else ""
if 'F' in sp:
fi=sp.find('F')
fp=sp[fi+1:].split(',')
try:
w=int(fp[0])
f=float(fp[1]) if len(fp)>1 else 1.0
ss(w,f)
except: pass
if txt:
words = [w for w in txt.split() if w]
if words:
abt()
_pending_words = words[1:]
_word_done_flag = False
_waiting_for_gap = False
_pending_word_next = ""
ssnd_word(words[0])
elif 'F' in c:
fi=c.find('F')
fp=c[fi+1:].split(',')
try:
w=int(fp[0])
f=float(fp[1]) if len(fp)>1 else 1.0
ss(w,f)
except: pass
# ---------- main ----------
def main():
global R,MFS,SFB,STT,EL,LEL,RK,LR,LES,SK,SD,cbuf,BT,LRR,LPS
global LUR,LUT,LRX,LTX,LMA,LHS,HTX,HRX,HLO,LUB,UBO,UBT,MCO,RCO,TXO,RXO
global MC,MCS,_pending_words,_word_done_flag,_waiting_for_gap,_next_word_time,_pending_word_next
global LAST_RADIO_ACT, REINIT_PENDING, REINIT_TIME, UART_ENABLED
global last_radio_reinit, last_audio_freq
uart.init(baudrate=SB)
utime.sleep_ms(100)
radio.on()
utime.sleep_ms(100)
radio.config(group=RG, channel=RC, power=RP, data_rate=RDR)
display.scroll("v8.54", delay=80)
sleep(3000)
display.clear()
global _seed
_seed=running_time()&0x7fffffff
BT=running_time()
LRR=BT; LPS=BT; LES=BT
LEL=0; LR=0; LMA=BT
LUR=0; LUT=0; LRX=0; LTX=0
LHS=0; LUB=0; TXO=0; RXO=0
MC=False
_pending_words = []
_word_done_flag = False
_waiting_for_gap = False
_next_word_time = 0
_pending_word_next = ""
last_blink = BT
LAST_RADIO_ACT = BT
REINIT_PENDING = False
REINIT_TIME = 0
last_radio_reinit = BT
last_audio_freq = 0
next_mem_report = BT + MEM_CHECK_MS
if R == 'M':
MFS = BT
else:
SFB = 0
# Ensure relay is off at start of main loop
relay_set(False)
while True:
n=running_time()
feed_wdt()
if n - last_radio_reinit > RADIO_FORCE_REINIT_MS:
rri()
if n >= next_mem_report:
free = gc.mem_free()
if UART_ENABLED:
uw("MEM:"+str(free)+"\n")
if free < MEM_THRESHOLD:
gc.collect()
if gc.mem_free() < MEM_THRESHOLD:
machine.reset()
next_mem_report = n + MEM_CHECK_MS
if n - LAST_RADIO_ACT > RADIO_SILENCE_LIMIT:
if not REINIT_PENDING:
rri()
else:
if n - REINIT_TIME > RADIO_DEAD_LIMIT:
machine.reset()
if uart.any():
ch=uart.read(1)
if ch:
c=ch.decode()
if c=='\n': pcmd(cbuf); cbuf=""
else: cbuf+=c
LUR=n
upk()
EL=PK or SK
pkt=radio.receive()
if pkt:
LRX=n; LRR=n; RXO=n+RXM
is_M = (pkt[0]=='M' and len(pkt)==2 and pkt[1].isdigit())
is_S = (pkt[0]=='S' and len(pkt)==2 and pkt[1].isdigit())
if is_M:
LAST_RADIO_ACT = n
st = int(pkt[1])
if R == 'S':
SFB = n
STT = SFB + SM
if st != RK:
RK = st
send_ev('R', RK, n)
LMA = n
elif is_S:
LAST_RADIO_ACT = n
try:
st = int(pkt[1])
if st != RK:
RK = st
send_ev('R', RK, n)
LMA = n
except:
pass
else:
for _ in range(10):
if radio.receive() is None:
break
if R == 'S' and (n - LRR > WRT):
rri()
LRR = running_time()
if R=='M': MCO=(n-LRR)<CLT
elif R=='S': RCO=(n-LRR)<CLT
else: RCO=False
if R == 'M' and n - MFS >= FM:
MFS = n
msg = 'M' + str(EL)
if safe_radio_send(msg):
LAST_RADIO_ACT = n
LTX=n; TXO=n+TXM; utime.sleep_us(FPU)
if R == 'S' and SFB != 0 and n >= STT:
msg = 'S' + str(EL)
if safe_radio_send(msg):
LAST_RADIO_ACT = n
LTX=n; TXO=n+TXM
STT += FM
utime.sleep_us(FPU)
if EL or RK:
new_freq = MASTER_LOCAL_TONE if (R=='M' and EL) else (SLAVE_LOCAL_TONE if (R=='S' and EL) else REMOTE_TONE)
if new_freq != last_audio_freq:
music.pitch(int(new_freq), -1)
last_audio_freq = new_freq
else:
if last_audio_freq != 0:
music.stop()
last_audio_freq = 0
if n-LUB>=RHI:
LUB=n; UBO=True; UBT=n+HLD
if EL!=LEL:
LEL=EL
send_ev('L', EL, n)
LMA=n
if SD: usd()
if _word_done_flag:
_word_done_flag = False
if _pending_words:
_next_word_time = n + WGM
_pending_word_next = _pending_words.pop(0)
_waiting_for_gap = True
else:
uw("DONE\n")
if _waiting_for_gap and n >= _next_word_time:
_waiting_for_gap = False
ssnd_word(_pending_word_next)
if n - last_blink >= 10000:
last_blink = n
display.set_pixel(2,2,9) if display.get_pixel(2,2)==0 else display.set_pixel(2,2,0)
ul()
utime.sleep_ms(5)
gc.collect()
try:
main()
except Exception as e:
if UART_ENABLED:
uart.write("CRASH:"+str(e)+"\n")
display.scroll("ERR")
sleep(500)
reset()
I had to change.py to .txt. Here is the code for your Windows laptop so it will do the stats and also decode in real time.
Again .py changed to .txt. Just cut and paste into your editor. I use Idle available for free at Python.org.
I also made provision for a relay output and an external key input using the micro:bit driver expansion board Version 2.0 however the relays I used were too slow to keep up and so I suggest using a solid state version matched to your Ham Radio transceiver. It is handy for a Key on P8 and relay on P16 though. My relay board was a fake with no opto isolation :( It was horrible.
Using this from a .cmd prompt "pyinstaller --onefile --windowed --name "MorseDashboard841" --icon=morse.ico --add-data "morse.ico:." FasterMorse_841.py" I can turn the above files into an executable file, so you can give it to a friend that does not even need to have Python installed.