Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion mk4-date/Core/Src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,13 @@ static inline void latchDisplay(void){
buffer_a[3] = pre_buffer_a[3];
buffer_a[4] = pre_buffer_a[4];

// dp_pos is the 1-based digit the decimal point attaches to (0 = none). It comes from
// text_idx, which the sender can advance past the 10-digit display, and it indexes the
// 5-entry buffer_a/buffer_b below. Clamp to each orientation's valid range so a stray or
// garbled '.' in the UART stream can't drive a negative / out-of-range index into RAM.
if (!dp_pos) return;
if (inverted) { if (dp_pos > 9) return; } // inverted: 9-dp_pos goes negative at 10
else { if (dp_pos > 10) return; } // non-inverted: dp_pos-6 tops out at [4]

if (inverted){
if (dp_pos>=5) {
Expand Down Expand Up @@ -601,7 +607,7 @@ static inline void parseByte(uint8_t x){
dp_pos = text_idx;
return;
}
if(text_idx > MAX_TEXT_LEN) return;
if(text_idx >= MAX_TEXT_LEN) return; // was '>': at text_idx==MAX_TEXT_LEN this wrote text[32], 1 byte past the buffer

if (text_idx < 10) setDigitPre(text_idx, x);
text[text_idx++] = x;
Expand Down
48 changes: 48 additions & 0 deletions mk4-time/Core/Inc/astro.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* astro.h — minimal solar/lunar/grid astronomy for the Precision Clock Mk IV.
*
* Self-contained C99 + <math.h>; NO firmware dependencies, so it compiles and
* unit-tests natively (see test_astro.c). All angles in degrees at the API
* boundary; UTC instants are passed as a double of Unix seconds.
*
* Algorithms are low-precision (NOAA/Meeus-class) approximations — accurate to a
* fraction of a degree / a minute or two, which is all a 7-segment readout shows,
* and cheap enough to evaluate once per mode entry on the STM32's soft-double.
*/
#ifndef ASTRO_H
#define ASTRO_H

/* (a) Sun apparent alt/az for an observer at (lat, lon) decimal degrees, N+/E+,
* at the given UTC instant. Writes azimuth in [0,360) measured from North
* clockwise, and elevation in [-90,90] (negative = below the horizon).
* No refraction or parallax correction. */
void sun_az_el(double lat, double lon, double unix_s, double *az, double *el);

/* (b) Sun event times for the UTC calendar day containing unix_s.
* Every output is a decimal UTC hour and MAY be < 0 or > 24 (the event falls
* on the previous/next day) — callers add the local offset and wrap, they do
* NOT clamp. solar_noon is always written. Returns 0 normally; returns
* nonzero on polar day/night (sun never crosses -0.833 deg), in which case
* sunrise/sunset are left untouched and only solar_noon is meaningful.
* Any of the optional twilight pointers may be NULL. */
int sun_times(double lat, double lon, double unix_s,
double *sunrise, double *sunset, double *solar_noon,
double *civil_dusk, double *nautical_dusk, double *golden_dusk);

/* (c) Moon. phase is the synodic fraction [0,1): 0=new, .25=first quarter,
* .5=full, .75=last quarter. */
double moon_phase(double unix_s);
double moon_illuminated_fraction(double phase); /* (1 - cos(2*pi*phase)) / 2 */
int moon_phase_index(double phase); /* 0..7, see ASTRO_MOON_NAMES */

/* (d) Equation of time in minutes (+ = apparent sun ahead of mean/clock sun). */
double equation_of_time(double unix_s);

/* (e) 6-character Maidenhead locator for (lat, lon). out must hold >= 7 bytes.
* Writes "----\0" if either coordinate is non-finite. */
void maidenhead(double lat, double lon, char out[7]);

/* Phase-index -> short name. Index from moon_phase_index(). */
extern const char *const ASTRO_MOON_NAMES[8];

#endif /* ASTRO_H */
15 changes: 13 additions & 2 deletions mk4-time/Core/Inc/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ extern uint16_t buffer_b[];

extern _Bool delayedReadConfigFile;
extern _Bool delayedCheckOnEject;
extern volatile uint8_t fatfs_busy;

extern _Bool waitingForLatch;
extern _Bool resendDate;
Expand Down Expand Up @@ -125,8 +126,9 @@ extern _Bool resendDate;
#define DAC_BUFFER_SIZE 20
#define ADC_BUFFER_SIZE 50

// NMEA 0183 messages have a max length of 82 characters
#define NMEA_BUF_SIZE 90
// NMEA 0183 messages have a max length of 82 characters; the extended $PMTXTS (with the SOF-
// correlation tail: dwt_pps, sof_frame, dwt_sof) runs ~110, so this sizes the tx/rx buffers for it.
#define NMEA_BUF_SIZE 128

#define CMD_LOAD_TEXT 0x90
#define CMD_SET_FREQUENCY 0x91
Expand Down Expand Up @@ -162,6 +164,15 @@ enum {
MODE_DDMMYYYY,
#endif

// Astro pack — GPS-derived astronomy read-outs. SATVIEW-style: the payload
// shows on the 10-char date row while the live clock keeps running on the
// time row. Enabled individually via the MODE_* config keys, like any mode.
MODE_SUN, // sunrise / sunset / solar noon (local), auto-paged
MODE_SUN_AZEL, // sun azimuth & elevation, now
MODE_MOON, // moon phase index + illuminated %
MODE_GRID, // Maidenhead grid locator
MODE_LATLON, // latitude / longitude, auto-paged

NUM_DISPLAY_MODES
};

Expand Down
140 changes: 140 additions & 0 deletions mk4-time/Core/Src/astro.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* astro.c — see astro.h.
* Pure C99 + <math.h>; every intermediate is double on purpose (single-precision
* loses the sub-arc-minute accuracy these approximations are otherwise good for).
*/
#include "astro.h"
#include <math.h>
#include <string.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define J2000_UNIX 946728000.0 /* 2000-01-01 12:00:00 UTC = JD 2451545.0 */
#define DEG (M_PI / 180.0)
#define RAD (180.0 / M_PI)

const char *const ASTRO_MOON_NAMES[8] = {
"New", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
"Full", "Waning Gibbous", "Last Quarter", "Waning Crescent",
};

static double days_since_j2000(double unix_s) { return (unix_s - J2000_UNIX) / 86400.0; }

/* The shared low-precision solar block: from days-since-J2000 produce mean
* longitude L, anomaly g, ecliptic longitude lambda, obliquity eps, and the
* apparent right ascension alpha (deg) and declination delta (rad). */
static void sun_ecliptic(double n, double *L, double *g, double *lambda,
double *eps, double *alpha, double *delta) {
double Lv = fmod(280.460 + 0.9856474 * n, 360.0);
double gv = fmod(357.528 + 0.9856003 * n, 360.0);
double lam = Lv + 1.915 * sin(gv * DEG) + 0.020 * sin(2.0 * gv * DEG);
double ep = (23.439 - 0.0000004 * n) * DEG;
double al = atan2(cos(ep) * sin(lam * DEG), cos(lam * DEG)) * RAD;
double de = asin(sin(ep) * sin(lam * DEG));
if (L) *L = Lv;
if (g) *g = gv;
if (lambda) *lambda = lam;
if (eps) *eps = ep;
if (alpha) *alpha = al;
if (delta) *delta = de;
}

void sun_az_el(double lat, double lon, double unix_s, double *az, double *el) {
double n = days_since_j2000(unix_s);
double alpha, delta;
sun_ecliptic(n, NULL, NULL, NULL, NULL, &alpha, &delta);

double gmst = fmod(18.697374558 + 24.06570982441908 * n, 24.0); /* GMST in hours, used as-is */
double lst = gmst * 15.0 + lon; /* degrees */
double ha = (lst - alpha) * DEG; /* radians */

double e = asin(sin(lat * DEG) * sin(delta) + cos(lat * DEG) * cos(delta) * cos(ha)) * RAD;
double a = atan2(-sin(ha), tan(delta) * cos(lat * DEG) - sin(lat * DEG) * cos(ha)) * RAD;
if (a < 0.0) a += 360.0;
if (el) *el = e;
if (az) *az = a;
}

double equation_of_time(double unix_s) {
double n = days_since_j2000(unix_s);
double L, alpha;
sun_ecliptic(n, &L, NULL, NULL, NULL, &alpha, NULL);
double diff = fmod(L - alpha, 360.0);
if (diff > 180.0) diff -= 360.0;
else if (diff <= -180.0) diff += 360.0;
return 4.0 * diff; /* minutes */
}

int sun_times(double lat, double lon, double unix_s,
double *sunrise, double *sunset, double *solar_noon,
double *civil_dusk, double *nautical_dusk, double *golden_dusk) {
/* Reference instant = 12:00:00 UTC of the calendar day of unix_s. */
double noon_unix = trunc(unix_s / 86400.0) * 86400.0 + 43200.0;
double n = days_since_j2000(noon_unix);
double delta;
sun_ecliptic(n, NULL, NULL, NULL, NULL, NULL, &delta);
double eot = equation_of_time(noon_unix); /* minutes */

double noon = 12.0 - eot / 60.0 - lon / 15.0;
if (solar_noon) *solar_noon = noon;

/* Hour angle (deg) at which the sun centre sits at altitude h (deg). */
double cosO = (sin(-0.833 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta));
if (cosO < -1.0 || cosO > 1.0) return 1; /* polar day / night: no rise or set */
double omega = acos(cosO) * RAD;
if (sunrise) *sunrise = noon - omega / 15.0;
if (sunset) *sunset = noon + omega / 15.0;

if (civil_dusk) {
double c = (sin(-6.0 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta));
double o = (c < -1.0 || c > 1.0) ? omega : acos(c) * RAD;
*civil_dusk = noon + o / 15.0;
}
if (nautical_dusk) {
double c = (sin(-12.0 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta));
double o = (c < -1.0 || c > 1.0) ? omega : acos(c) * RAD;
*nautical_dusk = noon + o / 15.0;
}
if (golden_dusk) {
double c = (sin(6.0 * DEG) - sin(lat * DEG) * sin(delta)) / (cos(lat * DEG) * cos(delta));
double o = (c < -1.0 || c > 1.0) ? omega : acos(c) * RAD;
*golden_dusk = noon + o / 15.0;
}
return 0;
}

double moon_phase(double unix_s) {
double n = days_since_j2000(unix_s);
double phase = fmod((n - 5.26) / 29.53059, 1.0);
if (phase < 0.0) phase += 1.0;
return phase;
}

double moon_illuminated_fraction(double phase) { return (1.0 - cos(2.0 * M_PI * phase)) / 2.0; }

int moon_phase_index(double phase) { return ((int)(phase * 8.0 + 0.5)) & 7; }

void maidenhead(double lat, double lon, char out[7]) {
if (!isfinite(lat) || !isfinite(lon)) { strcpy(out, "----"); return; }
lat = fmin(nextafter(90.0, -INFINITY), fmax(-90.0, lat));
lon = fmin(nextafter(180.0, -INFINITY), fmax(-180.0, lon));
double LON = lon + 180.0; /* [0,360) */
double LAT = lat + 90.0; /* [0,180) */

int f0 = (int)(LON / 20.0); if (f0 < 0) f0 = 0; if (f0 > 17) f0 = 17;
int f1 = (int)(LAT / 10.0); if (f1 < 0) f1 = 0; if (f1 > 17) f1 = 17;
int s2 = (int)(fmod(LON, 20.0) / 2.0); if (s2 < 0) s2 = 0; if (s2 > 9) s2 = 9;
int s3 = (int)(fmod(LAT, 10.0)); if (s3 < 0) s3 = 0; if (s3 > 9) s3 = 9;
int u4 = (int)(fmod(LON, 2.0) * 12.0); if (u4 < 0) u4 = 0; if (u4 > 23) u4 = 23;
int u5 = (int)(fmod(LAT, 1.0) * 24.0); if (u5 < 0) u5 = 0; if (u5 > 23) u5 = 23;

out[0] = (char)('A' + f0);
out[1] = (char)('A' + f1);
out[2] = (char)('0' + s2);
out[3] = (char)('0' + s3);
out[4] = (char)('a' + u4);
out[5] = (char)('a' + u5);
out[6] = '\0';
}
9 changes: 6 additions & 3 deletions mk4-time/Core/Src/chainloader.c
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,12 @@ void firmwareCheckOnEject(){
unsigned int rc;
FIL file1, file2;

// if QSPI is locked in this context, we have interrupted another fatfs read
// we can't wait for it to finish as it's running at lower priority
if (QSPI_Locked()) {delayedCheckOnEject=1; return;}
// This runs from the USB MSC eject command, i.e. in the USB OTG ISR. FATFS is not
// reentrant, so if the lower-priority main loop is mid-operation we must not touch it.
// QSPI_Locked() only catches an in-flight QSPI transfer; fatfs_busy covers the whole
// main-loop FATFS region, including the gaps between transfers where QSPI_Locked() reads
// false. If either says busy, defer back to the main loop (it polls delayedCheckOnEject).
if (fatfs_busy || QSPI_Locked()) {delayedCheckOnEject=1; return;}

if (f_open(&file2, "/FWT.BIN", FA_READ) == FR_OK) {
f_lseek(&file2, TIME_APP_SIZE - 4);
Expand Down
Loading