up
This commit is contained in:
+10
-1
@@ -4,7 +4,16 @@
|
||||
"Bash(go get *)",
|
||||
"Bash(go build *)",
|
||||
"Bash(wails generate *)",
|
||||
"Bash(npm run *)"
|
||||
"Bash(npm run *)",
|
||||
"Bash(gofmt -w app.go internal/winkeyer/winkeyer.go)",
|
||||
"Bash(timeout 20 git status --short)",
|
||||
"Bash(echo \"exit: $?\")",
|
||||
"Bash(GIT_TERMINAL_PROMPT=0 timeout 20 git push --dry-run)",
|
||||
"Bash(echo \"push exit: $?\")",
|
||||
"Bash(git config *)",
|
||||
"Bash(ls \"/c/Program Files/Git/mingw64/bin/git-credential-manager\"*.exe)",
|
||||
"Bash(ls \"/c/Program Files/Git/mingw64/libexec/git-core/git-credential-manager\"*.exe)",
|
||||
"Bash(which git-credential-manager *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -1194,10 +1195,20 @@ func (a *App) reloadLookupProviders() {
|
||||
|
||||
// --- QSO bindings ---
|
||||
|
||||
func (a *App) AddQSO(q qso.QSO) (int64, error) {
|
||||
func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
|
||||
if a.qso == nil {
|
||||
return 0, fmt.Errorf("db not initialized")
|
||||
}
|
||||
// Never let a panic in the logging path crash the whole app — the user lost
|
||||
// QSOs that way (CW + WinKeyer). Surface it as an error instead.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
applog.Printf("PANIC in AddQSO: %v\n%s", r, debug.Stack())
|
||||
if id == 0 {
|
||||
err = fmt.Errorf("internal error while logging: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
a.applyStationDefaults(&q)
|
||||
a.applyDXCCNumber(&q)
|
||||
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
|
||||
@@ -1210,7 +1221,7 @@ func (a *App) AddQSO(q qso.QSO) (int64, error) {
|
||||
q.Email = lr.Email
|
||||
}
|
||||
}
|
||||
id, err := a.qso.Add(a.ctx, q)
|
||||
id, err = a.qso.Add(a.ctx, q)
|
||||
if err == nil {
|
||||
q.ID = id
|
||||
a.saveQSORecording(&q)
|
||||
|
||||
+16
-6
@@ -1294,9 +1294,15 @@ export default function App() {
|
||||
email: details.email,
|
||||
};
|
||||
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
|
||||
const loggedCall = String(payload.callsign ?? '');
|
||||
const loggedDxcc = typeof payload.dxcc === 'number' ? payload.dxcc : 0;
|
||||
await AddQSO(payload);
|
||||
resetEntry();
|
||||
await refresh();
|
||||
// Refresh the Worked-before matrix so the just-logged band/mode flips to
|
||||
// "worked" — resetEntry cleared it, so re-fetch for the logged call (a
|
||||
// live DB query, so it now includes this QSO).
|
||||
if (loggedCall.length >= 3) runWorkedBefore(loggedCall, loggedDxcc);
|
||||
} catch (e: any) {
|
||||
setError(String(e?.message ?? e));
|
||||
} finally { setSaving(false); }
|
||||
@@ -1556,6 +1562,11 @@ export default function App() {
|
||||
setDetails((d) => ({ ...d, award_refs: refs.map((r) => `POTA@${r}`).join(';') }));
|
||||
}
|
||||
function onCallsignInput(v: string, opts?: { force?: boolean }) {
|
||||
// Programmatic call-sets (force: spot click, UDP, external app) count as
|
||||
// "not manually typed", so a later UDP DX call (DXHunter remote control) can
|
||||
// still replace it. Without this, clicking a cluster spot froze the call:
|
||||
// applyUdpCall saw current != lastUdpCall and refused every later UDP call.
|
||||
if (opts?.force) lastUdpCallRef.current = v.trim().toUpperCase();
|
||||
// No-op guard: external apps (MSHV/WSJT-X) re-broadcast the same DX call
|
||||
// on every status packet. If it matches what's already in the entry,
|
||||
// do nothing — otherwise we'd re-run the QRZ lookup, hit the cache and
|
||||
@@ -1567,13 +1578,12 @@ export default function App() {
|
||||
// Recording START happens on blur (leaving the callsign field), NOT here —
|
||||
// you may type a call and work it minutes later. Clearing it cancels.
|
||||
if (v.trim() === '') { QSOAudioCancel(); setRecording(false); recordingCallRef.current = ""; }
|
||||
const wasEmpty = callsign.trim() === '';
|
||||
const isEmpty = v.trim() === '';
|
||||
if (wasEmpty && !isEmpty && !locks.start) {
|
||||
// First keystroke of a new QSO — freeze the start time so it doesn't
|
||||
// drift even if the lookup or typing takes 30 seconds. Skip when
|
||||
// start is locked: the user is back-entering a past QSO and set a
|
||||
// specific time manually.
|
||||
if (!isEmpty && !locks.start) {
|
||||
// Restart the start time on every callsign change (each keystroke, a
|
||||
// clicked spot, a new call): "Start UTC" should mark when you actually
|
||||
// began this contact, not a stale time frozen at the first keystroke.
|
||||
// Skip when start is locked (back-entering a past QSO at a chosen time).
|
||||
setQsoStartedAt(new Date());
|
||||
} else if (isEmpty && !locks.start) {
|
||||
// Callsign wiped → user abandoned this QSO; reset the timer.
|
||||
|
||||
@@ -13,6 +13,7 @@ package winkeyer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -26,28 +27,28 @@ import (
|
||||
type Mode string
|
||||
|
||||
const (
|
||||
ModeIambicB Mode = "iambic_b"
|
||||
ModeIambicA Mode = "iambic_a"
|
||||
ModeUltimatic Mode = "ultimatic"
|
||||
ModeBug Mode = "bug"
|
||||
ModeIambicB Mode = "iambic_b"
|
||||
ModeIambicA Mode = "iambic_a"
|
||||
ModeUltimatic Mode = "ultimatic"
|
||||
ModeBug Mode = "bug"
|
||||
)
|
||||
|
||||
// Config is the keyer configuration the UI persists and applies on connect.
|
||||
type Config struct {
|
||||
Port string `json:"port"` // e.g. "COM6"
|
||||
Baud int `json:"baud"` // 1200 for WK2, also fine for WK3
|
||||
WPM int `json:"wpm"` // 5..99
|
||||
Weight int `json:"weight"` // 10..90, 50 = normal
|
||||
LeadInMs int `json:"lead_in_ms"` // PTT lead-in, 10 ms units sent to device
|
||||
TailMs int `json:"tail_ms"` // PTT tail
|
||||
Ratio int `json:"ratio"` // dah/dit ratio 33..66 (50 = 3:1)
|
||||
Farnsworth int `json:"farnsworth"` // Farnsworth WPM (0 = off)
|
||||
Sidetone int `json:"sidetone_hz"` // 0 = off; else target Hz (mapped to WK code)
|
||||
Mode Mode `json:"mode"` // paddle mode
|
||||
Swap bool `json:"swap"` // swap dit/dah paddles
|
||||
AutoSpace bool `json:"autospace"` // auto letter-space
|
||||
UsePTT bool `json:"use_ptt"` // key PTT (Key/PTT output)
|
||||
SerialEcho bool `json:"serial_echo"` // device echoes sent chars back to host
|
||||
Port string `json:"port"` // e.g. "COM6"
|
||||
Baud int `json:"baud"` // 1200 for WK2, also fine for WK3
|
||||
WPM int `json:"wpm"` // 5..99
|
||||
Weight int `json:"weight"` // 10..90, 50 = normal
|
||||
LeadInMs int `json:"lead_in_ms"` // PTT lead-in, 10 ms units sent to device
|
||||
TailMs int `json:"tail_ms"` // PTT tail
|
||||
Ratio int `json:"ratio"` // dah/dit ratio 33..66 (50 = 3:1)
|
||||
Farnsworth int `json:"farnsworth"` // Farnsworth WPM (0 = off)
|
||||
Sidetone int `json:"sidetone_hz"` // 0 = off; else target Hz (mapped to WK code)
|
||||
Mode Mode `json:"mode"` // paddle mode
|
||||
Swap bool `json:"swap"` // swap dit/dah paddles
|
||||
AutoSpace bool `json:"autospace"` // auto letter-space
|
||||
UsePTT bool `json:"use_ptt"` // key PTT (Key/PTT output)
|
||||
SerialEcho bool `json:"serial_echo"` // device echoes sent chars back to host
|
||||
}
|
||||
|
||||
func (c Config) normalised() Config {
|
||||
@@ -77,9 +78,9 @@ func (c Config) normalised() Config {
|
||||
// Status is pushed to the UI whenever the link state or keyer activity changes.
|
||||
type Status struct {
|
||||
Connected bool `json:"connected"`
|
||||
Busy bool `json:"busy"` // device is currently sending CW
|
||||
WPM int `json:"wpm"` // current speed (tracks the speed pot)
|
||||
Version int `json:"version"` // host firmware version byte
|
||||
Busy bool `json:"busy"` // device is currently sending CW
|
||||
WPM int `json:"wpm"` // current speed (tracks the speed pot)
|
||||
Version int `json:"version"` // host firmware version byte
|
||||
Port string `json:"port"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
@@ -177,11 +178,11 @@ func (m *Manager) Connect(cfg Config) error {
|
||||
// applyConfig pushes the keying parameters to the device.
|
||||
func (m *Manager) applyConfig(c Config) error {
|
||||
cmds := [][]byte{
|
||||
{0x0E, modeRegister(c)}, // set mode register (paddle mode, swap, autospace…)
|
||||
{0x02, byte(c.WPM)}, // set speed (WPM)
|
||||
{0x03, byte(c.Weight)}, // set weighting
|
||||
{0x0E, modeRegister(c)}, // set mode register (paddle mode, swap, autospace…)
|
||||
{0x02, byte(c.WPM)}, // set speed (WPM)
|
||||
{0x03, byte(c.Weight)}, // set weighting
|
||||
{0x04, byte(c.LeadInMs / 10), byte(c.TailMs / 10)}, // PTT lead-in / tail (10 ms units)
|
||||
{0x11, byte(c.Ratio)}, // set dit/dah ratio
|
||||
{0x11, byte(c.Ratio)}, // set dit/dah ratio
|
||||
}
|
||||
// Sidetone: <0x01 n>. Bit6 enables, low nibble selects the pitch divisor.
|
||||
cmds = append(cmds, []byte{0x01, sidetoneCode(c.Sidetone)})
|
||||
@@ -197,9 +198,11 @@ func (m *Manager) applyConfig(c Config) error {
|
||||
}
|
||||
|
||||
// modeRegister builds the WinKey mode-register byte (command 0x0E).
|
||||
// bits 1..0 : paddle mode (00 Iambic-B, 01 Iambic-A, 10 Ultimatic, 11 Bug)
|
||||
// bit 3 : paddle swap
|
||||
// bit 0/... : (autospace is bit 0 of a separate group on some firmwares)
|
||||
//
|
||||
// bits 1..0 : paddle mode (00 Iambic-B, 01 Iambic-A, 10 Ultimatic, 11 Bug)
|
||||
// bit 3 : paddle swap
|
||||
// bit 0/... : (autospace is bit 0 of a separate group on some firmwares)
|
||||
//
|
||||
// We keep to the widely-compatible WK2 layout.
|
||||
func modeRegister(c Config) byte {
|
||||
var b byte
|
||||
@@ -339,6 +342,14 @@ func (m *Manager) Disconnect() {
|
||||
// the echo of characters being sent. We track busy/idle and the speed pot.
|
||||
func (m *Manager) readLoop(p serial.Port, stop, done chan struct{}) {
|
||||
defer close(done)
|
||||
// A panic here (e.g. in a status/echo callback) would otherwise take down
|
||||
// the whole app — which showed up as a crash when logging a CW QSO while
|
||||
// the keyer was still streaming echo bytes. Recover, log, end the loop.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
applog.Printf("winkeyer: readLoop panic recovered: %v\n%s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
buf := make([]byte, 64)
|
||||
for {
|
||||
select {
|
||||
|
||||
Reference in New Issue
Block a user