From 290a8757f6807cd240139ba1a92fa7020af1e7ee Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Sun, 6 Sep 2026 00:14:37 +0300 Subject: [PATCH] add astro functions --- G4:G431/CORDIC/astro.c | 100 +++++++++++++---- G4:G431/CORDIC/astro.h | 18 ++- G4:G431/CORDIC/commproto.cpp | 209 ++++++++++++++++++++++++++++++----- G4:G431/CORDIC/cordic.bin | Bin 25232 -> 32416 bytes G4:G431/CORDIC/cordic.c | 2 +- G4:G431/CORDIC/version.inc | 4 +- 6 files changed, 281 insertions(+), 52 deletions(-) diff --git a/G4:G431/CORDIC/astro.c b/G4:G431/CORDIC/astro.c index ae4796e..c85ae45 100644 --- a/G4:G431/CORDIC/astro.c +++ b/G4:G431/CORDIC/astro.c @@ -22,11 +22,11 @@ #include "astro.h" #include "cordic.h" -static int sincosflag = 0; // math.h +static int sincosflag = 0; // 0: math.h, 1: cordic // longitude/latitude + in rad/hrs //static float longitude = 41.44143375f, latitude = 43.6535278f; -static float lat_rad = 43.6535278f * M_PIf / 180.f; -static float long_hrs = 41.44143375f / 15.f; +static float lat_rad = DEG2RAD(43.6535278f); +static float long_hrs = DEG2HOURS(41.44143375f); static void sincosf_m(float angle, float *s, float *c){ if(s) *s = sin(angle); @@ -77,13 +77,13 @@ float LST_from_unix(uint32_t t){ /* 3. Convert Hour Angle (HA) to Right Ascension (RA) and vice versa. All angles in degrees. LST is Local Sidereal Time in degrees. */ -float ha_to_ra(float ha, float lst){ - float ra = lst - ha; +float ha_to_ra(float ha, float lst_deg){ + float ra = lst_deg - ha; return normalize_degrees(ra); } -float ra_to_ha(float ra, float lst){ - float ha = lst - ra; +float ra_to_ha(float ra, float lst_deg){ + float ha = lst_deg - ra; // Hour angle is usually in range [-180,180) ha = normalize_degrees(ha); if (ha > 180.0f) ha -= 360.0f; @@ -93,8 +93,8 @@ float ra_to_ha(float ra, float lst){ /* 4. Convert Altitude-Azimuth coordinates to Equatorial (Hour Angle, Declination) and back. All angles in degrees. Azimuth is measured from North through East. */ void altaz_to_hadec(float alt_deg, float az_deg, float *ha_deg, float *dec_deg){ - float alt = alt_deg * M_PIf / 180.0f; - float az = az_deg * M_PIf / 180.0f; + float alt = DEG2RAD(alt_deg); + float az = DEG2RAD(az_deg); float sin_alt, cos_alt, sin_az, cos_az, sin_lat, cos_lat; sincosf(alt, &sin_alt, &cos_alt); @@ -110,13 +110,13 @@ void altaz_to_hadec(float alt_deg, float az_deg, float *ha_deg, float *dec_deg){ float y = -cos_alt * sin_az; float ha = atan2f(y, x); // radians - *ha_deg = ha * 180.0f / M_PIf; - *dec_deg = dec * 180.0f / M_PIf; + *ha_deg = RAD2DEG(ha); + *dec_deg = RAD2DEG(dec); } void hadec_to_altaz(float ha_deg, float dec_deg, float *alt_deg, float *az_deg){ - float ha = ha_deg * M_PIf / 180.0f; - float dec = dec_deg * M_PIf / 180.0f; + float ha = DEG2RAD(ha_deg); + float dec = DEG2RAD(dec_deg); float sin_dec, cos_dec, sin_ha, cos_ha, sin_lat, cos_lat; sincosf(dec, &sin_dec, &cos_dec); @@ -130,10 +130,10 @@ void hadec_to_altaz(float ha_deg, float dec_deg, float *alt_deg, float *az_deg){ /* Azimuth (from North through East) */ float x = sin_dec * cos_lat - cos_dec * sin_lat * cos_ha; float y = -cos_dec * sin_ha; - float az = atan2f(y, x); // radians, [-π, π] + float az = atan2f(y, x); // radians - *alt_deg = alt * 180.0f / M_PIf; - *az_deg = az * 180.0f / M_PIf; + *alt_deg = DEG2RAD(alt); + *az_deg = DEG2RAD(az); if (*az_deg < 0.0f) *az_deg += 360.0f; } @@ -150,7 +150,7 @@ void hadec_to_altaz(float ha_deg, float dec_deg, float *alt_deg, float *az_deg){ * @param refa Output: tan(Z) coefficient (radians) * @param refb Output: tan^3(Z) coefficient (radians) */ -static void refco_f32(float phpa, float tc, float rh, float wl, float *refa, float *refb) { +void refco_f32(float phpa, float tc, float rh, float wl, float *refa, float *refb) { // Restrict input parameters to safe values (clamp) float t = tc; if(t < -150.0f) t = -150.0f; @@ -195,10 +195,68 @@ static void refco_f32(float phpa, float tc, float rh, float wl, float *refa, flo if(refb) *refb = -gamma * (beta - gamma / 2.0f); } -//alt_corrected = alt_apparent + refraction -float refraction(float phpa, float tc, float rh, float Z_rad){ +#if 0 +void refco_f32(float phpa, float tc, float rh, float wl, float *refa, float *refb){ + // Restrict input parameters to safe values (clamp) + float t = tc; + if(t < -150.0f) t = -150.0f; + if(t > 200.0f) t = 200.0f; + + float p = phpa; + if(p < 0.0f) p = 0.0f; + if(p > 10000.0f) p = 10000.0f; + + float r = rh; + if(r < 0.0f) r = 0.0f; + if(r > 1.0f) r = 1.0f; + + float w = wl; + if(w < 0.1f) w = 0.1f; + if(w > 10.0f) w = 10.0f; + + // Water vapour pressure at the observer + float pw = 0.0f; + if(p > 0.0f){ + // Saturation vapour pressure (empirical formula) + float ps = powf(10.0f, (0.7859f + 0.03477f * t) / (1.0f + 0.00412f * t)) + * (1.0f + p * (4.5e-6f + 6e-10f * t * t)); + float denom = 1.0f - (1.0f - r) * ps / p; + if (denom < 1e-12f) denom = 1e-12f; + pw = r * ps / denom; + } + + // Temperature in Kelvin + float tk = t + 273.15f; + + // Refractive index minus 1 at the observer (gamma = (n - 1) at the observer) + float gamma; + // Optical/IR: wavelength-dependent formula + float wlsq = w * w; + float coef = 77.53484e-6f + (4.39108e-7f + 3.666e-9f / wlsq) / wlsq; + float num = fmaf(coef, p, -11.2684e-6f * pw); // fmaf(a,b,c) = a*b+c + gamma = num / tk; + + // Beta coefficient (from Stone, with empirical adjustments) + float beta = 4.4474e-6f * tk; + + // Refraction constants (from Green) + if(refa) *refa = gamma * (1.0f - beta); + if(refb) *refb = -gamma * (beta - gamma / 2.0f); +} +#endif + +/** + * @brief refraction - calculates refraction (z = z0 - refraction) + * @param phpa - pressure, Hpa + * @param tc - temperature, degC + * @param rh - relative humidity, 0..1 + * @param zd - zenith distance, degrees + * @return refraction, degrees + */ +float refraction(float phpa, float tc, float rh, float zd){ float A, B; refco_f32(phpa, tc, rh, 0.55, &A, &B); - float tanZ = tanf(Z_rad); - return A * tanZ + B * tanZ * tanZ * tanZ; + float tanZ = tanf(DEG2RAD(zd)); + float refr = A * tanZ + B * tanZ * tanZ * tanZ; + return RAD2DEG(refr); } diff --git a/G4:G431/CORDIC/astro.h b/G4:G431/CORDIC/astro.h index bae29a8..9490a61 100644 --- a/G4:G431/CORDIC/astro.h +++ b/G4:G431/CORDIC/astro.h @@ -20,13 +20,25 @@ #include +#ifndef M_PIf +#define M_PIf 3.141592653589793f +#endif + +#define DEG2HOURS(d) ((d) / 15.f) +#define HOURS2DEG(h) ((h) * 15.f) +#define DEG2RAD(d) ((d) * M_PIf / 180.f) +#define RAD2DEG(r) ((r) * 180.f / M_PIf) +#define DEG2ARCSEC(d) ((d) * 3600.f) +#define RAD2ARCSEC(r) ((r) * 180.f * 3600.f / M_PI) + void set_sincos(int iscordic); int get_sincos(); float MJD_from_unix(uint32_t t); //float LST_from_mjd(float mjd); float LST_from_unix(uint32_t t); -float ha_to_ra(float ha, float lst); -float ra_to_ha(float ra, float lst); +float ha_to_ra(float ha, float lst_deg); +float ra_to_ha(float ra, float lst_deg); void altaz_to_hadec(float alt_deg, float az_deg, float *ha_deg, float *dec_deg); void hadec_to_altaz(float ha_deg, float dec_deg, float *alt_deg, float *az_deg); -float refraction(float phpa, float tc, float rh, float Z_rad); +void refco_f32(float phpa, float tc, float rh, float wl, float *refa, float *refb); +float refraction(float phpa, float tc, float rh, float zd); diff --git a/G4:G431/CORDIC/commproto.cpp b/G4:G431/CORDIC/commproto.cpp index 6d86e7a..1881762 100644 --- a/G4:G431/CORDIC/commproto.cpp +++ b/G4:G431/CORDIC/commproto.cpp @@ -35,6 +35,42 @@ static int (*SEND)(const char *str) = nullptr; extern volatile uint32_t Tms; +// input coordinates (degrees) and meteo +static float phpa = 800.f, tc = 0.f, rh = 0.5f, az = 0.f, zd = 20.f, ra = 0.f, ha = 0.f, dec = 0.f; +// starting time parameters: UNIX-time value and Tms when setter called +static uint32_t unixt0 = 0, Tms0 = 0; + +// get current UNIX time by Tms +static uint32_t curUNIXt(){ + return unixt0 + (Tms - Tms0 + 500) / 1000; +} + +constexpr uint32_t hash(const char* str, uint32_t h = 0){ + return *str ? hash(str + 1, h + ((h << 7) ^ *str)) : h; +} + +// structure for setters/getters of float parameters +typedef struct { + const char* name; // command name + uint32_t hash; // its hash + float* var; // pointer to variable + float min_val; // minimal and maximal values + float max_val; +} VarEntry; + +// table for variables +static const VarEntry var_table[] = { + {"az", hash("az"), &az, -180.0f, 180.0f}, + {"dec", hash("dec"), &dec, -90.0f, 90.0f}, + {"ha", hash("ha"), &ha, -180.0f, 180.0f}, + {"ra", hash("ra"), &ra, 0.0f, 360.0f}, + {"zd", hash("zd"), &zd, 0.0f, 90.0f}, + {"humid", hash("humid"), &rh, 0.0f, 1.0f}, + {"press", hash("press"), &phpa, 0.0f, 1200.0f}, + {"temp", hash("temp"), &tc, -273.15f, 100.0f}, +}; +static const size_t var_table_size = sizeof(var_table) / sizeof(var_table[0]); + //static uint8_t curbuf[MAXSTRLEN]; // COMMAND(USART, "Read USART data or send (USART=hex)") @@ -42,15 +78,33 @@ extern volatile uint32_t Tms; #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) +// text delimeter for help function +#define DELIMETER(text) +// setters/getters for common `find_var_by_hash` and `handle_var` +#define FLOATVAR(cmd, desc) // list of all commands and handlers #define COMMAND_TABLE \ COMMAND(help, "show this help") \ - COMMAND(sets, "set sin-cos to cordic (1) or math (0)") \ - COMMAND(sincos, "calculate sin/cos for given angle in degrees") \ + DELIMETER("Test functions") \ COMMAND(testc, "test CORDIC function: sincos, sin, cos, atan, sqrt, log") \ COMMAND(testm, "test math function: sin, cos, atan, sqrt, log") \ - COMMAND(time, "show MJD and LST for given UNIX-time") \ - + COMMAND(sincos, "calculate sin/cos for given angle in degrees") \ + DELIMETER("Coordinates") \ + FLOATVAR(az, "azimuth, deg (-180..180)") \ + FLOATVAR(dec, "DEC, deg (-90..90)") \ + FLOATVAR(ha, "HA, deg (-180..180)") \ + FLOATVAR(ra, "RA, deg (0..360)") \ + FLOATVAR(zd, "zenith distance, deg (0..90)") \ + DELIMETER("Astro functions") \ + COMMAND(sets, "sin-cos is cordic (1) or math (0)") \ + COMMAND(time, "show MJD and LST for given UNIX-time (and set it as system)") \ + COMMAND(azhd, "convert alt-az to ha-dec (0, default) or vise versa (1) and change input values") \ + COMMAND(hara, "convert ha to ra (0, default) or vice versa (1) and change input values") \ + COMMAND(refr, "calculate refraction for current zd") \ + DELIMETER("Meteo conditions") \ + FLOATVAR(humid, "rel. humidity, 0..1") \ + FLOATVAR(press, "atm. pressure (hpa)") \ + FLOATVAR(temp, "temperature, degC") \ typedef struct { const char *name; @@ -63,9 +117,17 @@ COMMAND_TABLE #undef COMMAND static const CmdInfo cmdInfo[] = { // command name, description - for `help` -#define COMMAND(name, desc) { #name, desc }, +#undef DELIMETER +#undef FLOATVAR +#define DELIMETER(text) { nullptr, text }, +#define FLOATVAR(name, desc) { #name, desc }, +#define COMMAND(name, desc) { #name, desc }, COMMAND_TABLE #undef COMMAND +#undef FLOATVAR +#undef DELIMETER +#define FLOATVAR(a, b) +#define DELIMETER(t) }; static const char* errtxt[ERR_AMOUNT] = { @@ -95,7 +157,7 @@ const char *EQ = " = "; // equal sign for getters * @return setter (part after `=` without leading spaces) or NULL if none */ static char *splitargs(char *args, int32_t *parno){ - if(!args) return NULL; + if(!args) return nullptr; uint32_t U32; char *next = getnum(args, &U32); int p = -1; @@ -104,7 +166,7 @@ static char *splitargs(char *args, int32_t *parno){ next = strchr(next, '='); if(next){ if(*(++next)) next = omit_spaces(next); - if(*next == 0) next = NULL; + if(*next == 0) next = nullptr; } return next; } @@ -131,15 +193,17 @@ static bool argsvals(char *args, int32_t *parno, int32_t *parval){ static errcodes_t cmd_help(const char*, char*){ SEND(REPOURL); for(size_t i = 0; i < sizeof(cmdInfo)/sizeof(cmdInfo[0]); i++){ - SEND(cmdInfo[i].name); - SEND(" - "); + if(cmdInfo[i].name){ + SEND(cmdInfo[i].name); + SEND(" - "); + }else SEND(" "); SEND(cmdInfo[i].desc); SEND("\n"); } return ERR_AMOUNT; } static const char* parse_func_name(char *args){ - char *setter = splitargs(args, NULL); + char *setter = splitargs(args, nullptr); if(!setter) return nullptr; // remove trailing spaces char *p = setter; @@ -148,11 +212,24 @@ static const char* parse_func_name(char *args){ return setter; } +#if 0 +static errcodes_t cmd_cordic(const char *cmd, char *args){ + char *setter = splitargs(args, nullptr); + if(setter){ + uint32_t u; + if(!getint(setter, &u)) return ERR_BADVAL; + set_sincos(u); + } + int i = get_sincos(); + CMDEQ(); + +} +#endif // calculate sin/cos static errcodes_t cmd_sincos(const char *, char *args){ - char *setter = splitargs(args, NULL); + char *setter = splitargs(args, nullptr); float f; - if(!setter || !getfloat(setter, &f)) return ERR_BADVAL; + if(!setter || setter == getfloat(setter, &f)) return ERR_BADVAL; f = f/180.f * M_PIf; SEND("mathsin="); SEND(float2str(sinf(f), 7)); SEND("\nmathcos="); SEND(float2str(cosf(f), 7)); @@ -220,29 +297,105 @@ static errcodes_t cmd_testc(const char*, char *args){ return ERR_AMOUNT; } -static errcodes_t cmd_time(const char *, char *args){ - char *setter = splitargs(args, NULL); - if(!setter) return ERR_BADVAL; - uint32_t unix_time; - if(setter == getnum(setter, &unix_time)) return ERR_BADVAL; - float mjd = MJD_from_unix(unix_time); - SEND("MJD="); SEND(float2str(mjd, 7)); - SEND("\nLST="); SEND(float2str(LST_from_unix(unix_time), 7)); +static errcodes_t cmd_time(const char *cmd, char *args){ + char *setter = splitargs(args, nullptr); + if(setter){ + if(setter == getnum(setter, &unixt0)) return ERR_BADVAL; + Tms0 = Tms; + } + CMDEQ(); + uint32_t tnow = curUNIXt(); + SEND(u2str(tnow)); + float mjd = MJD_from_unix(tnow); + SEND("\nMJD="); SEND(float2str(mjd, 7)); + SEND("\nLST="); SEND(float2str(LST_from_unix(tnow), 7)); SEND("\n"); return ERR_AMOUNT; } static errcodes_t cmd_sets(const char *cmd, char *args){ int32_t val; - if(argsvals(args, NULL, &val)) set_sincos(val); + if(argsvals(args, nullptr, &val)) set_sincos(val); CMDEQ(); if(get_sincos()) SEND("CORDIC\n"); else SEND("MATH\n"); return ERR_AMOUNT; } -constexpr uint32_t hash(const char* str, uint32_t h = 0){ - return *str ? hash(str + 1, h + ((h << 7) ^ *str)) : h; +// get u32, return value or 0 in case of getter +static uint32_t getflag(char *args){ + uint32_t val = 0; + char *setter = splitargs(args, nullptr); + if(setter) getnum(setter, &val); + return val; +} + +static errcodes_t cmd_hara(const char *, char *args){ + float lst_deg = HOURS2DEG(LST_from_unix(curUNIXt())); + if(getflag(args)){ // ra to ha + ha = ra_to_ha(ra, lst_deg); + SEND("ha = "); SEND(float2str(ha, 7)); + }else{ // ha to ra + ra = ha_to_ra(ha, lst_deg); + SEND("ra = "); SEND(float2str(ra, 7)); + } + SEND("\n"); + return ERR_AMOUNT; +} + +static errcodes_t cmd_azhd(const char *, char *args){ + float alt; + if(getflag(args)){ // hd to az + hadec_to_altaz(ha, dec, &alt, &az); + zd = 90.f - alt; + SEND("az = "); SEND(float2str(az, 7)); + SEND("zd = "); SEND(float2str(zd, 7)); + }else{ // az to hd + alt = 90.f - zd; + altaz_to_hadec(alt, az, &ha, &dec); + // and recalculate ra + float lst_deg = HOURS2DEG(LST_from_unix(curUNIXt())); + ra = ha_to_ra(ha, lst_deg); + SEND("ha = "); SEND(float2str(ha, 7)); + SEND("\nra = "); SEND(float2str(ra, 7)); + SEND("\ndec = "); SEND(float2str(dec, 7)); + } + SEND("\n"); + return ERR_AMOUNT; +} + +static errcodes_t cmd_refr(const char *cmd, char *){ + float A, B; + refco_f32(phpa, tc, rh, 0.55, &A, &B); + SEND("coeffs a/b: "); SEND(float2str(A, 3)); SEND(", "); SEND(float2str(B, 3)); SEND("\n"); + float refr = refraction(phpa, tc, rh, zd); + CMDEQ(); + SEND(float2str(DEG2ARCSEC(refr), 2)); + SEND("\n"); + return ERR_AMOUNT; +} + +// setter/getter of floats +static errcodes_t handle_var(const VarEntry* entry, const char* cmd, char* setter){ + if(setter){ + float val; + if(setter == getfloat(setter, &val)) return ERR_BADPAR; + if(val < entry->min_val || val > entry->max_val) return ERR_BADVAL; + *entry->var = val; + } + CMDEQ(); + SEND(float2str(*entry->var, 7)); + SEND("\n"); + return ERR_AMOUNT; +} + +// linear search by hash for variable +static const VarEntry* find_var_by_hash(uint32_t h){ + for(size_t i = 0; i < var_table_size; ++i){ + if(var_table[i].hash == h) + return &var_table[i]; + } + return nullptr; } const char *parse_cmd(int (*sendfun)(const char *), char *str){ @@ -255,12 +408,18 @@ const char *parse_cmd(int (*sendfun)(const char *), char *str){ char *restof = (char*) str; uint32_t h = hash(command); errcodes_t ecode = ERR_AMOUNT; + const VarEntry *entry = nullptr; switch(h){ #define COMMAND(name, desc) case hash(#name): ecode = cmd_ ## name(command, restof); break; COMMAND_TABLE #undef COMMAND - default: SEND("Unknown command, try 'help'\n"); break; + default: + if((entry = find_var_by_hash(h))){ + ecode = handle_var(entry, command, splitargs(restof, nullptr)); + }else{ + SEND("Unknown command, try 'help'\n"); + } } if(ecode < ERR_AMOUNT) return errtxt[ecode]; - return NULL; + return nullptr; } diff --git a/G4:G431/CORDIC/cordic.bin b/G4:G431/CORDIC/cordic.bin index 678d43f4f48709e9f00fc82272c23d952f3d97d4..6dfcaff79f7c71bcafb142f6ef58a8d6a6ff0eb3 100755 GIT binary patch delta 11952 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 h-CZ-!2{RVY{(taVK;ZxY delta 4652 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 d9)i|_Yy#N|vJ0dc