在现在是几奌?我开始描述一个钟表制造项目。 我在项目的中途(我希望!),它与它开始的地方有点不同。 以前的重点是从互联网上获取时间,如果 GPS 没有收到信号,它仍然可以作为备用,但主要时间源可能是 GPS。
但是,无论哪种情况,时间都以 UTC 时间到达,而不是本地时间。 在运行真实操作系统的计算机上,如何将 UTC 转换为本地不是问题。 安装真正的操作系统时,会提供一个时区,例如 America/New_York 等。这就是操作系统所需要的(以及有关名称、与 UTC 的偏移量以及夏令时开始和结束时间的数据表,这是内置的Windows、Mac 和 Linux 操作系统。)
这ESP32基于时钟的总内存只有大约 4MB,并且运行一个版本自由实时操作系统,用于微型计算机的实时操作系统。 它不包括将时区名称转换为时区规则的表格,但它确实理解规则。 规则是一个短字符串,指示与 UTC 的偏移量、是否遵守夏令时以及夏令时的开始和结束时间。
如果带有 GPS 的时钟具有完整的纬度/经度数据到时区规则的地图,它应该能够将自己设置为本地时间。 制作该地图并将其打包成 <2MB 就是此 JSL 所做的事情。 评论讲述了使用 JSL 构建运行长度编码数据的二进制 blob 以闪存到 ESP32 SoC 上的故事(片上系统.) 代码第一次运行时需要 if(0) 部分;其中一些需要很长时间,并且在开发代码时不需要重新运行。 JSL 的各个部分需要运行大约 3 次才能达到最佳压缩状态,因为区域绘制的顺序发生了变化,这会改变运行代码的使用次数,这会影响像素在图中的隐藏方式,因为使用最少最后绘制以确保它们出现。
另外:如果你使用它,分辨率会比 1 英里更差。 做你自己的测试!
/*
Problem one: A GPS device returns both the lat,lon location and the
UTC time. How can you get the local time from that information?
You can't. You also need to know what timezone boundary the lat,lon
falls in. There's a nice github project, updated frequently, with
changes.
https://github.com/evansiroky/timezone-boundary-builder
There's also one or more web sites like
https://timezonedb.com that provide an API to some database similar
to the boundaries.
Problem two: Build a clock using an ESP32 and a GPS. How can it get the
timezone? It only has about 2MB of memory and it can only access the
internet if it has wifi access and a password.
Since a clock should "just work", the internet can be the primary
answer, used if available. A fallback answer is needed too; the data
for the timezones needs to be compressed into a structure that is
small, sufficiently detailed, and reasonably fast. (I probably will
not use the website because I'd have to bake in my API-key...)
Timezones are identified by IANA with names like America/New_York
(no longer Eastern Standard Time, EDT, DST for daylight saving time, etc
because "lots of places have an east.") America/New_York implies
when the time changes happen, but it requires another table to
get those rules. The names change, sometimes, as do the rules.
America/Godthab changed its name to America/Nuuk in 2020, and
everyone has experienced their local leaders playing with "summer time".
https://data.iana.org/time-zones/tzdb/NEWS
Linux systems have another way to represent timezones, using a rule
rather than a name. (Going out on a limb here...) a Linux system has
tables for converting names to rules. A tiny ESP32 system only
understands the rules. A rule looks something like
<-03>3<-02>,M3.5.0/-2,M10.5.0/-1 (for America/Nuuk)
or
EST5EDT,M3.2.0,M11.1.0 (for America/New_York)
I think those are called "posix" rules. EST5 means 5 hours behind
UTC, the EDT means daylight time is observed in the summer. The two
time-like values says when to start and end daylight time using, for
the New York example, third month, second week, sunday with a 2AM default.
Nuuk is 3 or 2 hours behind and uses the last(5) week and 1AM (or
is that 11PM?) Fortunately, we don't need to read or write the rules, the OS does.
The name-based America/New_York and the rule-based EST5EDT,M3.2.0,M11.1.0
provide different benefits for different use cases. The name-based system
uses a database to look up the name and can provide historical information
about whether daylight time was in effect in May of 1970, for example.
The rule-based system needs no database and encodes everything needed
to convert any time the rule covers (maybe not 1970!) between UTC and local.
In this project, I realized a bit late that I needed the rule-based answer;
had the name-based answers been converted to rule-based earlier, collapsing
adjacent names with the same rule into one area instead of several, the
file would be a bit smaller. But, it is small enough.
This is the JSL to build the timezone data structure that is stored
into the ESP32 flash memory (4MB which also holds the C code.) The
C language code on the ESP32 will use the resulting binary data.
*/
// data from https://github.com/evansiroky/timezone-boundary-builder
// https://github.com/evansiroky/timezone-boundary-builder/releases/download/2020d/timezones.geojson.zip
// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv to convert America/New_York to EST5EDT,M3.2.0,M11.1.0
// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
// made my own using the gen-tz.py script, modified with freqCodeToName names added, America/Nuuk was missing
/* here's the import of the file that converts from America/New_York to EST5EDT,M3.2.0,M11.1.0
I open the JMP table with the from and to column names below...
dtTrans = open("f:/ClockScripts/gen-tz.csv",Import Settings(
End Of Line( CRLF, CR, LF ),
End Of Field( Comma, CSV( 1 ) ),
Strip Quotes( 1 ),
Use Apostrophe as Quotation Mark( 0 ),
Use Regional Settings( 0 ),
Scan Whole File( 1 ),
Treat empty columns as numeric( 0 ),
CompressNumericColumns( 0 ),
CompressCharacterColumns( 0 ),
CompressAllowListCheck( 0 ),
Labels( 0 ),
Column Names Start( 1 ),
Data Starts( 1 ),
Lines To Read( "All" ),
Year Rule( "20xx" )
));*/
dtTrans = open("$desktop/timezoneNameConversion.jmp"); // cols from and to, used way below to make an associative array
/* this file was missing Nuuk, above was the regenerated file including Nuuk (run Python on Linux system on the gen-tz)
open("https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv",Import Settings(
End Of Line( CRLF, CR, LF ),
End Of Field( Comma, CSV( 1 ) ),
Strip Quotes( 1 ),
Use Apostrophe as Quotation Mark( 0 ),
Use Regional Settings( 0 ),
Scan Whole File( 1 ),
Treat empty columns as numeric( 0 ),
CompressNumericColumns( 0 ),
CompressCharacterColumns( 0 ),
CompressAllowListCheck( 0 ),
Labels( 0 ),
Column Names Start( 1 ),
Data Starts( 1 ),
Lines To Read( "All" ),
Year Rule( "20xx" )
));
*/
if( 0, // about 2 minutes...run once... download from https://github.com/evansiroky/timezone-boundary-builder/releases/download/2020d/timezones.geojson.zip
za = Open( "z:/timezones.geojson.zip", "zip" );
blob = za << read( "combined.json", Format( blob ) );
p = Parse JSON( Blob To Char( blob ) ); // this grinds for a bit, it is 135MB
Save Text File( "$desktop/rlejson.jsl", char(p) ); // this is what is needed next time
);
/* run the above to generate this file the first time */
P = Eval( Parse( Load Text File( "$desktop/rlejson.jsl" ) ) ); // loads faster this way
/*
this is the only interesting key, grab it once. probably this is what should have been saved/reloaded.
WATER: there is another version of the geojson that includes oceans. I did not look at it. The
JSL presets the bitmap to 0, then fills in polygons with values > 0 for timezones. This works out
well; the C array is indexed from 0 and has a entry for water there. The JSL test code detects
the 0 to avoid an index error and reports "water";
*/
featureList = P["features"]; // list of 426 AAs (no water)
/*
the "P" is for Parsed json. This is the best way to load this .json
data; using the json wizard almost works but does not handle the nested
arrays used to represent disjoint polygons.
The next two files are saved from a prev run. find "if(0, // regenerate" below
to generate them; takes a while and this saves time if nothing else changed.
If you are lucky enough to only run this code once, you won't care. Saved a lot
of development time further down in the file.
*/
rle = Eval( Parse( Load Text File( "$desktop/rletext.jsl" ) ) );
/*
you won't have this file the first time; I think freqCodeToName={"water"}; is the correct choice,
BUT you'll need to do a second run to get good compression! This list *should* have the
"America/New_York" names listed in order of how many times they create a run in the
run length encoding. The 128 most frequent ones get represented as 1-byte codes, the
remaining 300 or so get a 2-byte code. They get counted and saved in this file later.
*/
freqCodeToName = Eval( Parse( Load Text File( "$desktop/rlename.jsl" ) ) );
/*
Mostly this JSL uses the "America/New_York" names, in the order of the feature list.
This simple-minded list of those names can be indexed by the feature index (usually) or
using loc() (rarely) to get the index from the name.
The for loop ignores the geometry for now and make the iFeatureToName list.
*/
iFeatureToName = {};
for( global:iFeature = 1, global:iFeature <= N Items( featureList ), global:iFeature += 1,
tzdata = featureList[global:iFeature]; // aa {"geometry", "properties", "type"}
tzproperties = tzdata["properties"]; // aa ["tzid" => "Africa/Abidjan"]
tzname=tzproperties["tzid"];
insertinto(iFeatureToName,tzname);
if(!contains(freqCodeToName,tzname),
write("\!nadd ",tzname);
insertinto(freqCodeToName, tzname); // see comment above about "freqCodeToName={"water"};" -- this makes the uncompressed version
);
);
// sanity check, will need work or removal in the future.
if( freqCodeToName[1] != "water" | freqCodeToName[2] != "Asia/Shanghai" | freqCodeToName[426] != "Europe/Busingen" | freqCodeToName[427] != "Europe/Vatican",
Throw( "freqCodeToName" ) // probably comment this out on the first run which won't be sorted, and maybe later if the world changes.
);
/*
two helper functions for the graphics script to call
dopoly does one disjoint part of a timezone.
Further down, the graphbox will be recreated over and over for each global:iFeature because
there is an anti-aliased edge that has pixels of a different color than the center pixels
but they are all the same timezone...so doing the timezones one-at-a-time is the easiest way,
though it will take a long time because the bitmap is 16K x 30K huge.
*/
dopoly = function( {tzName, tzcoords},
{i, m, xs,ys},
for( i = 1, i <= N Items( tzcoords ), i += 1,
/*
I'm still not sure what the 2,3,... polys represent. Holes maybe,
in a single disjoint poly. Render same color, smaller polys render later (2nd run).
this is part of the data that is not right with the json wizard (comments at top)
*/
//if(i>1,fillcolor("magenta"));
m = Matrix( tzcoords[i] );
xs = m[0, 1];
ys = m[0, 2];
//pencolor("white");pensize(1);
polygon/*line*/(xs, ys);// xmatrix,ymatrix
)
);
/*
drawpolys does all the disjoint parts of one timezone; a timezone with only 1 part is not nested
quite as deep as a timezone with multiple disjoint parts.
*/
drawpolys = function( {}, //
{tzdata, tzgeometry, tzproperties, tztype, tzGeometryType, tzcoordinates, code, greenbits, bluebits, redbits, ipoly},
tzdata = featureList[global:iFeature]; // aa {"geometry", "properties", "type"}
tzgeometry = tzdata["geometry"]; // aa {"coordinates", "type"}
tzproperties = tzdata["properties"]; // aa ["tzid" => "Africa/Abidjan"]
/* sanity checking, these should match the log line that follows from the driver loop below */
write("\!n drawing ",global:iFeature,": ", tzproperties["tzid"] );
tztype = tzdata["type"]; // string "Feature"
tzGeometryType = tzgeometry["type"];//"Polygon" or "MultiPolygon"
tzcoordinates = tzgeometry["coordinates"]; // list
Fill Color( "white" );
if(
tzGeometryType == "MultiPolygon", /* a disjoint time zone */
for( ipoly = 1, ipoly <= N Items( tzcoordinates ), ipoly += 1,
dopoly( tzproperties["tzid"], tzcoordinates[ipoly] ) /* deeper nest */
);//
, /*else if*/tzGeometryType == "Polygon", /* single poly timezone */
dopoly( tzproperties["tzid"], tzcoordinates );// no extra nest
, /*else*/
Throw( "tzGeometryType" || Char( tzGeometryType ) )
);
);
/*
this needs to run the first time to generate the bitmap to get the initial frequencies of the
run length codes. It needs to run the 2nd time too, using the frequency ordering. Maybe.
Originally 1-pixel zones like Europe/Vatican reported their name. Now they report thier
description, and in this case, the surrounding Europe/Rome has the same description. There
could be some other edge cases that still matter.
"composition" is a large matrix (3.5GB), if rerunning this code, set it to 0 to release the
old one before making a new one. otherwise 2*3.5GB must be in memory at the same time.
*/
composition = 0;
composition = J( 16000, 30000, 0 );42;
if(0, // let this code run to rebuild the composition
for( ifreq = 2/* 1 is water */, ifreq <= N Items( freqCodeToName ), ifreq += 1,
/*
this is the big-to-small list of timezones; get the name of the zone to do next...
*/
thisname=freqCodeToName[ifreq];
/*
this global variable tells the poly routine what to work on; locate the name and use the index from loc
*/
global:iFeature = loc(iFeatureToName,thisname)[1];// draw smallest last
/*
Surprisingly nothing happens when the graphbox is made without a window, until...
*/
gb = Graph Box(
X Scale( -180, 180 ),
Y Scale( -90, 90 ),
framesize( 30000 + 1, 16000 + 1 ), // drawing beyond 30,000 may not always work, 16x30 is about as big as it can be.
<<BACKGROUNDCOLOR>0];
rle = {};
for( irow = 1, irow <= N Rows( composition ), irow += 1,
if(mod(irow,100)==0,write(irow," ");wait(.01));
row = {}; // a list for one row
code = -1; // initial no match
length = 0; // initial no length
for( icol = 1, icol <= N Cols( composition ), icol += 1,
if( composition[irow, icol] != code, // new run begins
if( length > 0, // initial no match has length 0
run = {}; // output code&length of prev run
codecounts[code]+=1;
run[1] = code;
run[2] = length;
row[N Items( row ) + 1] = run;
);
length = 1;
code = composition[irow, icol];//
, // else
length += 1
)
);
run = {};// handle the last one on the row
codecounts[code]+=1;
run[1] = code;
run[2] = length;
row[N Items( row ) + 1] = run;
rle[N Items( rle ) + 1] = Matrix( row );// convert to Nx2 matrix and store at end of rle list
);
countkeys = codecounts<<GETKEYS>0,
iFeatureToName[code];
,//
"water"
)
);
/*
draw a bunch of markers, color coded for timezone, over a map.
This give a view of the accuracy near rivers and obvious political boundaries.
*/
//////////////////////////////////////////////////////////////////
markersOver = function({clat,clon,name,grid=1},
dtVat=New Table( name,
New Column( "tz", Character, "Nominal" ),
New Column( "lat", Numeric, "Continuous", Format( "Best", 12 ) ),
New Column( "lon", Numeric, "Continuous", Format( "Best", 12 ) )
);
for(ylat=clat-.5*grid, ylat<=clat+.5*grid,ylat+=.004*grid,
for(xlon=clon-.5*grid,xlon<=clon+.5*grid,xlon+=.006*grid,
dtVat<<ADDROWS></ADDROWS></GETKEYS></BACKGROUNDCOLOR>印第安纳州真的有十一个时区吗?
测试 blob 是否可以将纬度/经度转换为时区的部分测试涉及制作一些地图。 此地图的 JSL 在一个区域上创建了一个数据点网格。 印第安纳州是美国中部的一个州;它正处于从东部时间到中部时间的变化边缘。 这维基百科文章(有趣的阅读)表明夏令时是多么痛苦,远离时区的中心,导致州内的县多年来做出不同的选择。 过去,区域名称可能与白天时间开始和停止的时间或是否存在细微差别,以及东部时间和中部时间之间的不那么微妙的选择。 这些区域名称(从列)出现在印第安纳州:
New_York, Chicago, and Kentucky zones extend way beyond Indiana.New_York、Chicago 和 Kentucky 地区远远超出印第安纳州。
只有两个不同的规则(至列)在印第安纳州的时间。 印第安纳州使用中部时间的部分地区靠近印第安纳州以外使用中部时间的大城市。
Eleven zones in one state.一个州有 11 个区域。
堪萨斯时区
堪萨斯州位于印第安纳州以西 15 度(整个时区),在其西部边界也有类似的问题。 四个县选择使用丹佛(山区)时间而不是芝加哥(中部)时间。 这对县轮廓进行了很好的测试,不仅可以查看左右对齐,还可以查看上下对齐。 此处显示的状态大纲。 维基百科的堪萨斯故事不像印第安纳那样有趣(好!)
The blue Denver time is used by four counties on the western side of Kansas. The Red Chicago time is used by most of the state.堪萨斯州西部的四个县使用蓝色的丹佛时间。 该州大部分地区使用红色芝加哥时间。
将数据刷写到 ESP32:
Linux shell window. I should probably rename my computer. I dropped that subscription years ago after I got to the end.Linux 外壳窗口。 我可能应该重命名我的电脑。 几年前,我在结束后放弃了该订阅。
最后,顶一下IANA为他们在使互联网工作中发挥的作用。