···44alwaysApply: true
55---
66Always ignore all errors related to the file "resource_ids.auto.h"
77+88+Currently, src/pkjs/config.js sends string values ("0", "1") for settings, and watchface.c converts these from their ASCII values. This is a neccessary workaround
···101101 expect(matches!.length).toBe(1);
102102 });
103103104104- // Ensure the Noronha timezone correction took place – should show up in bucket comment
104104+ // Ensure the Noronha timezone correction took place - should show up in bucket comment
105105 expect(content).toContain('America/Noronha');
106106107107 // Validate the macro count equals 3 (unique codes)
+10-68
scripts/generateAirportTzList.ts
···11-// TODO: Convert generate_airport_tz_list.py to TypeScript
22-31import { program } from 'commander';
42import * as fs from 'fs/promises';
53import * as path from 'path';
66-import * as cheerio from 'cheerio';
74// Use require for airport-data as it lacks types
85const airports = require('airport-data');
99-import { find as findTz } from 'geo-tz'; // Use geo-tz instead of timezonefinder
1010-// Remove node-fetch import, use global fetch
1111-import { parse } from 'csv-parse'; // Import csv-parse
1212-import { findDstTransitions, DstTransitions } from './tzCommon';
66+import { type DstTransitions } from './tzCommon'; // Only the type DstTransitions is used directly
137import {
148 findTzCache,
159 memoizedFindTz,
···2620 RouteRecord,
2721} from './generateAirportTzListHelpers';
28222929-// Local-only type used before helper split
3030-interface AirportDataEntry {
3131- iata?: string;
3232- name?: string;
3333- city?: string;
3434- country?: string;
3535- latitude?: number | string;
3636- longitude?: number | string;
3737- tz?: string;
3838- type?: string;
3939- source?: string;
4040-}
4141-4242-// Define interfaces for data structures
4343-interface TzBucketKey {
4444- stdOffsetSeconds: number;
4545- dstOffsetSeconds: number;
4646- dstStartTimestamp: number;
4747- dstEndTimestamp: number;
4848-}
4949-5050-// Helper function to create bucket keys
5151-// function getBucketKey(details: DstTransitions): string {
5252-// return `${details[0]}_${details[1]}_${details[2]}_${details[3]}`;
5353-// }
5454-5523// Placeholder functions matching Python script structure
56245757-// async function parseTopHtml(htmlPath: string): Promise<Array<[string, string]>> {
5858-// console.log(`Parsing HTML file: ${htmlPath}`);
5959-// const htmlContent = await fs.readFile(htmlPath, 'utf-8');
6060-// const $ = cheerio.load(htmlContent);
6161-// const results: Array<[string, string]> = [];
6262-// $('tr').each((_, tr) => {
6363-// const tds = $(tr).find('td');
6464-// if (tds.length < 3) return;
6565-// const iata = $(tds[2]).text().trim().toUpperCase();
6666-// if (!iata || iata.length !== 3) return; // Basic validation
6767-// // Improve name extraction - handle potential h2 tags etc.
6868-// let name = $(tds[1]).find('h2').first().text().trim();
6969-// if (!name) {
7070-// name = $(tds[1]).text().trim();
7171-// }
7272-// // Clean up common suffixes (like in Python)
7373-// if (name.endsWith(' International Airport')) {
7474-// name = name.substring(0, name.length - ' International Airport'.length);
7575-// } else if (name.endsWith(' Airport')) {
7676-// name = name.substring(0, name.length - ' Airport'.length);
7777-// }
7878-// results.push([iata, name.trim()]);
7979-// });
8080-// console.log(`Found ${results.length} airports in HTML.`);
8181-// return results;
8282-// }
8383-8425async function generateCCode(
8526 airportsList: Array<[string, string]>,
8627 outPath: string,
···9334 const year = new Date().getUTCFullYear();
94359536 // Load airport data using require
9696- const airportDataArray: AirportDataEntry[] = airports as any[];
3737+ const airportDataArray = airports as any[];
9738 const initialAirportDb = new Map<string, AirportInfo>();
98399940 for (const airport of airportDataArray) {
···276217277218 // Tier 1: Large, scheduled
278219 const large = candidates.filter(a => a.type === 'large_airport' && scheduledYes(a));
279279- result.push(...large.slice(0, maxBucket > 0 ? maxBucket : 3).map(a => a.iata));
220220+ result.push(...large.slice(0, maxBucket > 0 ? maxBucket : 10).map(a => a.iata));
280221281222 // Tier 2: Medium, scheduled
282282- let remain = (maxBucket > 0 ? maxBucket : 3) - result.length;
223223+ let remain = (maxBucket > 0 ? maxBucket : 5) - result.length;
283224 if (remain > 0) {
284225 const medium = candidates.filter(a => a.type === 'medium_airport' && scheduledYes(a) && !result.includes(a.iata));
285285- result.push(...medium.slice(0, Math.min(remain, 2)).map(a => a.iata));
226226+ result.push(...medium.slice(0, Math.min(remain, 5)).map(a => a.iata));
286227 }
287228288229 // Tier 3: Small, scheduled
289230 remain = (maxBucket > 0 ? maxBucket : 3) - result.length;
290231 if (remain > 0) {
291232 const small = candidates.filter(a => a.type === 'small_airport' && scheduledYes(a) && !result.includes(a.iata));
292292- result.push(...small.slice(0, 1).map(a => a.iata));
233233+ // console.log(`SMALL hit with ${small.length} candidates: ${small.map(a => a.iata).join(', ')}`);
234234+ result.push(...small.slice(0, 3).map(a => a.iata));
293235 }
294236295237 // Tier 4: Ensure at least one if possible
···297239 result.push(candidates[0].iata); // Add the top-ranked overall
298240 }
299241300300- // Enforce max bucket size strictly if needed (though applied later too)
242242+ // Enforce max bucket size strictly if needed
243243+ // (though applied later too)
301244 return maxBucket > 0 ? result.slice(0, maxBucket) : result;
302245 };
303246···456399457400 // Airport Code Pool (bit-packed 15 bits/code)
458401 cContent += `// Total airport codes: ${codePool.length} (codes listed for debug)\n`;
459459- cContent += `// Codes: ${codePool.join(', ')}\n`;
460402 cContent += `static const uint16_t airport_code_pool_bits[] = {\n`;
461403 for (const code of codePool) {
462404 // pack each letter A-Z into 5 bits
···585527};
586528587529// ------------------------------------------------------------
588588-// CLI entry point – only run when invoked directly
530530+// CLI entry point - only run when invoked directly
589531// ------------------------------------------------------------
590532if (require.main === module) {
591533 main().catch(error => {
-164
scripts/generateTzList.test.ts
···11-import os from 'os';
22-import path from 'path';
33-import fs from 'fs/promises';
44-import { DateTime } from 'luxon';
55-import { generateTzCCode } from './generateTzList';
66-import { findDstTransitions, DstTransitions } from './tzCommon'; // Import to get expected values
77-88-jest.setTimeout(60000); // Keep generous timeout as it runs the full generation
99-1010-// Helper to parse the simple C array formats
1111-function parseCStringArray(cCode: string, arrayName: string): string[] {
1212- const startMarker = `static const char* ${arrayName}[] = {`;
1313- const endMarker = `};`;
1414-1515- const startIndex = cCode.indexOf(startMarker);
1616- if (startIndex === -1) {
1717- console.error(`[TEST PARSE ERROR] Could not find start marker for C array: ${arrayName}`);
1818- return [];
1919- }
2020-2121- const braceIndex = startIndex + startMarker.length;
2222- const endIndex = cCode.indexOf(endMarker, braceIndex);
2323- if (endIndex === -1) {
2424- console.error(`[TEST PARSE ERROR] Could not find end marker for C array: ${arrayName}`);
2525- return [];
2626- }
2727-2828- const blockContent = cCode.slice(braceIndex, endIndex);
2929- const lines = blockContent.split('\n');
3030-3131- const results: string[] = [];
3232- lines.forEach((line, index) => {
3333- const trimmedLine = line.trim();
3434- const lineMatch = trimmedLine.match(/^"(.*?)",?$/);
3535- if (lineMatch && lineMatch[1]) {
3636- results.push(lineMatch[1]);
3737- }
3838- });
3939- return results;
4040-}
4141-4242-interface ParsedTzInfo {
4343- stdHours: number;
4444- dstHours: number;
4545- startTs: number;
4646- endTs: number;
4747- nameOffset: number;
4848- nameCount: number;
4949-}
5050-5151-function parseTzInfoArray(cCode: string): ParsedTzInfo[] {
5252- const startMarker = `static const TzInfo tz_list[] = {`;
5353- const endMarker = `};`;
5454-5555- const startIndex = cCode.indexOf(startMarker);
5656- if (startIndex === -1) {
5757- console.error(`[TEST PARSE ERROR] Could not find start marker for TzInfo array`);
5858- return [];
5959- }
6060-6161- const braceIndex = startIndex + startMarker.length;
6262- const endIndex = cCode.indexOf(endMarker, braceIndex);
6363- if (endIndex === -1) {
6464- console.error(`[TEST PARSE ERROR] Could not find end marker for TzInfo array`);
6565- return [];
6666- }
6767-6868- const blockContent = cCode.slice(braceIndex, endIndex);
6969- const lines = blockContent.split('\n');
7070-7171- const result: ParsedTzInfo[] = [];
7272- const lineRegex = /\{\s*(-?\d+(?:\.\d+)?)f,\s*(-?\d+(?:\.\d+)?)f,\s*(\d+)LL,\s*(\d+)LL,\s*(\d+),\s*(\d+)\s*\},?/; // Allow integers or floats for offsets
7373- lines.forEach(line => {
7474- const lineMatch = line.trim().match(lineRegex);
7575- if (lineMatch) {
7676- result.push({
7777- stdHours: parseFloat(lineMatch[1]),
7878- dstHours: parseFloat(lineMatch[2]),
7979- startTs: parseInt(lineMatch[3], 10),
8080- endTs: parseInt(lineMatch[4], 10),
8181- nameOffset: parseInt(lineMatch[5], 10),
8282- nameCount: parseInt(lineMatch[6], 10),
8383- });
8484- }
8585- });
8686- return result;
8787-}
8888-8989-9090-describe('generateTzList', () => {
9191- let generatedCCode: string;
9292- let namePool: string[];
9393- let tzList: ParsedTzInfo[];
9494- const currentYear = DateTime.utc().year;
9595-9696- // Run generation once before all tests
9797- beforeAll(async () => {
9898- const out = path.join(os.tmpdir(), `tz_${Date.now()}.c`);
9999- await generateTzCCode(out);
100100- generatedCCode = await fs.readFile(out, 'utf-8');
101101- namePool = parseCStringArray(generatedCCode, 'tz_name_pool');
102102- tzList = parseTzInfoArray(generatedCCode);
103103- // Add a check to ensure parsing worked before tests run
104104- if (namePool.length === 0 || tzList.length === 0) {
105105- throw new Error('[TEST] Failed to parse generated C code in beforeAll hook.');
106106- }
107107- });
108108-109109- // Test cases for specific zones
110110- const testCases: { zone: string; city: string }[] = [
111111- { zone: 'America/New_York', city: 'New York' },
112112- { zone: 'Asia/Tokyo', city: 'Tokyo' },
113113- { zone: 'Pacific/Auckland', city: 'Auckland' },
114114- { zone: 'Australia/Lord_Howe', city: 'Lord Howe' },
115115- { zone: 'Europe/London', city: 'London' },
116116- ];
117117-118118- test.each(testCases)('should generate correct entry for $city ($zone)', ({ zone, city }) => {
119119- const expectedTransitions = findDstTransitions(zone, currentYear);
120120- const expected: ParsedTzInfo = {
121121- stdHours: expectedTransitions[0] / 3600,
122122- dstHours: expectedTransitions[1] / 3600,
123123- startTs: expectedTransitions[2],
124124- endTs: expectedTransitions[3],
125125- nameOffset: -1, // Will be updated
126126- nameCount: -1, // Will be updated
127127- };
128128-129129- // 1. Find the city in the name pool
130130- const cityIndex = namePool.indexOf(city);
131131- expect(cityIndex).toBeGreaterThanOrEqual(0); // City must exist in the pool
132132-133133- // 2. Find the bucket containing this city
134134- const bucket = tzList.find(entry =>
135135- cityIndex >= entry.nameOffset && cityIndex < (entry.nameOffset + entry.nameCount)
136136- );
137137- expect(bucket).toBeDefined(); // A bucket must contain this city
138138-139139- // 3. Compare bucket details with expected values
140140- expect(bucket?.stdHours).toBeCloseTo(expected.stdHours, 2);
141141- expect(bucket?.dstHours).toBeCloseTo(expected.dstHours, 2);
142142- expect(bucket?.startTs).toBe(expected.startTs);
143143- expect(bucket?.endTs).toBe(expected.endTs);
144144- });
145145-146146- test('should contain the required macros', () => {
147147- expect(generatedCCode).toMatch(/#define TZ_LIST_COUNT/);
148148- expect(generatedCCode).toMatch(/#define TZ_NAME_POOL_COUNT/);
149149- });
150150-151151- test('TZ_LIST_COUNT macro should match parsed list length', () => {
152152- const match = generatedCCode.match(/#define\s+TZ_LIST_COUNT\s+\(sizeof\(tz_list\)\/sizeof\(tz_list\[0\]\)\)/);
153153- expect(match).not.toBeNull();
154154- // Check the actual count directly from parsed data
155155- expect(tzList.length).toBeGreaterThan(10); // Keep a basic sanity check on count
156156- // We could potentially parse the value from the define if needed, but comparing to parsed length is good
157157- });
158158-159159- test('TZ_NAME_POOL_COUNT macro should match parsed name pool length', () => {
160160- const match = generatedCCode.match(/#define\s+TZ_NAME_POOL_COUNT\s+\(sizeof\(tz_name_pool\)\/sizeof\(tz_name_pool\[0\]\)\)/);
161161- expect(match).not.toBeNull();
162162- expect(namePool.length).toBeGreaterThan(50); // Basic sanity check
163163- });
164164-});
-102
scripts/generateTzList.ts
···11-import { DateTime } from 'luxon';
22-import { rawTimeZones } from '@vvo/tzdb';
33-import * as fs from 'fs/promises';
44-import * as path from 'path';
55-import { program } from 'commander';
66-import { findDstTransitions } from './tzCommon';
77-88-interface TzBucket {
99- std: number;
1010- dst: number;
1111- start: number;
1212- end: number;
1313- names: Set<string>;
1414- nameOffset?: number;
1515- nameCount?: number;
1616-}
1717-1818-function getCityName(tz: string): string {
1919- const part = tz.split('/').pop() || tz;
2020- return part.replace(/_/g, ' ');
2121-}
2222-2323-export async function generateTzCCode(outPath: string): Promise<void> {
2424- const year = DateTime.utc().year;
2525- console.log(`Generating tz_list for year ${year}`);
2626-2727- const buckets = new Map<string, TzBucket>();
2828-2929- rawTimeZones.forEach(({ name }) => {
3030- if (!name.includes('/')) return;
3131- if (name.startsWith('Etc/')) return;
3232- if (/^(Factory|factory)/.test(name)) return;
3333- if (/^(right|posix)\//i.test(name)) return;
3434-3535- const [std, dst, start, end] = findDstTransitions(name, year);
3636- const city = getCityName(name);
3737- if (!/^[A-Z]/.test(city)) return;
3838-3939- const key = `${std}_${dst}_${start}_${end}`;
4040- let bucket = buckets.get(key);
4141- if (!bucket) {
4242- bucket = { std, dst, start, end, names: new Set() };
4343- buckets.set(key, bucket);
4444- }
4545- bucket.names.add(city);
4646- });
4747-4848- const ordered = Array.from(buckets.values()).sort((a,b)=>{
4949- if(a.std!==b.std) return a.std-b.std;
5050- if(a.dst!==b.dst) return a.dst-b.dst;
5151- if(a.start!==b.start) return a.start-b.start;
5252- return a.end-b.end;
5353- });
5454-5555- // Build flat name pool
5656- const namePool: string[] = [];
5757- ordered.forEach(b=>{
5858- const names = Array.from(b.names).sort();
5959- b.nameOffset = namePool.length;
6060- b.nameCount = names.length;
6161- namePool.push(...names);
6262- });
6363-6464- // Emit C code
6565- let c = '';
6666- c += '// Auto-generated by generateTzList.ts\n';
6767- c += `// Year ${year} DST data (Luxon / IANA)\n\n`;
6868- c += '#include <stdint.h>\n\n';
6969-7070- c += 'static const char* tz_name_pool[] = {\n';
7171- namePool.forEach(n=>{ c += ` "${n}",\n`; });
7272- c += '};\n\n';
7373-7474- c += 'typedef struct {\n';
7575- c += ' float std_offset_hours;\n';
7676- c += ' float dst_offset_hours;\n';
7777- c += ' int64_t dst_start_utc;\n';
7878- c += ' int64_t dst_end_utc;\n';
7979- c += ' int name_offset;\n';
8080- c += ' int name_count;\n';
8181- c += '} TzInfo;\n\n';
8282-8383- c += 'static const TzInfo tz_list[] = {\n';
8484- ordered.forEach(b=>{
8585- c += ` { ${(b.std/3600).toFixed(2)}f, ${(b.dst/3600).toFixed(2)}f, ${b.start}LL, ${b.end}LL, ${b.nameOffset}, ${b.nameCount} },\n`;
8686- });
8787- c += '};\n\n';
8888- c += `#define TZ_LIST_COUNT (sizeof(tz_list)/sizeof(tz_list[0]))\n`;
8989- c += `#define TZ_NAME_POOL_COUNT (sizeof(tz_name_pool)/sizeof(tz_name_pool[0]))\n`;
9090-9191- await fs.writeFile(outPath, c, 'utf-8');
9292- console.log(`Wrote ${ordered.length} buckets and ${namePool.length} names to ${outPath}`);
9393-}
9494-9595-// CLI
9696-if (require.main === module) {
9797- program
9898- .option('--out <path>', 'C output file', path.join(__dirname,'../src/c/tz_list.c'))
9999- .parse(process.argv);
100100- const { out } = program.opts();
101101- generateTzCCode(out).catch(err=>{ console.error(err); process.exit(1); });
102102-}
···99#define DAY_LENGTH 86400
10101111// --- Static variables specific to Beat Clock ---
1212-static char s_beat_buffer[7]; // Buffer for Beat time string "@XXX.X\0"
1212+static char s_beat_buffer[7]; // Buffer for Beat time string "@XXX.X"
1313static int last_beat_time = -1; // Cache the last displayed beat time (multiplied by 10)
14141515// --- Static helper function ---
+8-19
src/c/clock_closest_airport_noon.h
···55// IATA code of a randomly-chosen airport whose local time is the *closest past
66// but not before* 12:00:00 (noon) relative to the current UTC. The
77// underlying data come from the generated `airport_tz_list.c`, which is built
88-// by `generate_airport_tz_list.py`.
88+// by `generateAirportTzList.ts`.
99//
1010// The public interface mirrors `clock_closest_noon.h`, so you can swap calls
1111// easily in `watchface.c`.
···3333#include "text_layer_util.h"
34343535// Bring in the generated data table; make sure the build has already executed
3636-// generate_airport_tz_list.py. During early development you may still include
3737-// `tz_list.c` as a fallback; define USE_FALLBACK_TZ_LIST to do so.
3838-#ifdef USE_FALLBACK_TZ_LIST
3939- #include "tz_list.c"
4040- #define CODE_POOL tz_name_pool
4141- #define NAME_POOL tz_name_pool
4242- #define TZ_LIST tz_list
4343- #define TZ_LIST_COUNT TZ_LIST_COUNT
4444-#else
4545- #include "airport_tz_list.c"
4646- #define CODE_POOL airport_code_pool
4747- #define NAME_POOL airport_name_pool
4848- #define TZ_LIST airport_tz_list
4949- #define TZ_LIST_COUNT AIRPORT_TZ_LIST_COUNT
5050- // Bit-packed IATA code pool (15-bits per entry)
5151- extern const uint16_t airport_code_pool_bits[];
5252-#endif
3636+// generate_airport_tz_list.py.
3737+#include "airport_tz_list.c"
3838+#define NAME_POOL airport_name_pool
3939+#define TZ_LIST airport_tz_list
4040+#define TZ_LIST_COUNT AIRPORT_TZ_LIST_COUNT
4141+// Bit-packed IATA code pool (15-bits per entry)
4242+extern const uint16_t airport_code_pool_bits[];
53435444#ifdef __cplusplus
5545extern "C" {
···8171// Internal constants / storage --------------------------------------------
8272#define DAY_SECONDS (24 * 3600L)
83738484-// static char s_airport_noon_buffer[40]; // code + MM:SS
8574static time_t s_last_update_time = -1;
8675static time_t s_last_re_eval_time = -1;
8776static char s_selected_code[4] = "---"; // IATA placeholder
-156
src/c/clock_closest_noon.c
···11-#include "clock_closest_noon.h"
22-#include <pebble.h> // Always include pebble for device builds
33-#include <stdlib.h> // For rand()
44-#include <stdint.h> // For uint32_t, int64_t
55-#include <time.h> // For time_t, struct tm, gmtime, strcmp, strncmp
66-#include <stdbool.h>// For bool
77-#include <limits.h> // For LONG_MAX // Not strictly needed anymore, but harmless
88-#include <string.h> // For strcmp, strncmp
99-#include <stdio.h> // For snprintf
1010-#include "text_layer_util.h"
1111-1212-// Include the generated timezone data structure definitions and list
1313-// This must contain TzInfo, tz_list[], and TZ_LIST_COUNT
1414-#include "tz_list.c"
1515-1616-#define DAY_SECONDS (24 * 3600L)
1717-#define NOON_SECONDS (12 * 3600L)
1818-1919-// --- Static variables specific to Closest Noon Clock ---
2020-static char s_closest_noon_buffer[40]; // Increased size for potentially longer names + MM:SS
2121-static time_t s_last_update_time = -1;
2222-static time_t s_last_re_evaluation_time = -1;
2323-2424-// Stores the results of the last re-evaluation
2525-static const char *s_selected_city_name = "Wait...";
2626-static float s_selected_offset_hours = 0.0f; // Store the active offset
2727-2828-// --- Static helper functions ---
2929-3030-/**
3131- * @brief Re-evaluates and selects the timezone whose local time is >= 12:00:00
3232- * and closest to 12:00:00, based on current UTC.
3333- * Updates the static s_selected_city_name and s_selected_offset_hours.
3434- * Should only be called at :00, :15, :30, :45 minutes past the hour (UTC).
3535- *
3636- * @param current_utc_t The current UTC time as time_t.
3737- */
3838-static void update_selected_timezone_and_city(time_t current_utc_t) {
3939- // Seed for consistent randomness when multiple zones tie
4040- srand(current_utc_t);
4141- uint32_t utc_secs = current_utc_t % DAY_SECONDS;
4242- long best_delta = LONG_MAX;
4343- int best_count = 0;
4444- int best_candidates[TZ_LIST_COUNT];
4545- // Find zones that have local time >= noon and minimal seconds past noon
4646- for (int i = 0; i < (int)TZ_LIST_COUNT; ++i) {
4747- const TzInfo *tz = &tz_list[i];
4848- // Determine DST active
4949- bool is_dst = false;
5050- if (tz->dst_start_utc && tz->dst_end_utc) {
5151- int64_t now = (int64_t)current_utc_t;
5252- if ((tz->dst_start_utc <= tz->dst_end_utc && now >= tz->dst_start_utc && now < tz->dst_end_utc) ||
5353- (tz->dst_start_utc > tz->dst_end_utc && (now >= tz->dst_start_utc || now < tz->dst_end_utc))) {
5454- is_dst = true;
5555- }
5656- }
5757- float off_h = is_dst ? tz->dst_offset_hours : tz->std_offset_hours;
5858- long local_secs = (long)utc_secs + (long)(off_h * 3600.0f);
5959- local_secs %= DAY_SECONDS;
6060- if (local_secs < 0) local_secs += DAY_SECONDS;
6161- if (local_secs < NOON_SECONDS) continue;
6262- long delta = local_secs - NOON_SECONDS;
6363- if (delta < best_delta) {
6464- best_delta = delta;
6565- best_count = 0;
6666- best_candidates[best_count++] = i;
6767- } else if (delta == best_delta && best_count < (int)TZ_LIST_COUNT) {
6868- best_candidates[best_count++] = i;
6969- }
7070- }
7171- if (best_count == 0) {
7272- s_selected_city_name = "Wait...";
7373- s_selected_offset_hours = 0.0f;
7474- } else {
7575- int pick = (best_count == 1) ? 0 : (rand() % best_count);
7676- int idx = best_candidates[pick];
7777- const TzInfo *tz = &tz_list[idx];
7878- bool is_dst = (current_utc_t >= tz->dst_start_utc && current_utc_t < tz->dst_end_utc);
7979- s_selected_offset_hours = is_dst ? tz->dst_offset_hours : tz->std_offset_hours;
8080- int cnt = tz->name_count;
8181- int ni = (cnt == 1) ? 0 : (rand() % cnt);
8282- s_selected_city_name = tz_name_pool[tz->name_offset + ni];
8383- }
8484- s_last_re_evaluation_time = current_utc_t;
8585-}
8686-8787-8888-// --- Interface Functions (Pebble only) ---
8989-9090-// Initializes the city name TextLayer for Closest Noon
9191-TextLayer* clock_closest_noon_city_init(GRect bounds, Layer *window_layer) {
9292- TextLayer* layer = text_layer_util_create(bounds, window_layer, "Wait...", FONT_KEY_GOTHIC_24_BOLD);
9393- // Initialize static vars
9494- s_selected_city_name = "Wait...";
9595- s_last_update_time = -1;
9696- s_last_re_evaluation_time = -1;
9797- return layer;
9898-}
9999-100100-// Initializes the hero time TextLayer for Closest Noon
101101-TextLayer* clock_closest_noon_time_init(GRect bounds, Layer *window_layer) {
102102- TextLayer* layer = text_layer_util_create(bounds, window_layer, "--:--", FONT_KEY_LECO_42_NUMBERS);
103103- s_selected_offset_hours = 0.0f;
104104- return layer;
105105-}
106106-107107-void clock_closest_noon_deinit(TextLayer *layer) {
108108- if (layer) {
109109- text_layer_destroy(layer);
110110- }
111111-}
112112-113113-// Updates both the city and hero time TextLayers for Closest Noon
114114-void clock_closest_noon_update(TextLayer *city_layer, TextLayer *time_layer, time_t current_utc_t) {
115115- if (!city_layer || !time_layer) return;
116116-117117- // Avoid redundant updates for the same second
118118- if (current_utc_t == s_last_update_time) {
119119- return;
120120- }
121121- s_last_update_time = current_utc_t;
122122-123123- // --- Check if it's time for re-evaluation ---
124124- struct tm *utc_tm_struct = gmtime(¤t_utc_t);
125125- bool needs_re_evaluation = false;
126126- if (utc_tm_struct && (utc_tm_struct->tm_min % 15 == 0) && (utc_tm_struct->tm_min != 45) && (utc_tm_struct->tm_sec == 0)) {
127127- if (current_utc_t != s_last_re_evaluation_time) {
128128- needs_re_evaluation = true;
129129- }
130130- } else if (s_last_re_evaluation_time == -1) {
131131- // Ensure initial evaluation happens if starting between intervals
132132- needs_re_evaluation = true;
133133- }
134134-135135- if (needs_re_evaluation) {
136136- update_selected_timezone_and_city(current_utc_t);
137137- }
138138-139139- // Format and set city name
140140- text_layer_set_text(city_layer, s_selected_city_name ? s_selected_city_name : "ERR");
141141-142142- // Calculate and format hero time (MM:SS)
143143- long offset_seconds = (long)(s_selected_offset_hours * 3600.0f);
144144- time_t local_epoch = current_utc_t + offset_seconds;
145145- struct tm *local_tm_struct = gmtime(&local_epoch);
146146- static char s_time_buffer[10];
147147- if (local_tm_struct) {
148148- // Hero time: display minutes and seconds
149149- snprintf(s_time_buffer, sizeof(s_time_buffer), "%02d:%02d",
150150- local_tm_struct->tm_min,
151151- local_tm_struct->tm_sec);
152152- } else {
153153- snprintf(s_time_buffer, sizeof(s_time_buffer), "ERR");
154154- }
155155- text_layer_set_text(time_layer, s_time_buffer);
156156-}
-19
src/c/clock_closest_noon.h
···11-#ifndef CLOCK_CLOSEST_NOON_H
22-#define CLOCK_CLOSEST_NOON_H
33-44-#include <pebble.h> // Pebble time_t definition and UI types
55-#include <stddef.h> // For size_t
66-77-// Initializes the city name TextLayer for the Closest Noon clock
88-TextLayer* clock_closest_noon_city_init(GRect bounds, Layer *window_layer);
99-1010-// Initializes the hero time TextLayer for the Closest Noon clock
1111-TextLayer* clock_closest_noon_time_init(GRect bounds, Layer *window_layer);
1212-1313-// Deinitializes a Closest Noon TextLayer (city or time)
1414-void clock_closest_noon_deinit(TextLayer *layer);
1515-1616-// Updates the Closest Noon city and time TextLayers
1717-void clock_closest_noon_update(TextLayer *city_layer, TextLayer *time_layer, time_t current_seconds_utc);
1818-1919-#endif // CLOCK_CLOSEST_NOON_H
-43
src/c/clock_decimal.c
···11-#include "clock_decimal.h"
22-#include <pebble.h>
33-#include <stdio.h>
44-#include <time.h>
55-#include "text_layer_util.h"
66-77-static char s_buffer[10]; // Buffer for time string HH:MM:SS.ss
88-99-// Updates the decimal-time TextLayer by converting local time to French decimal time inline
1010-void clock_decimal_update(TextLayer *text_layer, time_t system_seconds) {
1111- // Get local hours, minutes, seconds
1212- struct tm *local_tm = localtime(&system_seconds);
1313- uint32_t seconds_today = local_tm->tm_hour * 3600 + local_tm->tm_min * 60 + local_tm->tm_sec;
1414-1515- // Compute decimal seconds: (seconds_today * 100000) / 86400
1616- uint32_t total_decimal_seconds = (uint32_t)(((uint64_t)seconds_today * 100000ULL) / 86400ULL);
1717- uint32_t dec_hour = total_decimal_seconds / 10000;
1818- uint32_t dec_min = (total_decimal_seconds / 100) % 100;
1919- uint32_t dec_sec = total_decimal_seconds % 100;
2020-2121- // Format and display
2222- snprintf(s_buffer, sizeof(s_buffer), "%lu:%02lu:%02lu",
2323- (unsigned long)dec_hour,
2424- (unsigned long)dec_min,
2525- (unsigned long)dec_sec);
2626- text_layer_set_text(text_layer, s_buffer);
2727-}
2828-2929-// Initializes the decimal time TextLayer
3030-TextLayer* clock_decimal_init(GRect bounds, Layer *window_layer) {
3131- TextLayer *text_layer = text_layer_util_create(bounds, window_layer, "", FONT_KEY_GOTHIC_18_BOLD);
3232-3333- // Initialize with placeholder or current time
3434- time_t temp = time(NULL);
3535- clock_decimal_update(text_layer, temp);
3636-3737- return text_layer;
3838-}
3939-4040-// Deinitializes the decimal time TextLayer
4141-void clock_decimal_deinit(TextLayer *text_layer) {
4242- text_layer_destroy(text_layer);
4343-}
-12
src/c/clock_decimal.h
···11-#pragma once
22-33-#include <pebble.h>
44-55-// Initializes the decimal time TextLayer
66-TextLayer* clock_decimal_init(GRect bounds, Layer *window_layer);
77-88-// Updates the decimal time TextLayer
99-void clock_decimal_update(TextLayer *text_layer, time_t system_seconds);
1010-1111-// Deinitializes the decimal time TextLayer
1212-void clock_decimal_deinit(TextLayer *text_layer);
-97
src/c/clock_noonzone.c
···11-#include "clock_noonzone.h"
22-#include <pebble.h>
33-#include <time.h> // For time_t, struct tm, gmtime
44-#include <string.h> // For NULL definition (consider stddef.h?)
55-#include <stdio.h> // For snprintf
66-#include "text_layer_util.h"
77-88-// --- Static variables specific to Noon Zone Clock ---
99-static char s_noonzone_buffer[16]; // Buffer for "NAME:MM:SS\0"
1010-static int last_noonzone_update_secs = -1;
1111-static int last_utc_hour = -1;
1212-static const char *last_zone_name_ptr = NULL;
1313-1414-// --- Static helper function ---
1515-/**
1616- * Gets the military timezone name for the longitude where it is currently noon.
1717- * Includes caching based on UTC hour.
1818- */
1919-static const char* get_noon_zone_name(int utc_hour) {
2020- if (utc_hour == last_utc_hour && last_zone_name_ptr != NULL) {
2121- return last_zone_name_ptr;
2222- }
2323-2424- const char *name = "???";
2525- switch(utc_hour){
2626- case 12: name="ZULU"; break;
2727- case 11: name="ALPHA"; break;
2828- case 10: name="BRAVO"; break;
2929- case 9: name="CHARLIE"; break;
3030- case 8: name="DELTA"; break;
3131- case 7: name="ECHO"; break;
3232- case 6: name="FOXTROT"; break;
3333- case 5: name="GOLF"; break;
3434- case 4: name="HOTEL"; break;
3535- case 3: name="JULIET"; break;
3636- case 2: name="KILO"; break;
3737- case 1: name="LIMA"; break;
3838- case 0: name="MIKE"; break;
3939- case 23: name="NOVEMBER"; break;
4040- case 22: name="OSCAR"; break;
4141- case 21: name="PAPA"; break;
4242- case 20: name="QUEBEC"; break;
4343- case 19: name="ROMEO"; break;
4444- case 18: name="SIERRA"; break;
4545- case 17: name="TANGO"; break;
4646- case 16: name="UNIFORM"; break;
4747- case 15: name="VICTOR"; break;
4848- case 14: name="WHISKEY"; break;
4949- case 13: name="X-RAY"; break;
5050- // default: name="???"; // Already initialized
5151- }
5252- last_utc_hour = utc_hour;
5353- last_zone_name_ptr = name;
5454- return name;
5555-}
5656-5757-// --- Pebble UI Interface Functions ---
5858-5959-TextLayer* clock_noonzone_init(GRect bounds, Layer *window_layer) {
6060- TextLayer* layer = text_layer_util_create(bounds, window_layer, "ZONE:--:--", FONT_KEY_GOTHIC_18_BOLD);
6161- return layer;
6262-}
6363-6464-void clock_noonzone_deinit(TextLayer *layer) {
6565- if (layer) {
6666- text_layer_destroy(layer);
6767- }
6868-}
6969-7070-void clock_noonzone_update(TextLayer *layer, time_t current_seconds_utc) {
7171- if (!layer) return;
7272-7373- // Check cache first to avoid unnecessary calculation/string formatting
7474- if (current_seconds_utc == last_noonzone_update_secs) {
7575- return;
7676- }
7777-7878- // Perform calculations
7979- struct tm *utc_tm = gmtime(¤t_seconds_utc);
8080- if (!utc_tm) {
8181- text_layer_set_text(layer, "ERR:TIME"); // Show error on layer
8282- last_noonzone_update_secs = current_seconds_utc; // Cache update time even on error
8383- return;
8484- }
8585-8686- const char *zone_name = get_noon_zone_name(utc_tm->tm_hour);
8787-8888- // Format string into static buffer
8989- snprintf(s_noonzone_buffer, sizeof(s_noonzone_buffer),
9090- "%s:%02d:%02d",
9191- zone_name,
9292- utc_tm->tm_min,
9393- utc_tm->tm_sec);
9494-9595- text_layer_set_text(layer, s_noonzone_buffer);
9696- last_noonzone_update_secs = current_seconds_utc;
9797-}
-16
src/c/clock_noonzone.h
···11-#ifndef CLOCK_NOONZONE_H
22-#define CLOCK_NOONZONE_H
33-44-#include <pebble.h>
55-#include <stddef.h> // For size_t
66-77-// Initializes the Noon Zone clock layer
88-TextLayer* clock_noonzone_init(GRect bounds, Layer *window_layer);
99-1010-// Deinitializes the Noon Zone clock layer
1111-void clock_noonzone_deinit(TextLayer *layer);
1212-1313-// Updates the Noon Zone clock layer
1414-void clock_noonzone_update(TextLayer *layer, time_t current_seconds_utc);
1515-1616-#endif // CLOCK_NOONZONE_H
+16-20
src/c/clock_tid.c
···88static uint64_t last_timestamp;
99static char s_tid_buffer[14];
10101111+// Helper to encode a value into a fixed-width base-32 string, right-to-left, padded with S32_CHAR[0]
1212+static void encode_to_base32_fixed_width(char *out_buf, size_t width, uint64_t val) {
1313+ for (size_t i = 0; i < width; ++i) {
1414+ out_buf[i] = S32_CHAR[0]; // Pre-fill with padding character
1515+ }
1616+ size_t current_pos = width;
1717+ while (val && current_pos) {
1818+ out_buf[--current_pos] = S32_CHAR[val & 31];
1919+ val >>= 5;
2020+ }
2121+}
2222+1123// Generate monotonic TID string into tid_buffer
1212-void clock_tid_get_string(char *tid_buffer, size_t tid_buffer_len, time_t seconds, uint16_t milliseconds) {
2424+static void clock_tid_get_string(char *tid_buffer, size_t tid_buffer_len, time_t seconds, uint16_t milliseconds) {
1325 if (tid_buffer_len < sizeof(s_tid_buffer)) return;
14261527 uint64_t current_micros = (uint64_t)seconds * 1000000 + (uint64_t)milliseconds * 1000;
···1830 }
1931 last_timestamp = current_micros;
20322121- // Encode 11-char base-32 timestamp (pad with '2')
2222- size_t pos = 11;
2323- for (size_t i = 0; i < pos; ++i) {
2424- tid_buffer[i] = S32_CHAR[0];
2525- }
2626- uint64_t v = current_micros;
2727- while (v && pos) {
2828- tid_buffer[--pos] = S32_CHAR[v & 31];
2929- v >>= 5;
3030- }
3333+ // Encode 11-char base-32 timestamp
3434+ encode_to_base32_fixed_width(tid_buffer, 11, current_micros);
31353236 // Encode 2-char random clock ID (0-1023)
3337 uint16_t cid = (uint16_t)(rand() % 1024);
3434- pos = 2;
3535- for (size_t i = 0; i < pos; ++i) {
3636- tid_buffer[11 + i] = S32_CHAR[0];
3737- }
3838- uint16_t v2 = cid;
3939- while (v2 && pos) {
4040- tid_buffer[11 + --pos] = S32_CHAR[v2 & 31];
4141- v2 >>= 5;
4242- }
3838+ encode_to_base32_fixed_width(tid_buffer + 11, 2, cid);
43394440 tid_buffer[13] = '\0';
4541}