Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | 1x 1x 1x 1x 10x 9x 9x 9x 9x 32x 32x 32x 32x 32x 32x 25x 25x 25x 25x 25x 9x 9x 9x 9x 1x 1x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | #!/usr/bin/env node
/**
* Generates the C weather-condition lookups from the shared vocabulary.
*
* Reads the framework's src/ts/weather/conditions.ts and src/ts/pkjs/wire.ts, the two tables the phone
* owns, and writes
* src/c/pebble/ui/weather/icons_table.g.h (token -> icon resource)
* src/c/core/wire/wire_caps.g.h (the strip caps both sides bound against)
*
* Each header carries its own Doxygen blocks, so the generated lookups show up in the framework docs
* like any hand-written file.
*
* The headers are committed. The host C suite and Doxygen read them straight from the tree, a face
* build compiles them as they are, and the spec fails when one no longer matches its source. A header
* whose contents would not change is left alone.
*
* Run via `npm run build:conditions` after editing either source.
*/
import path from 'node:path';
import conditionVocabulary from '../src/ts/weather/conditions.ts';
import { WIRE_CAPS } from '../src/ts/pkjs/wire.ts';
import type { ConditionVocabulary } from '../src/ts/weather/conditions.ts';
import { writeIfChanged } from '../src/tools/shared/files.ts';
import { ENGINE } from '../src/tools/shared/paths.ts';
import { isMainScript } from '../src/tools/shared/entry.ts';
/** The framework repo's root, which the written paths are logged from. */
const ROOT = path.resolve(import.meta.dirname, '..');
const WEATHER_DIR = path.join(ENGINE, 'c', 'pebble', 'ui', 'weather');
const ICONS_OUT = path.join(WEATHER_DIR, 'icons_table.g.h');
const WIRE_CAPS_OUT = path.join(ENGINE, 'c', 'core', 'wire', 'wire_caps.g.h');
/**
* The auto-generated banner, the Doxygen file block, and `#pragma once` every emitted header opens
* with.
*
* fileName goes on `@file` and has to match the real file name or Doxygen files the docs under
* nothing. brief is the one line summary for that file.
*/
function bannerLines(fileName: string, brief: string, source = 'ts/weather/conditions.ts', group = 'lib_ui') {
return [
'//------------------------------------------------------------------------------',
'// <auto-generated>',
"// This code was generated by the framework's tools/build-conditions.ts.",
`// Source: the framework's ${source}`,
'//',
'// Changes to this file may be lost if the code is regenerated.',
'// Edit the source and run `npm run build:conditions` in the framework repo instead.',
'// </auto-generated>',
'//------------------------------------------------------------------------------',
'/**',
` * @file ${fileName}`,
` * @brief ${brief}`,
' *',
` * Generated from the framework's \`${source}\`, so the phone and the watch read the same numbers.`,
' *',
` * @ingroup ${group}`,
' */',
'#pragma once',
'',
];
}
/**
* Builds the C icon-lookup source from a condition vocabulary.
*
* Pure string transform so it can be tested without touching the filesystem.
* For each condition emits a token -> resource branch, and when the row has a
* nightResource also emits a "<token>_NIGHT" -> nightResource branch (the wire
* carries the _NIGHT token after dark).
*
* @param vocabulary The fallback icon plus the token/resource rows to build the lookup from.
* @return The generated C source for icons_table.g.h.
*/
export function buildIconsTable({ fallback, conditions }: ConditionVocabulary) {
const fallbackResource = `RESOURCE_ID_ICON_${fallback.resource}`;
const lines = bannerLines('icons_table.g.h', 'Condition token to weather icon resource lookup.');
lines.push(
'/**',
' * @brief Finds the icon resource for a condition token.',
' *',
' * A token ending in `_NIGHT` gets its night glyph where the condition has one.',
` * An unknown token, or a NULL one, gets the \`${fallback.resource}\` icon.`,
' *',
' * @param condition The condition token from the phone, such as `RAIN` or `RAIN_NIGHT`. May be NULL.',
' * @return The `RESOURCE_ID_ICON_*` resource to load.',
' *',
' * @ingroup lib_ui',
' */',
'static uint32_t wx_resource_for_table(const char *condition)',
'{',
' if (!condition)',
' {',
` return ${fallbackResource};`,
' }',
''
);
for (const { token, resource, nightResource } of conditions) {
lines.push(` if (!strcmp(condition, "${token}"))`);
lines.push(' {');
lines.push(` return RESOURCE_ID_ICON_${resource};`);
lines.push(' }');
lines.push('');
if (nightResource) {
lines.push(` if (!strcmp(condition, "${token}_NIGHT"))`);
lines.push(' {');
lines.push(` return RESOURCE_ID_ICON_${nightResource};`);
lines.push(' }');
lines.push('');
}
}
lines.push(` return ${fallbackResource};`);
lines.push('}');
lines.push('');
return lines.join('\n');
}
/**
* Builds the shared wire caps header.
*
* Each text cap gains one for the terminator the C buffer has to hold, which is the one place that
* conversion is written down. The night bit comes from the vocabulary rather than the cap table,
* because it is a property of the condition code the forecast strip carries.
*
* @param caps The cap table from the framework's wire module.
* @param nightBit The bit the phone sets on an hourly forecast code after dark.
* @return The full contents of wire_caps.g.h.
*/
export function buildWireCaps(caps: typeof WIRE_CAPS, nightBit: number): string {
const lines = bannerLines(
'wire_caps.g.h',
'Strip caps and marker values shared by the phone and the watch.',
'ts/pkjs/wire.ts',
'lib_core'
);
// a negative value is wrapped, so a macro written as -WEATHER_NO_TEMP reads as a minus and not --
const entry = (name: string, value: number | string, brief: string) => {
const text = typeof value === 'number' && value < 0 ? `(${value})` : value;
lines.push(`#define ${name} ${text} ///< ${brief}`);
};
entry('STOCK_SLOTS_MAX', caps.STOCK_SLOTS_MAX, 'How many tickers the watchlist strip carries');
entry('STOCK_MAX_SLOTS', 'STOCK_SLOTS_MAX', 'The old name for STOCK_SLOTS_MAX, deprecated');
entry('STOCK_SYMBOL_LEN', caps.STOCK_LABEL_MAX + 1, `A watchlist label, ${caps.STOCK_LABEL_MAX} characters plus its terminator`);
lines.push('');
entry('CALENDAR_SLOTS_MAX', caps.CALENDAR_SLOTS_MAX, 'How many events the agenda strip carries');
entry('CALENDAR_MAX_SLOTS', 'CALENDAR_SLOTS_MAX', 'The old name for CALENDAR_SLOTS_MAX, deprecated');
entry('CAL_TITLE_LEN', caps.CALENDAR_TITLE_MAX + 1, `An event title, ${caps.CALENDAR_TITLE_MAX} characters plus its terminator`);
entry('CAL_LOC_LEN', caps.CALENDAR_LOC_MAX + 1, `An event location, ${caps.CALENDAR_LOC_MAX} characters plus its terminator`);
lines.push('');
entry('WEATHER_FORECAST_COLS', caps.FORECAST_COLS_MAX, 'How many columns a forecast strip carries');
entry('WEATHER_NO_TEMP', caps.FORECAST_NO_TEMP, 'A column with no reading, so the watch draws a placeholder');
lines.push('');
entry('WX_FORECAST_NIGHT_BIT', `0x${nightBit.toString(16).toUpperCase()}`, 'Set on an hourly forecast code for a column after dark');
lines.push('');
entry('SETTINGS_REQUEST_FULL', caps.SETTINGS_REQUEST_FULL, 'A settings request for the watch\'s whole snapshot');
entry('SETTINGS_REQUEST_FRESH', caps.SETTINGS_REQUEST_FRESH, 'A settings request for only whether the watch booted empty');
lines.push('');
return lines.join('\n');
}
/** One header this tool owns: where it lands, and what it should contain right now. */
export interface GeneratedHeader {
out: string;
build: () => string;
}
/**
* Every header this tool writes.
*
* `main` writes each one and the spec checks each committed file still matches, so a header added
* here is covered by both without either being edited. The committed copies are what the host C
* suite compiles and what Doxygen documents, neither of which runs this generator.
*/
export const GENERATED: GeneratedHeader[] = [
{ out: ICONS_OUT, build: () => buildIconsTable(conditionVocabulary) },
{ out: WIRE_CAPS_OUT, build: () => buildWireCaps(WIRE_CAPS, conditionVocabulary.FORECAST_NIGHT_BIT) },
];
/** Writes every generated header that changed from its source and logs which ones it wrote. */
function main() {
const written = GENERATED.filter((header) => writeIfChanged(header.out, header.build()))
.map((header) => path.relative(ROOT, header.out).split(path.sep).join('/'));
const count = conditionVocabulary.conditions.length;
console.log(written.length ? `generated ${written.join(', ')} (${count} conditions)` : `every header already matches (${count} conditions)`);
}
Iif (isMainScript(import.meta)) {
main();
}
|