feat: panadapter spots that read, a CI-V link that survives, no auto-call
Panadapter spots are now worth reading. The comment carries the spotter, the
entity and the status in DXHunter's own shape — "CQ up 2 [F4BPO] [Franz Josef
Land] [New Slot]" — which needed two things nothing documents: SmartSDR splits
its command line on SPACES, so the words ran together until every space became
non-breaking; and it truncates past ~60 characters, so the cluster's own words
are trimmed first and the three brackets always survive. RBN column padding is
collapsed on the way in, or a preserved run of spaces opened a gap wide enough
to push the rest off screen.
"Already worked" means the CALLSIGN is in the log, not the entity: saying it of
a station never contacted was simply wrong. Each status can also be kept off the
panadapter entirely, and the WSJT-X decode spots obey the same switches — the
palette governs the panadapter, not one of the two things that feed it.
And the radio is no longer hammered: a spot whose frequency, colour and comment
are unchanged is not removed and redrawn. A busy skimmer feed re-spots the same
station every few seconds; one two-minute session sent 2128 adds, 88 of them for
a single callsign, and the display did not move a pixel for any of them.
CI-V, from an IC-7850 that kept killing JTDX: a reply the rig sent to another
controller on the same bus is no longer taken for ours, and a set_ptt, set_freq
or set_mode whose acknowledgement goes missing is verified by reading the rig
back instead of being reported as a failure. WSJT-X and JTDX answer a failed
command with a Rig Control Error and drop the link mid-over — 98 keyings, 6 lost
acknowledgements, 2 dropped connections in one session. The check waits 700 ms,
not the poll's 150: the rig has just failed to answer twice because it was
retuning, and a short probe would fail for the same reason.
Auto-call is withdrawn — it duplicated DXHunter, which already answers decodes,
and two programs deciding that from one shack key over each other. The library
is kept whole and dormant; a guard in App.tsx makes sure a stored preference
cannot key a transmitter whose switch no longer exists.
Also:
- the log rotates while running, not only at startup: the CI-V trace left on
wrote 416 MB and nothing would have stopped it before the disk did. Closing
it now releases the crash file too — the runtime keeps its own duplicate.
- the interface zoom announces itself, with a badge, a click back to 100% and
a View menu; Ctrl+wheel and Ctrl+0 always worked and nothing said so.
- no more elastic bounce, and no swipe-to-navigate out of the app.
- Edit QSO: your own TX power and the contacted station's extended locator
were saved and written back with no box to set them.
- FT decodes: continents are a multiple choice; a compound MSHV message that
answers two stations in one line is recognised as addressed to you.
This commit is contained in:
@@ -20,9 +20,15 @@ var (
|
||||
mu sync.Mutex
|
||||
file *os.File
|
||||
path string
|
||||
written int64 // bytes in the CURRENT file, for the size check in Printf
|
||||
crashFile *os.File // kept open so the runtime can write a crash traceback to it
|
||||
)
|
||||
|
||||
// maxLogBytes is where the file rolls over, at startup AND while running.
|
||||
// A variable, not a constant, so the rotation can be exercised in a test without
|
||||
// writing ten megabytes to do it.
|
||||
var maxLogBytes int64 = 10 * 1024 * 1024
|
||||
|
||||
// Init opens (creates) the log file in dataDir. On rotation we truncate
|
||||
// at startup if the file is too big; for now it's a single file, no
|
||||
// rolling — the volume is low (a few KB per session).
|
||||
@@ -47,12 +53,12 @@ func Init(dataDir string) (string, error) {
|
||||
_ = os.Rename(oldLog, logPath)
|
||||
}
|
||||
}
|
||||
// Rotate (don't delete) once the file grows past ~10MB: rename it to
|
||||
// Rotate (don't delete) once the file grows past maxLogBytes: rename it to
|
||||
// opslog.log.1 so the PREVIOUS session's log survives. Deleting it outright used
|
||||
// to erase exactly the diagnostics we needed when a user reported an issue from
|
||||
// the session that just ended. One generation kept — enough, without unbounded growth.
|
||||
if fi, err := os.Stat(logPath); err == nil && fi.Size() > 10*1024*1024 {
|
||||
_ = os.Remove(logPath + ".1") // drop the older generation
|
||||
if fi, err := os.Stat(logPath); err == nil && fi.Size() > maxLogBytes {
|
||||
_ = os.Remove(logPath + ".1") // drop the older generation
|
||||
if err := os.Rename(logPath, logPath+".1"); err != nil {
|
||||
_ = os.Remove(logPath) // rename failed (locked?) → fall back to the old behaviour
|
||||
}
|
||||
@@ -96,13 +102,57 @@ func Printf(format string, args ...any) {
|
||||
msg = msg[:len(msg)-1]
|
||||
}
|
||||
if file != nil {
|
||||
fmt.Fprintf(file, "%s %s\n", stamp, msg)
|
||||
n, _ := fmt.Fprintf(file, "%s %s\n", stamp, msg)
|
||||
written += int64(n)
|
||||
rotateIfBigLocked()
|
||||
}
|
||||
// Also dump to stderr in case the binary was launched with a console
|
||||
// attached (wails dev, custom build).
|
||||
fmt.Fprintf(os.Stderr, "%s %s\n", stamp, msg)
|
||||
}
|
||||
|
||||
// rotateIfBigLocked rolls the file over mid-session once it passes maxLogBytes.
|
||||
//
|
||||
// The check used to happen at STARTUP only, on the reasonable belief that a
|
||||
// session writes a few kilobytes. The CI-V wire trace disproved it: left on for
|
||||
// a day it wrote 416 MB, and nothing would have stopped it before the disk did.
|
||||
// A log that has to be rotated by quitting the program is not a bound.
|
||||
//
|
||||
// Same one-generation scheme as the startup rotation, so the previous stretch
|
||||
// survives — which is the half an operator usually needs, the part just before
|
||||
// the thing they are reporting.
|
||||
//
|
||||
// Caller holds mu.
|
||||
func rotateIfBigLocked() {
|
||||
if written < maxLogBytes || file == nil || path == "" {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(file, "%s ── log reached %d MB, rotating to %s.1 ──\n",
|
||||
time.Now().Format("15:04:05.000"), maxLogBytes/(1024*1024), filepath.Base(path))
|
||||
_ = file.Close()
|
||||
file = nil
|
||||
_ = os.Remove(path + ".1")
|
||||
if err := os.Rename(path, path+".1"); err != nil {
|
||||
// Rename refused (the file is held open elsewhere, a virus scanner): keep
|
||||
// writing to the old handle rather than losing the log entirely, and try
|
||||
// again in another maxLogBytes rather than on every single line.
|
||||
f, oerr := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if oerr == nil {
|
||||
file = f
|
||||
}
|
||||
written = 0
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return // nothing more to be done; the next Printf simply writes nowhere
|
||||
}
|
||||
file = f
|
||||
written = 0
|
||||
fmt.Fprintf(file, "%s ── continued from %s.1 ──\n",
|
||||
time.Now().Format("15:04:05.000"), filepath.Base(path))
|
||||
}
|
||||
|
||||
// Path returns where the file is so the UI can surface it.
|
||||
func Path() string {
|
||||
mu.Lock()
|
||||
@@ -118,4 +168,19 @@ func Close() {
|
||||
_ = file.Close()
|
||||
file = nil
|
||||
}
|
||||
// The crash file is held open for the whole run so the runtime can dump a
|
||||
// traceback into it without allocating; on the way out it has to be released
|
||||
// like any other handle, or the data directory cannot be moved or removed.
|
||||
if crashFile != nil {
|
||||
// The RUNTIME holds its own duplicate of this handle (SetCrashOutput), so
|
||||
// closing ours releases nothing on Windows — the data directory stays
|
||||
// locked. Hand the runtime a nil first. Anything fatal in the last moments
|
||||
// of shutdown is no longer captured, which is the right trade at a point
|
||||
// where the databases are already closed.
|
||||
_ = debug.SetCrashOutput(nil, debug.CrashOptions{})
|
||||
_ = crashFile.Close()
|
||||
crashFile = nil
|
||||
}
|
||||
path = ""
|
||||
written = 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user