First commit with working display

This commit is contained in:
Ricardo LAP
2026-09-13 20:50:51 +00:00
parent a83af259b3
commit c54c68c899
6690 changed files with 1093387 additions and 2 deletions

View File

@@ -0,0 +1,2 @@
CHANGELOG_LAST.md
CHANGELOG_LAST.rst

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
#!/bin/bash
set -e
# These variables allow us to specify an alternate repository URL and commit reference
# This is particularly useful when running in CI environments for pull requests
# where we need to build from the contributor's forked repository
ARG_1="${1:-}"
ARG_2="${2:-}"
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
rm -rf emscripten_builder
git clone https://github.com/lvgl/lv_sim_emscripten.git emscripten_builder
cd emscripten_builder
if [ "$ARG_1" != "--symlink" ]; then
REPO_URL="$ARG_1"
COMMIT_REF="$ARG_2"
git submodule update --init -- lvgl
if [ -n "$REPO_URL" ] && [ -n "$COMMIT_REF" ]; then
cd lvgl
echo "Using provided repo URL: $REPO_URL and commit ref: $COMMIT_REF for lvgl submodule"
git remote set-url origin "$REPO_URL"
git fetch origin
git checkout "$COMMIT_REF"
cd ..
fi
else
SYMLINK_TARGET="$ARG_2"
echo "Using provided symbolic link to LVGL directory (should be absolute): $SYMLINK_TARGET"
rmdir lvgl # remove the uninitialized submodule empty dir
ln -s -T "$SYMLINK_TARGET" lvgl
fi
# Grab the path to emscripten's port examplelist.c before changing to LVGL's directory
EXAMPLE_LIST_C=$(pwd)/examplelist.c
cd lvgl
scripts/genexamplelist.sh > $EXAMPLE_LIST_C
cd ..
# Generate lv_conf
LV_CONF_PATH=`pwd`/lvgl/configs/ci/docs/lv_conf_docs.h
python ./lvgl/scripts/generate_lv_conf.py \
--template lvgl/lv_conf_template.h \
--config $LV_CONF_PATH \
--defaults lvgl/configs/ci/docs/lv_conf_docs.defaults
mkdir cmbuild
cd cmbuild
emcmake cmake .. -DLV_BUILD_CONF_PATH=$LV_CONF_PATH -DLVGL_CHOSEN_DEMO=lv_example_noop -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
emmake make -j$(nproc)
rm -rf CMakeFiles
cd ..
if [ "$ARG_1" != "--symlink" ]; then
cp -a cmbuild ../docs/src/_static/built_lv_examples
else
cp -a cmbuild lvgl/docs/src/_static/built_lv_examples
fi
cd ..

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import argparse
from argparse import RawTextHelpFormatter
import os
import subprocess
import sys
parser = argparse.ArgumentParser(description="""Create fonts for LVGL including the built-in symbols. lv_font_conv needs to be installed. See https://github.com/lvgl/lv_font_conv
Example: python built_in_font_gen.py --size 16 -o lv_font_roboto_16.c --bpp 4 -r 0x20-0x7F""", formatter_class=RawTextHelpFormatter)
parser.add_argument('-s', '--size',
type=int,
metavar = 'px',
nargs='?',
help='Size of the font in px')
parser.add_argument('--bpp',
type=int,
metavar = '1,2,4',
nargs='?',
help='Bit per pixel')
parser.add_argument('-r', '--range',
nargs='+',
metavar = 'start-end',
default=['0x20-0x7F,0xB0,0x2022'],
help='Ranges and/or characters to include. Default is 0x20-7F (ASCII). E.g. -r 0x20-0x7F, 0x200, 324')
parser.add_argument('--symbols',
nargs='+',
metavar = 'sym',
default=[''],
help=u'Symbols to include. E.g. -s ÁÉŐ'.encode('utf-8'))
parser.add_argument('--font',
metavar = 'file',
nargs='?',
default='Montserrat-Medium.ttf',
help='A TTF or WOFF file')
parser.add_argument('-o', '--output',
nargs='?',
metavar='file',
help='Output file name. E.g. my_font_20.c')
parser.add_argument('--compressed', action='store_true',
help='Compress the bitmaps')
parser.add_argument('--subpx', action='store_true',
help='3 times wider letters for sub pixel rendering')
args = parser.parse_args()
if args.compressed == False:
compr = "--no-compress --no-prefilter"
else:
compr = ""
if len(args.symbols[0]) != 0:
args.symbols[0] = "--symbols " + args.symbols[0]
subpx = ""
if args.subpx: subpx = "--lcd"
#Built in symbols
syms = "61441,61448,61451,61452,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650"
#Run the command (Add degree and bullet symbol)
cmd = ["lv_font_conv"]
if subpx:
cmd.append(subpx)
if compr:
cmd.extend(compr.split())
cmd.extend(["--bpp", str(args.bpp), "--size", str(args.size), "--font", args.font, "-r", args.range[0]])
if args.symbols[0]:
cmd.extend(args.symbols[0].split())
cmd.extend(["--font", "FontAwesome5-Solid+Brands+Regular.woff", "-r", syms, "--format", "lvgl"])
if args.output:
cmd.extend(["-o", args.output])
cmd.append("--force-fast-kern-format")
subprocess.run(cmd, check=True)

View File

@@ -0,0 +1,99 @@
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
Bitstream Vera Fonts Copyright
------------------------------
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org.
Arev Fonts Copyright
------------------------------
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and
associated documentation files (the "Font Software"), to reproduce
and distribute the modifications to the Bitstream Vera Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to
the following conditions:
The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.
The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Tavmjong Bah" or the word "Arev".
This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the
"Tavmjong Bah Arev" names.
The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the name of Tavmjong Bah shall not
be used in advertising or otherwise to promote the sale, use or other
dealings in this Font Software without prior written authorization
from Tavmjong Bah. For further information, contact: tavmjong @ free
. fr.
$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $

View File

@@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

View File

@@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2022 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

View File

@@ -0,0 +1,94 @@
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,93 @@
Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,94 @@
Copyright 2021 Google Inc. All Rights Reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,94 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,96 @@
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font
Name 'Source'. Source is a trademark of Adobe in the United States
and/or other countries.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,97 @@
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,275 @@
<html>
<meta charset="UTF-8" />
<head>
<title>Unscii - a bitmapped Unicode font for blocky graphics</title>
<style>
<!--
@font-face{font-family:"unscii16";
src: url("unscii-16.woff") format("woff"),
url("unscii-16.ttf") format("ttf"); }
@font-face{font-family:"unscii8";
src: url("unscii-8.woff") format("woff"),
url("unscii-8.ttf") format("ttf"); }
a{color:#003;}
ul{list-style-type:square;}
h1{font-family:"unscii16",monospace;font-size:32px;text-align:center;}
body{font-family:"unscii16",monospace; background-color:#ccc;color:#000;width:640px;}
pre{font-family:"unscii16",monospace; font-size:16px;line-height:16px}
-->
</style>
<body>
<p align="center"><img src="unsciilogo.png" alt="UNSCII" /></p>
<p>Unscii is a set of bitmapped Unicode fonts based on classic system fonts.
Unscii attempts to support character cell art well while also being suitable
for terminal and programming use.</p>
<p>The two main variants are unscii-8 (8×8 pixels per glyph) and unscii-16
(8×16). There are also several alternative styles for unscii-8, as well as
an 8x16 "full" variant that incorporates missing Unicode glyphs from
Fixedsys Excelsior and GNU Unifont. "unscii-16-full" falls under GPL because
of how Unifont is licensed; the other variants are in the Public Domain.</p>
<p>Unscii was created by Viznut.</p>
<br />
<h1>UNSCII 2.0</h1>
<p>In 2020-03-10, the new <a
href="http://www.unicode.org/versions/Unicode13.0.0/">Unicode version
13.0</a> added 214 graphics characters for "legacy computing" (including,
among all, the missing PETSCII characters, and a majority of missing
Teletext/Videotex characters). Most of these were already included in Unscii
1.x, but now I have been able to give them proper Unicode mappings as well.
This is the main reason for the Unscii 2.0 release.</p>
<p>Additionally, Unscii 2.0 fixes errors in some characters, legibility in
some others and adds a bunch of new ones.</p>
<p>A test picture representing what is currently available in Unicode (feel
free to copy-paste it to your editor to see what it looks like in other
fonts):</p>
<pre>
╎┆┊ ╱🭽▔🭾╲ 🮲🮳 🮸🮀🮵🮶🮀🮁🮁🮀🮼🯁🯂🯃 ▵ ↑ ◬
╶─╴╺━╸ ═ ╎┆┊ ⎹ ⎸▣⎹ ⎸ ▝▛▀▜▘ 🯲🯷🯶 △ ▴ ╽ ◭⬘◮
╷┌┬┐┍┯┑╒╤╕╏┇┋ 🮷 🭼▁🭿 ⎸ ▚▌█▐▞ 🯹🯵🯱 🯰 ▁▂▃▄▅▆▇█ ◃◅◁╳▷▻▹ ▲ ←╼╋╾→ ◩⬒⬔
│├┼┤┝┿┥╞╪╡╏┇┋ ⎹╱ ╳ ╲⎸ ▗▙▄▟▖ 🯴🯳🯸 █🮆🮅🮄▀🮃🮂▔⎹ ▽ ◂◄◀🮽▶►▸╿ ⮝ ⬖◧◫◨⬗
╵└┴┘┕┷┙╘╧╛┞╀┦ ▔▔▔▔▔ 🬑🬜🬰🬪🬟 🮞🮟 ▕▉ ◞◡◯◡ ◎🭵 ▿ ▼ ↓ ⮜◈⮞ ⬕⬓◪
╻┎┰┒┏┳┓ ┭╆╈╅┮╍╍╌╌ 🬥🬦🬍🬲🬵🬹🬱🬷🬌🬓🬙 🮝🮜 🮇▊◝◠◯◉◯◡◟🭴 ▾ ⮟ ◕ ⬙ ◔
┃┠╂┨┣╋┫ ╺┽╊◙╉┾┅┅┄┄ 🬔🬡🬖🬻🬞🬭🬏🬺🬢🬒🬧 🮈▋◍ ◠◯◠◜ 🭳 ◿◺
╹┖┸┚┗┻┛ ━┵╄╇╃┶┉┉┈┈ 🬃🬤🬫🬴🬠🬋🬐🬸🬛🬗🬇 🭇🬼 ▐▌ ◌🮣🮢 🮦 🭲 ◹◸ 🭯 🮀⚞⚟🮀 🯊 ◙◛◶─◵
╓╥╖ ╔╦╗┢╁┪ ┟┱┲┧ 🬣🬯🬈🬬🬁🬂🬀🬝🬅🬮🬘 🭢🭗 🮉▍ 🮤🮪🮫🮥🮧 🭱 🭯 🭮◙🭬╭──╮⎫🮻⎧ ◘◙│◲┼◱╭◒╮
║╟╫╢🮐🮒🮐╠╬╣ ╹┃ ┡┹┺┩ 🬳🬉🬩🬕🬊🬎🬆🬨🬚🬄🬶 🭊🬿 🮊▎ 🮩🮬🮭🮨 🭰 ◢🭫◣ 🮚 │ ▢ ⎮🏎⎪ ◙◚◷┼◴│◑╋◐
╙╨╜🮔 🮓╚╩╝ 🯆 🯅 🯇 🮣🮢 🯉 🯈 🭥🭚 🮋▏🮮 🮡🮠 ⎸🭮🭪◆🭨🮛🮿🭬╰─🮯─╯⎬⎯⎨ ◳─◰╰◓╯
░░🮐🮑🮐▓▓██🮗🮗▤▤▥▥▦▦▩▩▧▧🮘🮘🮙🮙▨▨🮕🮕🮖🮖 🭋🭀 █▁🭻🭺🭹🭸🭷🭶▔ ◥🭩◤ 🭭 ⎮⯊⎪ ▱▰ ▭▬
░░▒🮎▒▓▓██🮗🮗▤▤▥▥▦▦▩▩▧▧🮘🮘🮙🮙▨▨🮕🮕🮖🮖 🭦🭛 🮰 🭇🬼🭭 🭊🬿 🭋🭀 ⎭⯋⎩ ▯▮ ▫◻□■◼▪⬝·
🮌█🮍 ╲╱ 🭇🬼🭈🬽🭉🬾◢◣🭇🭃🭎🬼🭈🭆🭂🭍🭑🬽🭉🭁🭌🬾🭈🭄🭏🬽🭅🭐 ◦○◯⬤◖◗ ⬫⬦⬨♢◊◇◆♦⬧⬥⬩⬪
▒🮏▒ 🭢🭗🭣🭘🭤🭙◥◤🭢🭔🭟🭗🭣🭧🭓🭞🭜🭘🭤🭒🭝🭙🭣🭕🭠🭘🭖🭡 ∘⭘●
🭢🭗 🭥🭚 🭦🭛 •
</pre>
<h1>EXAMPLES</h1>
<p>Here are some conversions of legacy character set art into Unscii.</p>
<p>Amiga ansi: Divine Stylers by Hellbeard, as rendered with unscii-16.
<a href="http://sixteencolors.net/pack/impure54/xz-dvn.ans">Source</a></p>
<img src="xz-dvn.png" />
<p>PC ansi: Ansi Love by Rad Man, as rendered with unscii-16.
<a
href="http://sixteencolors.net/pack/blocktronics_blockalypse/rad-LOVE.ANS">Source</a></p>
<img src="rad-love.png" />
<p>Commodore 64 petscii pictures as rendered with unscii-8, using the
256-color xterm palette: I Has Floppy by Redcrab; The First Ball by
Dr.TerrorZ; Gary by Mermaid.</p>
<p align="center">
<img src="ihasfloppy.png" />
<img src="thefirstball.png" />
<img src="gary.png" />
</p>
<p>The source code package includes a generic bitmap-to-unscii converter. Here's an
example of a conversion to unscii-8 using the 256-color xterm
palette, without dithering:</p>
<p align="center">
<img src="tta.png" />
</p>
<br />
<h1>DOWNLOADS</h1>
<p>HEX and PCF are the only actual bitmapped formats here. HEX is the same
simple hexdump format as used by the Unifont project. TTF, OTF and WOFF
are vectorized.</p>
<p>NOTE: Due to format limitations, the PCF versions lack all the characters
above U+FFFF! However, all the new graphics characters are provided in the
good old PUA range as well. A mapping is in the file <a
href="uns2uni.tr">uns2uni.tr</a>.</p>
<table>
<tr>
<td>
<img src="unscii16.png" alt="[Some Unscii16 glyphs]" />
</td><td>
<p>unscii-16: <a href="unscii-16.hex">hex</a> <a href="unscii-16.pcf">pcf</a> <a href="unscii-16.ttf">ttf</a> <a href="unscii-16.otf">otf</a> <a href="unscii-16.woff">woff</a><br />
unscii-16-full: <a href="unscii-16-full.hex">hex</a> <a href="unscii-16-full.pcf">pcf</a> <a href="unscii-16-full.ttf">ttf</a> <a href="unscii-16-full.otf">otf</a> <a href="unscii-16-full.woff">woff</a><br />
8x16. The latter is recommended for serious terminal use where a large
Unicode coverage is needed. (Warning: unscii16-full files range from 2
to 12 megabytes in size; the others range from 40 to 400 kilobytes.)</p>
</td></tr>
<tr bgcolor="#999">
<td>
<img src="unscii8.png" alt="[Some Unscii8 glyphs]" />
</td><td>
<p>unscii-8: <a href="unscii-8.hex">hex</a> <a href="unscii-8.pcf">pcf</a> <a href="unscii-8.ttf">ttf</a> <a href="unscii-8.otf">otf</a> <a href="unscii-8.woff">woff</a></p>
</td></tr>
<tr>
<td>
<img src="unscii8-tall.png" alt="[Ascii range in Unscii8-tall" />
</td><td>
<p>unscii-8-tall: <a href="unscii-8-tall.hex">hex</a> <a href="unscii-8-tall.pcf">pcf</a> <a href="unscii-8-tall.ttf">ttf</a> <a href="unscii-8-tall.otf">otf</a> <a href="unscii-8-tall.woff">woff</a><br />
Double-height version of unscii8.</p>
</td></tr>
<tr bgcolor="#999">
<td>
<img src="unscii8-thin.png" alt="[Ascii range in Unscii8-tall" />
</td><td>
<p>unscii-8-thin: <a href="unscii-8-thin.hex">hex</a> <a href="unscii-8-thin.pcf">pcf</a> <a href="unscii-8-thin.ttf">ttf</a> <a href="unscii-8-thin.otf">otf</a> <a href="unscii-8-thin.woff">woff</a><br />
Based on system fonts with 1-pixel-wide lines.</p>
</td></tr>
<tr>
<td>
<img src="unscii8-alt.png" alt="[Ascii range in Unscii8-alt" />
</td><td>
<p>unscii-8-alt: <a href="unscii-8-alt.hex">hex</a> <a href="unscii-8-alt.pcf">pcf</a> <a href="unscii-8-alt.ttf">ttf</a> <a href="unscii-8-alt.otf">otf</a> <a href="unscii-8-alt.woff">woff</a><br />
Based on the more peculiar glyph forms of the reference fonts.</p>
</td></tr>
<tr bgcolor="#999">
<td>
<img src="unscii8-mcr.png" alt="[Ascii range in Unscii8-alt" />
</td><td>
<p>unscii-8-mcr: <a href="unscii-8-mcr.hex">hex</a> <a href="unscii-8-mcr.pcf">pcf</a> <a href="unscii-8-mcr.ttf">ttf</a> <a href="unscii-8-mcr.otf">otf</a> <a href="unscii-8-mcr.woff">woff</a><br />
Based on retrofuturistic MCR-like 8×8 fonts used in various games, demos,
etc.</p>
</td></tr>
<tr>
<td>
<img src="unscii8-fantasy.png" alt="[Ascii range in Unscii8-alt" />
</td><td>
<p>unscii-8-fantasy: <a href="unscii-8-fantasy.hex">hex</a> <a href="unscii-8-fantasy.pcf">pcf</a> <a href="unscii-8-fantasy.ttf">ttf</a> <a href="unscii-8-fantasy.otf">otf</a> <a href="unscii-8-fantasy.woff">woff</a><br />
Based on fonts used in fantasy games.</p>
</td></tr>
<tr bgcolor="#999">
<td>
&nbsp;
</td><td>
<p><a href="unscii-2.1-src.tar.gz">Source code for current Unscii version (2.1)</a></p>
<p><a href="unscii-2.0-src.tar.gz">Source code for Unscii 2.0</a></p>
<p><a href="unscii-1.1-src.tar.gz">Source code for Unscii 1.1</a></p>
</td></tr>
</table>
<br />
<h1>BACKSTORY</h1>
<p>Years ago, I noticed that Unicode had a bunch of pseudographic characters
that could be used to enrichen Ansi art. However, no one seemed to use them.
Even MUDs that used the 256-color Xterm palette and had no issues with
Unicode still preferred to stick to the blocks available in the MS-DOS
codepage 437.</p>
<p>After looking into existing Unicode fonts, the reason became obvious: the
implementation of non-CP437 graphics characters was shaky at best. Unicode
Consortium doesn't even care how pseudographics are implemented. It was a
kind of chicken-and-egg problem: No commonly accepted Unicode graphics font,
no Unicode art scene; no art scene, no font support. The idea of an
art-compatible Unicode font was born.</p>
<p>For Unscii, I studied a bunch of classic system fonts and how their
characters had been used in Ascii and "extended-Ascii" art.</p>
<p>8×8 system fonts can be divided in two major categories according to
their line thickness: 1-pixel and 2-pixel. 2-pixel-wide lines are used in
more prominent classic systems, so I chose it. Also, 2-pixel 8×8 system
fonts are surprisingly similar to one another which made it easier to choose
neutral shapes.</p>
<p>The basic look of the 8×8 variant of Unscii is based on the following
systems:</p>
<ul>
<li>Amiga (Topaz-8)</li>
<li>Amstrad CPC</li>
<li>Atari 8-bit (as in 800, XL etc.)</li>
<li>Atari Arcade (the iconic ROM font)</li>
<li>Atari 32-bit (as in ST etc.)</li>
<li>BBC Micro (graphics mode font)</li>
<li>Commodore 64</li>
<li>IBM PC (the 8×8 ROM font as in CGA, or VGA 80×50)</li>
</ul>
<p>The 8×16 variant of Unscii has been mostly derived from the 8×8 variant
by using a set of transformation principles. When in doubt, the following
fonts have been looked at for additional reference:</p>
<ul>
<li>Windows Fixedsys 8×15 (and its modern successor Fixedsys Excelsior)</li>
<li>IBM PC VGA ROM font(s) (and their modern successor U_VGA)</li>
<li>X Window System fonts 8x13(B) and 9x15(B)</li>
<li>Classic Macintosh 12-point Monaco</li>
<li>Digital VT420 10×16 font (used in the 80×24 mode)</li>
<li>Modern monospaced vector fonts: DejaVu Sans Mono, Lucida Console,
Inconsolata</li>
</ul>
<p>In general, neutral shapes are preferred, unless art, legibility or
readability require otherwise: The characters /\XY are connective because of
their connetive use in ascii art, and the serifs in iIl are longer than in
most classic systems.</p>
<p>Whenever a 8×16 shape has not been defined, Unscii falls back to
height-doubled 8×8.</p>
<p>I also studied game fonts and thin-line system fonts. This resulted in
the variants unscii-8-thin, unscii-8-mcr and unscii-8-fantasy.</p>
<p>When studying legacy character sets, I found literally hundreds of
characters without proper Unicode codepoints. These are mapped in the PUA
range as follows:</p>
<ul>
<li>U+E080..E0FF: Teletext/Videotex block mosaics.</li>
<li>U+E100..: The most prominent and useful non-Unicode pseudographics:
everything found in PETSCII, Videotex smooth mosaics, extra shades,
round corners, X/Y doublers.</li>
<li>U+E800..: Somewhat stranger but still potentially useful: junctions with
border-aligned lines, diagonal line junctions, non-straight lines, weirder
fill patterns, etc.</li>
<li>U+EC00..: Total oddities. Mostly game-oriented bitmaps and other
depictive characters from Sharp MZ, Aquarius, etc.</li>
</ul>
<p>Since Unicode 13.0, many of these are also available in Unicode, but
the PUA mappings are retained for compatibility.</p>
<br />
</body>
</html>

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
import os
print("Generating 8 px")
os.system("./built_in_font_gen.py --size 8 -o lv_font_montserrat_8.c --bpp 4")
print("\nGenerating 10 px")
os.system("./built_in_font_gen.py --size 10 -o lv_font_montserrat_10.c --bpp 4")
print("\nGenerating 12 px")
os.system("./built_in_font_gen.py --size 12 -o lv_font_montserrat_12.c --bpp 4")
print("\nGenerating 14 px")
os.system("./built_in_font_gen.py --size 14 -o lv_font_montserrat_14.c --bpp 4")
print("\nGenerating 16 px")
os.system("./built_in_font_gen.py --size 16 -o lv_font_montserrat_16.c --bpp 4")
print("\nGenerating 18 px")
os.system("./built_in_font_gen.py --size 18 -o lv_font_montserrat_18.c --bpp 4")
print("\nGenerating 20 px")
os.system("./built_in_font_gen.py --size 20 -o lv_font_montserrat_20.c --bpp 4")
print("\nGenerating 22 px")
os.system("./built_in_font_gen.py --size 22 -o lv_font_montserrat_22.c --bpp 4")
print("\nGenerating 24 px")
os.system("./built_in_font_gen.py --size 24 -o lv_font_montserrat_24.c --bpp 4")
print("\nGenerating 26 px")
os.system("./built_in_font_gen.py --size 26 -o lv_font_montserrat_26.c --bpp 4")
print("\nGenerating 28 px")
os.system("./built_in_font_gen.py --size 28 -o lv_font_montserrat_28.c --bpp 4")
print("\nGenerating 30 px")
os.system("./built_in_font_gen.py --size 30 -o lv_font_montserrat_30.c --bpp 4")
print("\nGenerating 32 px")
os.system("./built_in_font_gen.py --size 32 -o lv_font_montserrat_32.c --bpp 4")
print("\nGenerating 34 px")
os.system("./built_in_font_gen.py --size 34 -o lv_font_montserrat_34.c --bpp 4")
print("\nGenerating 36 px")
os.system("./built_in_font_gen.py --size 36 -o lv_font_montserrat_36.c --bpp 4")
print("\nGenerating 38 px")
os.system("./built_in_font_gen.py --size 38 -o lv_font_montserrat_38.c --bpp 4")
print("\nGenerating 40 px")
os.system("./built_in_font_gen.py --size 40 -o lv_font_montserrat_40.c --bpp 4")
print("\nGenerating 42 px")
os.system("./built_in_font_gen.py --size 42 -o lv_font_montserrat_42.c --bpp 4")
print("\nGenerating 44 px")
os.system("./built_in_font_gen.py --size 44 -o lv_font_montserrat_44.c --bpp 4")
print("\nGenerating 46 px")
os.system("./built_in_font_gen.py --size 46 -o lv_font_montserrat_46.c --bpp 4")
print("\nGenerating 48 px")
os.system("./built_in_font_gen.py --size 48 -o lv_font_montserrat_48.c --bpp 4")
print("\nGenerating 12 px subpx")
os.system("./built_in_font_gen.py --size 12 -o lv_font_montserrat_12_subpx.c --bpp 4 --subpx")
print("\nGenerating 28 px compressed")
os.system("./built_in_font_gen.py --size 28 -o lv_font_montserrat_28_compressed.c --bpp 4 --compressed")
print("\nGenerating 16 px Hebrew, Persian")
os.system("./built_in_font_gen.py --size 16 -o lv_font_dejavu_16_persian_hebrew.c --bpp 4 --font DejaVuSans.ttf -r 0x20-0x7f,0x5d0-0x5ea,0x600-0x6FF,0xFB50-0xFDFF,0xFE70-0xFEFF")
print("\nGenerating 14 px CJK")
os.system(u"./built_in_font_gen.py --size 14 -o lv_font_source_han_sans_sc_14_cjk.c --bpp 4 --font SourceHanSansSC-Normal.otf -r 0x20-0x7f --symbols (),盗提陽帯鼻画輕ッ冊ェル写父ぁフ結想正四O夫源庭場天續鳥れ講猿苦階給了製守8祝己妳薄泣塩帰ぺ吃変輪那着仍嗯爭熱創味保字宿捨準查達肯ァ薬得査障該降察ね網加昼料等図邪秋コ態品屬久原殊候路願楽確針上被怕悲風份重歡っ附ぷ既4黨價娘朝凍僅際洋止右航よ专角應酸師個比則響健昇豐筆歷適修據細忙跟管長令家ザ期般花越ミ域泳通些油乏ラ。營ス返調農叫樹刊愛間包知把ヤ貧橋拡普聞前ジ建当繰ネ送習渇用補ィ覺體法遊宙ョ酔余利壊語くつ払皆時辺追奇そ們只胸械勝住全沈力光ん深溝二類北面社值試9和五勵ゃ貿幾逐打課ゲて領3鼓辦発評1渉詳暇込计駄供嘛郵頃腦反構絵お容規借身妻国慮剛急乗静必議置克土オ乎荷更肉還混古渡授合主離條値決季晴東大尚央州が嗎験流先医亦林田星晩拿60旅婦量為痛テ孫う環友況玩務其ぼち揺坐一肩腰犯タょ希即果ぶ物練待み高九找やヶ都グ去」サ、气仮雑酒許終企笑録形リ銀切ギ快問滿役単黄集森毎實研喜蘇司鉛洲川条媽ノ才兩話言雖媒出客づ卻現異故り誌逮同訊已視本題ぞを横開音第席費持眾怎選元退限ー賽処喝就残無いガ多ケ沒義遠歌隣錢某雪析嬉採自透き側員予ゼ白婚电へ顯呀始均畫似懸格車騒度わ親店週維億締慣免帳電甚來園浴ゅ愈京と杯各海怒ぜ排敗挙老買7極模実紀ヒ携隻告シ並屋這孩讓質ワブ富賃争康由辞マ火於短樣削弟材注節另室ダ招擁ぃ若套底波行勤關著泊背疲狭作念推ぐ民貸祖介說ビ代温契你我レ入描變再札ソ派頭智遅私聽舉灣山伸放直安ト誕煙付符幅ふ絡她届耳飲忘参革團仕様載ど歩獲嫌息の汚交興魚指資雙與館初学年幸史位柱族走括び考青也共腕Lで販擔理病イ今逃當寺猫邊菓係ム秘示解池影ド文例斷曾事茶寫明科桃藝売便え導禁財飛替而亡到し具空寝辛業ウ府セ國何基菜厳市努張缺雲根外だ断万砂ゴ超使台实ぽ礼最慧算軟界段律像夕丈窓助刻月夏政呼ぴざ擇趣除動従涼方勉名線対存請子氏將5少否諸論美感或西者定食御表は參歳緑命進易性錯房も捕皿判中觀戦ニ緩町ピ番ず金千ろ?不た象治関ャ每看徒卒統じ手範訪押座步号ベ旁以母すほ密減成往歲件緒読歯效院种七謂凝濃嵌震喉繼クュ拭死円2積水欲如ポにさ寒道區精啦姐ア聯能足及停思壓2春且メ裏株官答概黒過氷柿戻厚ぱ党祭織引計け委暗複誘港バ失下村較続神ぇ尤強秀膝兒来績十書済化服破新廠1紹您情半式產系好教暑早め樂地休協良な哪常要揮周かエ麗境働避護ンツ香夜太見設非改広聲他検求危清彼經未在起葉控靴所差內造寄南望尺換向展備眠點完約ぎ裡分説申童優伝島机須塊日立拉,鉄軽單気信很転識支布数紙此迎受心輸坊モ處「訳三曇兄野顔戰增ナ伊列又髪両有取左毛至困吧昔赤狀相夠整別士経頼然簡ホ会發隨営需脱ヨば接永居冬迫圍甘醫誰部充消連弱宇會咲覚姉麼的増首统帶糖朋術商担移景功育庫曲總劃牛程駅犬報ロ學責因パ嚴八世後平負公げ曜陸專午之閉ぬ談ご災昨冷職悪謝對它近射敢意運船臉局難什産頗!球真記ま但蔵究制機案湖臺ひ害券男留内木驗雨施種特復句末濟キ色訴依せ百型る石牠討呢时任執飯歐宅組傳配小活ゆべ暖ズ漸站素らボ束価チ浅回女片独妹英目從認生違策僕楚ペ米こ掛む爸六状落漢プ投カ校做啊洗声探あ割体項履触々訓技ハ低工映是標速善点人デ口次可廿节宵植树端阳旦腊妇费愚劳动儿军师庆圣诞闰".encode('utf-8'))
print("\nGenerating 16 px CJK")
os.system(u"./built_in_font_gen.py --size 16 -o lv_font_source_han_sans_sc_16_cjk.c --bpp 4 --font SourceHanSansSC-Normal.otf -r 0x20-0x7f --symbols (),盗提陽帯鼻画輕ッ冊ェル写父ぁフ結想正四O夫源庭場天續鳥れ講猿苦階給了製守8祝己妳薄泣塩帰ぺ吃変輪那着仍嗯爭熱創味保字宿捨準查達肯ァ薬得査障該降察ね網加昼料等図邪秋コ態品屬久原殊候路願楽確針上被怕悲風份重歡っ附ぷ既4黨價娘朝凍僅際洋止右航よ专角應酸師個比則響健昇豐筆歷適修據細忙跟管長令家ザ期般花越ミ域泳通些油乏ラ。營ス返調農叫樹刊愛間包知把ヤ貧橋拡普聞前ジ建当繰ネ送習渇用補ィ覺體法遊宙ョ酔余利壊語くつ払皆時辺追奇そ們只胸械勝住全沈力光ん深溝二類北面社值試9和五勵ゃ貿幾逐打課ゲて領3鼓辦発評1渉詳暇込计駄供嘛郵頃腦反構絵お容規借身妻国慮剛急乗静必議置克土オ乎荷更肉還混古渡授合主離條値決季晴東大尚央州が嗎験流先医亦林田星晩拿60旅婦量為痛テ孫う環友況玩務其ぼち揺坐一肩腰犯タょ希即果ぶ物練待み高九找やヶ都グ去」サ、气仮雑酒許終企笑録形リ銀切ギ快問滿役単黄集森毎實研喜蘇司鉛洲川条媽ノ才兩話言雖媒出客づ卻現異故り誌逮同訊已視本題ぞを横開音第席費持眾怎選元退限ー賽処喝就残無いガ多ケ沒義遠歌隣錢某雪析嬉採自透き側員予ゼ白婚电へ顯呀始均畫似懸格車騒度わ親店週維億締慣免帳電甚來園浴ゅ愈京と杯各海怒ぜ排敗挙老買7極模実紀ヒ携隻告シ並屋這孩讓質ワブ富賃争康由辞マ火於短樣削弟材注節另室ダ招擁ぃ若套底波行勤關著泊背疲狭作念推ぐ民貸祖介說ビ代温契你我レ入描變再札ソ派頭智遅私聽舉灣山伸放直安ト誕煙付符幅ふ絡她届耳飲忘参革團仕様載ど歩獲嫌息の汚交興魚指資雙與館初学年幸史位柱族走括び考青也共腕Lで販擔理病イ今逃當寺猫邊菓係ム秘示解池影ド文例斷曾事茶寫明科桃藝売便え導禁財飛替而亡到し具空寝辛業ウ府セ國何基菜厳市努張缺雲根外だ断万砂ゴ超使台实ぽ礼最慧算軟界段律像夕丈窓助刻月夏政呼ぴざ擇趣除動従涼方勉名線対存請子氏將5少否諸論美感或西者定食御表は參歳緑命進易性錯房も捕皿判中觀戦ニ緩町ピ番ず金千ろ?不た象治関ャ每看徒卒統じ手範訪押座步号ベ旁以母すほ密減成往歲件緒読歯效院种七謂凝濃嵌震喉繼クュ拭死円2積水欲如ポにさ寒道區精啦姐ア聯能足及停思壓2春且メ裏株官答概黒過氷柿戻厚ぱ党祭織引計け委暗複誘港バ失下村較続神ぇ尤強秀膝兒来績十書済化服破新廠1紹您情半式產系好教暑早め樂地休協良な哪常要揮周かエ麗境働避護ンツ香夜太見設非改広聲他検求危清彼經未在起葉控靴所差內造寄南望尺換向展備眠點完約ぎ裡分説申童優伝島机須塊日立拉,鉄軽單気信很転識支布数紙此迎受心輸坊モ處「訳三曇兄野顔戰增ナ伊列又髪両有取左毛至困吧昔赤狀相夠整別士経頼然簡ホ会發隨営需脱ヨば接永居冬迫圍甘醫誰部充消連弱宇會咲覚姉麼的増首统帶糖朋術商担移景功育庫曲總劃牛程駅犬報ロ學責因パ嚴八世後平負公げ曜陸專午之閉ぬ談ご災昨冷職悪謝對它近射敢意運船臉局難什産頗!球真記ま但蔵究制機案湖臺ひ害券男留内木驗雨施種特復句末濟キ色訴依せ百型る石牠討呢时任執飯歐宅組傳配小活ゆべ暖ズ漸站素らボ束価チ浅回女片独妹英目從認生違策僕楚ペ米こ掛む爸六状落漢プ投カ校做啊洗声探あ割体項履触々訓技ハ低工映是標速善点人デ口次可廿节宵植树端阳旦腊妇费愚劳动儿军师庆圣诞闰".encode('utf-8'))
print("\nGenerating 8 px unscii")
os.system("lv_font_conv --no-compress --no-prefilter --bpp 1 --size 8 --font unscii-8.ttf -r 0x20-0x7F --format lvgl -o lv_font_unscii_8.c --force-fast-kern-format")
print("\nGenerating 16 px unscii")
os.system("lv_font_conv --no-compress --no-prefilter --bpp 1 --size 16 --font unscii-8.ttf -r 0x20-0x7F --format lvgl -o lv_font_unscii_16.c --force-fast-kern-format")
os.system('sed -i \'s|#include "lvgl/lvgl.h"|#include "../../lvgl.h"|\' lv_font_*.c')
os.system('astyle --ignore-exclude-errors --options=../code-format.cfg "lv_font_*.c"')
os.system('mv lv_font_*.c ../../src/font/')

View File

@@ -0,0 +1,122 @@
{{#each releases}}
`{{title}} <{{href}}>`__ {{niceDate}}
---------------------------------------------------------------------------------------------------------------------------------------------------
Breaking Changes
~~~~~~~~~~~~~~~~
{{#commit-list merges heading='' message='BREAKING CHANGE'}}
- .. warning: {{message}} `{{id}} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' message='BREAKING CHANGE'}}
- .. warning: {{subject}} `{{shorthash}} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='BREAKING CHANGE'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
Architectural
~~~~~~~~~~~~~
{{#commit-list merges heading='' message='^arch' exclude='BREAKING CHANGE'}}
- **{{message}}** `{{id}} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' message='^arch' exclude='BREAKING CHANGE'}}
- **{{subject}}** `{{shorthash}} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='^arch' exclude='BREAKING CHANGE'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
New Features
~~~~~~~~~~~~
{{#commit-list merges heading='' message='^feat' exclude='BREAKING CHANGE'}}
- **{{message}}** `{{id}} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' message='^feat' exclude='BREAKING CHANGE'}}
- **{{subject}}** `{{shorthash}} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='^feat' exclude='BREAKING CHANGE'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
Performance
~~~~~~~~~~~
{{#commit-list merges heading='' message='^perf' exclude='BREAKING CHANGE'}}
- **{{message}}** `{{id}} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' message='^perf' exclude='BREAKING CHANGE'}}
- **{{subject}}** `{{shorthash}} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='^perf' exclude='BREAKING CHANGE'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
Fixes
~~~~~
{{#commit-list merges heading='' message='^fix' exclude='(^fix conflict|^fix warning|BREAKING CHANGE)'}}
- **{{message}}** `{{id}} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' message='^fix' exclude='(^fix conflict|^fix warning|BREAKING CHANGE)'}}
- **{{subject}}** `{{shorthash}} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='^fix' exclude='(^fix conflict|^fix warning|BREAKING CHANGE)'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
Examples
~~~~~~~~
{{#commit-list merges heading='' message='^example'}}
- **{{message}}** `{{id}} <({{href}})>`__
{{/commit-list}}
{{#commit-list commits heading='' message='^example'}}
- **{{subject}}** `{{shorthash}} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='^example'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
Docs
~~~~
{{#commit-list merges heading='' message='^docs'}}
- **{{message}}** `{{id}} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' message='^docs'}}
- **{{subject}}** `{{shorthash}} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='^docs'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
CI and tests
~~~~~~~~~~~~
{{#commit-list merges heading='' message='(^ci|^test)'}}
- **{{message}}** `{{id}} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' message='(^ci|^test)'}}
- **{{subject}}** `{{shorthash }} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' message='(^ci|^test)'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
Others
~~~~~~
{{#commit-list merges heading='' exclude='(^fix|^feat|^perf|^docs|^example|^ci|^test)'}}
- **{{message}}** `{{id }} <{{href}}>`__
{{/commit-list}}
{{#commit-list commits heading='' exclude='(^fix|^feat|^perf|^docs|^example|^ci|^test)'}}
- **{{subject}}** `{{shorthash }} <{{href}}>`__
{{/commit-list}}
{{#commit-list fixes heading='' exclude='(^fix|^feat|^perf|^docs|^example|^ci|^test)'}}
- **{{commit.subject}}** `{{commit.shorthash}} <{{commit.href}}>`__
{{/commit-list}}
{{/each}}

View File

@@ -0,0 +1,25 @@
#!/bin/sh
# Generate CHANGELOG_LAST.md from changes since the last version tag. (vx.y.z-dev tags are ignored)
# CHANGELOG_LAST.md can be edited manually if required and manually added to docs/CHANGELOG.md
#
# Requirements:
# npm install -g auto-changelog
#
# Usage:
# changelog-gen <next-version>
#
# Example: if the latest version is v5.6.7 the following can be used for bugfix, minor or major versions:
# changelog-gen v5.6.8
# changelog-gen v5.7.0
# changelog-gen v6.0.0
OUTPUT_NAME=CHANGELOG_LAST.rst
auto-changelog -t changelog-template.hbs -l false --latest-version $1 --unreleased-only --tag-pattern ^v[0-9]+.[0-9]+.[0-9]+$ --merge-url "https://github.com/lvgl/lvgl/pull/{id}" -o $OUTPUT_NAME
# Remove leading space from PR commit name
# Eg "- ** PR TITLE" becomes "- **PR TITLE"
sed -i 's/- \*\* */- \*\*/g' $OUTPUT_NAME
# Remove trailing space from PR commit name
# Eg "- **PR TITLE **" becomes "- **PR TITLE**"
sed -i 's/ \*\* /\*\* /g' $OUTPUT_NAME

View File

@@ -0,0 +1,396 @@
#!/usr/bin/env python3
"""
Check code coverage for new lines in a git commit using gcovr
"""
import argparse
import subprocess
import json
import re
import sys
import os
from typing import Dict, Set, Tuple, List, Optional
def create_argument_parser() -> argparse.ArgumentParser:
"""Create argument parser"""
parser = argparse.ArgumentParser(
description="Check gcov coverage for new code in a git commit",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--commit",
help="Git commit hash, reference (e.g. HEAD, HEAD~1) or range (base...head)",
default="HEAD",
)
parser.add_argument(
"--path",
metavar="PATH",
help=(
"Analyze coverage for a single file or a directory (relative or absolute path). "
"When specified, --commit is ignored."
),
)
parser.add_argument(
"--fail-under",
type=float,
metavar="PERCENT",
default=0,
help="Fail if coverage is below this percentage (0-100), default: 0 (no failure)",
)
return parser
def run_git_command(args: List[str], cwd: str = ".") -> str:
"""Run git command and return output"""
result = subprocess.run(
["git"] + args, capture_output=True, text=True, cwd=cwd, check=True
)
return result.stdout
def get_changed_lines(commit: str, root: str) -> Dict[str, Set[int]]:
"""
Get changed lines in specified commit
Returns: {file_path: {set of changed line numbers}}
"""
diff_output = run_git_command(
["diff", f"{commit}^", commit, "--unified=0"], cwd=root
)
changed_lines: Dict[str, Set[int]] = {}
current_file = None
for line in diff_output.split("\n"):
if line.startswith("+++ b/"):
current_file = line[6:]
if current_file not in changed_lines:
changed_lines[current_file] = set()
elif line.startswith("@@"):
match = re.search(r"\+(\d+)(?:,(\d+))?", line)
if match and current_file:
start_line = int(match.group(1))
count = int(match.group(2)) if match.group(2) else 1
for lineno in range(start_line, start_line + count):
changed_lines[current_file].add(lineno)
return changed_lines
def get_coverage_data(root: str) -> Dict[str, Dict[int, int]]:
"""
Get coverage data using gcovr
Returns: {rel_file_path: {line_number: execution_count}}
Notes:
- Only lines explicitly present in gcovr JSON are considered "coverable".
Lines that are missing from the JSON (e.g. preprocessor directives like
#include, comments, whitespace, or excluded lines) are treated as
non-coverable and will be ignored by the uncovered check.
Raises: subprocess.CalledProcessError if gcovr fails
"""
filter_pattern = os.path.join(root, r"src/(?:.*/)?lv_.*\.c")
cmd = [
"gcovr",
"--gcov-ignore-parse-errors",
"--json",
"-",
"--root",
root,
"--filter",
filter_pattern,
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=root,
check=False, # Don't raise exception automatically
)
if result.returncode != 0:
error_msg = f"gcovr command failed (exit code {result.returncode}):\n"
if result.stderr:
error_msg += f"Error output:\n{result.stderr}\n"
if result.stdout:
error_msg += f"Standard output:\n{result.stdout}\n"
raise subprocess.CalledProcessError(
result.returncode, cmd, result.stdout, result.stderr
)
coverage_json = json.loads(result.stdout)
coverage_data: Dict[str, Dict[int, int]] = {}
for file_info in coverage_json.get("files", []):
# Normalize path to be relative to repo root with POSIX separators.
filename = file_info["file"]
rel = os.path.relpath(filename, root)
rel = rel.replace(os.path.sep, "/")
coverage_data[rel] = {}
for line_info in file_info.get("lines", []):
line_number = line_info.get("line_number")
# Only consider lines explicitly listed by gcovr as coverable.
# Some gcovr versions may include additional flags like
# "gcovr/noncode" or "excluded"; we simply ignore such lines
# by relying on their absence from the JSON or by requiring
# a numeric execution count.
if line_number is None:
continue
count = line_info.get("count")
if isinstance(count, int):
coverage_data[rel][line_number] = count
return coverage_data
def check_commit_coverage(
commit: str, root: str
) -> Tuple[int, int, List[Tuple[str, int]], int]:
"""
Check coverage for a commit or range
Returns: (covered_lines, total_new_lines, uncovered_lines, skipped_noncoverable)
"""
if "..." in commit:
# Handle range format (base...head)
base, head = commit.split("...")
# Get changes for each commit in the range
commits = run_git_command(["rev-list", f"{base}...{head}"], cwd=root).split()
else:
# Handle single commit
commits = [commit]
print(f"Found {len(commits)} commits in range:")
changed_lines = {}
for c in commits:
print(f" {c}")
cl = get_changed_lines(c, root)
for f, lines in cl.items():
if f not in changed_lines:
changed_lines[f] = set()
changed_lines[f].update(lines)
print(f"Found changes in {len(changed_lines)} files (before filtering):")
for filename, lines in sorted(changed_lines.items()):
print(f" {filename}: {len(lines)} lines changed")
print("Getting coverage data...")
coverage_data = get_coverage_data(root)
# Normalize changed file paths to POSIX separators for matching.
normalized_changed: Dict[str, Set[int]] = {
f.replace(os.path.sep, "/"): lines for f, lines in changed_lines.items()
}
# If desired, we could additionally filter by the gcovr filter pattern.
# However, by intersecting with coverage_data keys, we inherently ignore
# files that are not part of coverage anyway.
total_new_lines = 0 # total coverable new lines (per gcovr)
covered_lines = 0
uncovered_lines: List[Tuple[str, int]] = []
skipped_noncoverable = 0 # changed lines that gcovr doesn't consider coverable
# Build quick lookup for filenames present in coverage.
coverage_files: Set[str] = set(coverage_data.keys())
# Iterate changed files and intersect with gcovr-provided coverable lines.
for filename, line_numbers in normalized_changed.items():
if filename not in coverage_files:
# Entire file has no coverable lines in gcovr output; skip all.
skipped_noncoverable += len(line_numbers)
continue
file_coverage = coverage_data[filename]
for lineno in line_numbers:
if lineno not in file_coverage:
skipped_noncoverable += 1
continue
total_new_lines += 1
if file_coverage[lineno] > 0:
covered_lines += 1
else:
uncovered_lines.append((filename, lineno))
return covered_lines, total_new_lines, uncovered_lines, skipped_noncoverable
def check_path_coverage(path: str, root: str) -> Tuple[int, int, List[Tuple[str, int]]]:
"""
Compute coverage for a specific file or directory.
Returns: (covered_lines, total_coverable_lines, uncovered_lines)
Notes:
- Only lines reported by gcovr as coverable are counted toward totals.
- If PATH is a directory, coverage is aggregated over all coverable files within it.
- If PATH is a file, coverage is computed only for that file.
"""
# Normalize input path
abs_path = os.path.abspath(path)
if not os.path.exists(abs_path):
print(f"Error: The specified path does not exist: {abs_path}", file=sys.stderr)
sys.exit(1)
# Ensure we operate from repo root to construct relative POSIX paths
root = os.path.abspath(root)
# Build relative POSIX target(s)
rel = os.path.relpath(abs_path, root)
rel_posix = rel.replace(os.path.sep, "/")
print("Getting coverage data...")
coverage_data = get_coverage_data(root)
covered = 0
total = 0
uncovered: List[Tuple[str, int]] = []
if os.path.isdir(abs_path):
# Directory scope: include all files under this directory
scoped_files = {}
for f, lines in coverage_data.items():
# Convert both to absolute paths for comparison
f_abs = os.path.abspath(os.path.join(root, f.replace("/", os.path.sep)))
# If the common path of abs_path and f_abs is abs_path, f is under abs_path
if os.path.commonpath([abs_path, f_abs]) == abs_path:
scoped_files[f] = lines
print(f"Found {len(scoped_files)} coverable file(s) under: {rel_posix}")
for filename, line_map in sorted(scoped_files.items()):
for lineno, count in line_map.items():
total += 1
if count > 0:
covered += 1
else:
uncovered.append((filename, lineno))
else:
# File scope
if rel_posix in coverage_data:
line_map = coverage_data[rel_posix]
print(f"Found coverable file: {rel_posix}")
for lineno, count in line_map.items():
total += 1
if count > 0:
covered += 1
else:
uncovered.append((rel_posix, lineno))
else:
print(
f"Warning: No coverable lines found for '{rel_posix}' in gcovr output."
)
return covered, total, uncovered
def report_coverage(
header: str,
total: int,
covered: int,
uncovered: List[Tuple[str, int]],
fail_under: float,
*,
total_label: str,
skipped_noncoverable: Optional[int] = None,
) -> int:
"""
Print a standardized coverage report and return exit code (0/1).
- header: text to show in the report title (e.g., "commit <hash>", "'<path>'")
- total_label: label to use for the total line count (e.g.,
"New coverable lines (per gcovr)" for commit mode, or
"Coverable lines (per gcovr)" for path mode)
- skipped_noncoverable: when provided, prints the skipped non-coverable count
"""
title = f" Coverage analysis results for {header} "
separator = "=" * len(title)
print(f"\n{separator}\n{title}\n{separator}")
print(f"{total_label}: {total}")
print(f"Covered lines: {covered}")
print(f"Uncovered lines: {len(uncovered)}")
if skipped_noncoverable is not None:
print(f"Skipped non-coverable changed lines: {skipped_noncoverable}")
retval = 0
if total > 0:
coverage_percent = (covered / total) * 100
print(f"Coverage: {coverage_percent:.2f}%")
if coverage_percent < fail_under:
print(
f"\n✗ Coverage {coverage_percent:.2f}% is below required {fail_under}%"
)
retval = 1
if uncovered:
print(
"\nUncovered lines (explicitly reported by gcovr as coverable with 0 hits):"
)
for filename, lineno in sorted(uncovered):
print(f" {filename}:{lineno}")
else:
if total == 0:
print("\nNo coverable lines found.")
else:
print(f"\n✓ Code coverage check passed!")
return retval
def main() -> int:
"""Main entry point"""
parser = create_argument_parser()
args = parser.parse_args()
try:
script_dir = os.path.dirname(os.path.abspath(__file__))
root = os.path.dirname(script_dir)
os.chdir(root)
print(f"Current working directory: {root}")
if args.path:
# Path mode: ignore commit, compute coverage for file/dir
covered, total, uncovered = check_path_coverage(args.path, root)
return report_coverage(
header=f"'{args.path}'",
total=total,
covered=covered,
uncovered=uncovered,
fail_under=args.fail_under,
total_label="Coverable lines (per gcovr)",
)
else:
# Commit mode: default behavior
covered, total, uncovered, skipped_noncoverable = check_commit_coverage(
args.commit, root
)
return report_coverage(
header=f"commit {args.commit}",
total=total,
covered=covered,
uncovered=uncovered,
fail_under=args.fail_under,
total_label="New coverable lines (per gcovr)",
skipped_noncoverable=skipped_noncoverable,
)
except subprocess.CalledProcessError as e:
print(f"Error: Command failed - {e}", file=sys.stderr)
if e.stderr:
print(f"Error details:\n{e.stderr}", file=sys.stderr)
if e.stdout:
print(f"Command output:\n{e.stdout}", file=sys.stderr)
return e.returncode
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,65 @@
--style=kr
--indent=spaces=4
--indent-classes
--indent-switches
--indent-cases
--indent-preproc-block
--indent-preproc-define
--indent-col1-comments
--pad-oper
--unpad-paren
--align-pointer=middle
--align-reference=middle
--convert-tabs
--max-code-length=120
--break-after-logical
--break-closing-braces
--attach-closing-while
--min-conditional-indent=0
--max-continuation-indent=120
--mode=c
# Allows each platform to use its own line endings. This then does
# what Git does out of the box: uses platform line endings in the
# working directory, but uses only LF line endings in the repository.
# `astyle` also natively uses line the endings it finds in the files,
# so this will be LF-only on Linux and CR/LF on Windows, in alignment
# with Git's default behavior. This prevents `astyle` from modifying
# every source file, which Git perceives as a change on Windows platforms.
#--lineend=linux
--suffix=none
--preserve-date
--formatted
--ignore-exclude-errors
--ignore-exclude-errors-x
--exclude=assets
--exclude=test_assets
--exclude=test_fonts
--exclude=../src/lv_conf_internal.h
--exclude=../src/core/lv_obj_style_gen.c
--exclude=../src/core/lv_obj_style_gen.h
--exclude=../src/libs/gif
--exclude=../src/libs/frogfs
--exclude=../src/libs/lodepng/lodepng.c
--exclude=../src/libs/lodepng/lodepng.h
--exclude=../src/libs/qrcode/qrcodegen.c
--exclude=../src/libs/qrcode/qrcodegen.h
--exclude=../src/libs/tjpgd/tjpgd.c
--exclude=../src/libs/tjpgd/tjpgd.h
--exclude=../src/libs/tjpgd/tjpgdcnf.h
--exclude=../src/libs/thorvg
--exclude=../src/libs/expat
--exclude=../src/libs/lz4
--exclude=../src/libs/FT800-FT813
--exclude=../src/drivers/opengles/glad
--exclude=../src/others/vg_lite_tvg/vg_lite.h
--exclude=../demos/high_res/fonts
--exclude=../tests/unity/unity.c
--exclude=../tests/unity/unity_internals.h
--exclude=../tests/unity/unity_support.c
--exclude=../tests/unity/unity_support.h
--exclude=../tests/test_images
--exclude=../tests/build_test_defheap
--exclude=../tests/build_test_sysheap
--exclude=../tests/build_test_vg_lite
--exclude=../tests/build_test_perf32b
--exclude=../tests/build_test_perf64b

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import os
import sys
# Argument enhancement: to only run `astyle` on a specified directory, to
# only include changed source code, these arguments have been added. If
# run with no arguments, all the normal directories are examined as before:
# - /demos/
# - /examples/
# - /src/
# - /tests/
#
# Args:
# If ANY args are specified, ONLY run `astyle` on the specified directories.
# Any combination can be specified.
include_demos = True
include_examples = True
include_src = True
include_tests = True
args = sys.argv[1:]
# Have any args been specified?
if args:
include_demos = False
include_examples = False
include_src = False
include_tests = False
for arg in args:
if arg == "demos":
include_demos = True
elif arg == "examples":
include_examples = True
elif arg == "src":
include_src = True
elif arg == "tests":
include_tests = True
else:
print(f'Argument [{arg}] not recognized.')
print('Usage:')
print(' python code-format.py [dir [dir ...]]')
print(' where: dir can be demos, examples, src or tests.')
exit(1)
script_dir = os.path.realpath(__file__)
script_dir = os.path.dirname(script_dir)
cfg_file = os.path.join(script_dir, 'code-format.cfg')
if include_demos:
print("\nFormatting demos")
os.system(f'astyle --options={cfg_file} --recursive "{script_dir}/../demos/*.c,*.cpp,*.h"')
if include_examples:
print("\nFormatting examples")
os.system(f'astyle --options={cfg_file} --recursive "{script_dir}/../examples/*.c,*.cpp,*.h"')
if include_src:
print("\nFormatting src")
os.system(f'astyle --options={cfg_file} --recursive "{script_dir}/../src/*.c,*.cpp,*.h"')
if include_tests:
print("\nFormatting tests")
os.system(f'astyle --options={cfg_file} --recursive "{script_dir}/../tests/*.c,*.cpp,*.h"')

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cppcheck -j8 --template="{severity}\t{file}:{line}\t{id}: {message}" --enable=all ../src/ --output-file=cppcheck_res.txt --suppress=unusedFunction --suppress=preprocessorErrorDirective --force

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python3
import sys
import textwrap
import re
with open(sys.argv[1], 'r') as file:
s = file.read()
b = bytearray()
if '--filter-character' in sys.argv:
s = re.sub(r'[^\x00-\xff]', '', s)
if '--null-terminate' in sys.argv:
s += '\x00'
b.extend(map(ord, s))
print(textwrap.fill(', '.join([hex(a) for a in b]), 96))

View File

@@ -0,0 +1,4 @@
#!/bin/sh
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )"
sed -n '/LVGL_VERSION_MAJOR/ {N;s@#define \+LVGL_VERSION_M.... \+@@g;s@\n@.@p}' "$SCRIPTPATH/../lv_version.h"

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
import argparse
from typing import List, Tuple
import sys
import os
try:
from fontTools.ttLib import TTFont
except ImportError:
print("Need fonttools package, do `pip3 install fonttools`")
sys.exit(1)
# Fonts that are excluded from the license check
# Only add fonts that are known to be public domain or have a compatible license
_EXCLUDED_FONTS = {
"OpenTypeTest GPOS One",
}
# Font name mapping to remove any style suffix
_FONT_NAME_MAP = {
"Montserrat Medium": "Montserrat",
"Montserrat SemiBold": "Montserrat",
"Montserrat Bold": "Montserrat",
"Montserrat Regular": "Montserrat",
"Source Han Sans SC Normal": "Source Han Sans SC",
"Font Awesome 5 Free Solid": "Font Awesome",
"Lato Regular": "Lato",
"Noto Sans SC Regular": "Noto Sans SC",
}
def get_font_full_name(font_path: str) -> str:
font = TTFont(font_path)
name_records = font["name"].names
fallback = None
for record in name_records:
if record.nameID == 1: # ID 1 corresponds to the font family name and will be used if full name doesn't exists
fallback = record.toStr()
if record.nameID == 4: # ID 4 corresponds to the full font name
return record.toStr()
return fallback
def list_intree_fonts(path: str) -> List[Tuple[str, str]]:
fonts = []
for root, _, files in os.walk(path):
for file in files:
if file.lower().endswith((".ttf", ".otf", ".woff", ".woff2")):
font_path = os.path.join(root, file)
font_name = get_font_full_name(font_path).strip()
if font_name:
# Add a no-strict mode to ignore missing license files
fonts.append((font_path, font_name))
return fonts
def has_intree_license(license_root_folder: str, font_name: str) -> bool:
if font_name in _EXCLUDED_FONTS:
return True
# Prepare candidate folder names, ignoring casing
candidates = {
font_name.lower(),
font_name.replace(" ", "_").lower(),
font_name.replace(" ", "").lower(),
}
if font_name in _FONT_NAME_MAP:
candidates.add(_FONT_NAME_MAP[font_name].lower())
candidates.add(_FONT_NAME_MAP[font_name].replace(" ", "_").lower())
candidates.add(_FONT_NAME_MAP[font_name].replace(" ", "").lower())
# List all directories in the license_root_folder
for entry in os.listdir(license_root_folder):
entry_path = os.path.join(license_root_folder, entry)
if os.path.isdir(entry_path):
# Compare the directory name in lowercase with candidates
if entry.lower() in candidates:
# Check if the directory contains at least one file (ignoring subdirectories)
for item in os.listdir(entry_path):
item_path = os.path.join(entry_path, item)
if os.path.isfile(item_path):
return True
return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Verify font licenses")
parser.add_argument(
"--no-strict",
action="store_true",
help="Ignore missing license files",
default=False,
)
args = parser.parse_args()
SCRIPT_DIR = os.path.dirname(__file__)
# List of font files in the tree
fonts = list_intree_fonts(os.path.join(SCRIPT_DIR, ".."))
has_font_without_license = False
print("Verifying license for fonts:")
for path, name in fonts:
if has_intree_license(
os.path.join(SCRIPT_DIR, "built_in_font", "font_license"), name
):
print(f" [OK] '{name}'")
else:
print(f" [MISSING] '{name}' - {path}")
has_font_without_license = True
if has_font_without_license and not args.no_strict:
sys.exit(1)

View File

@@ -0,0 +1 @@
3.10

View File

@@ -0,0 +1,37 @@
# lvglgdb
lvglgdb is a GDB script for LVGL.
# Installation
```bash
pip install lvglgdb
```
# Simple Usage
In your GDB session, run:
```bash
py import lvglgdb
dump obj
dump cache image
dump cache image_header
```
# Structure
```mermaid
graph TD
lvgl["lvgl<br/>(mem→python object)"]
gdb_cmds["gdb_cmds<br/>(gdb commands)"]
lvglgdb["lvglgdb"]
lvglgdb --> lvgl
lvglgdb --> gdb_cmds
gdb_cmds --> lvgl
classDef pkg fill:white,stroke:gray
classDef core fill:white,stroke:gray
class lvglgdb,lvgl,gdb_cmds pkg
```

View File

@@ -0,0 +1,13 @@
import sys
from os import path
here = path.dirname(path.abspath(__file__))
if __name__ == "__main__":
if here not in sys.path:
sys.path.insert(0, here)
for key in tuple(filter(lambda m: m.startswith("lvglgdb"), sys.modules.keys())):
del sys.modules[key]
import lvglgdb # noqa: F401

View File

@@ -0,0 +1,38 @@
from .value import Value
from .lvgl import (
curr_inst,
LVDisplay,
LVDrawBuf,
LVList,
LVObject,
dump_style_info,
LVCache,
LVCacheEntry,
LVCacheLRURB,
LVCacheLRURBIterator,
LVCacheIteratorBase,
LVImageCache,
LVImageHeaderCache,
create_cache_iterator,
LVRedBlackTree,
)
from . import cmds as cmds
__all__ = [
"curr_inst",
"LVDisplay",
"LVDrawBuf",
"LVList",
"LVCache",
"LVRedBlackTree",
"LVObject",
"dump_style_info",
"Value",
"LVCacheEntry",
"LVCacheLRURB",
"LVCacheLRURBIterator",
"LVCacheIteratorBase",
"LVImageCache",
"LVImageHeaderCache",
"create_cache_iterator",
]

View File

@@ -0,0 +1,32 @@
import gdb
from .core import DumpObj
from .display import DumpDisplayBuf
from .draw import InfoDrawUnit
from .misc import InfoStyle, DumpCache
from .debugger import Debugger
from .drivers import Lvglobal
__all__ = []
# Set pagination off and python print-stack full
gdb.execute("set pagination off")
gdb.write("set pagination off\n")
gdb.execute("set python print-stack full")
gdb.write("set python print-stack full\n")
# Debugger
Debugger()
# Dumps
DumpObj()
DumpDisplayBuf()
DumpCache()
# Infos
InfoStyle()
InfoDrawUnit()
# Drivers
Lvglobal()

View File

@@ -0,0 +1,5 @@
from .lv_obj import DumpObj
__all__ = [
"DumpObj",
]

View File

@@ -0,0 +1,64 @@
import argparse
import gdb
from lvglgdb.lvgl import curr_inst
from lvglgdb.lvgl import LVObject, dump_obj_info
from lvglgdb.value import Value
class DumpObj(gdb.Command):
"""dump obj tree from specified obj"""
def __init__(self):
super(DumpObj, self).__init__(
"dump obj", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def dump_obj(self, obj: LVObject, depth=0, limit=None):
if not obj:
return
# dump self
print(" " * depth, end="")
dump_obj_info(obj)
if limit is not None and depth >= limit:
return
# dump children
for child in obj.children:
self.dump_obj(child, depth + 1, limit=limit)
def invoke(self, args, from_tty):
parser = argparse.ArgumentParser(description="Dump lvgl obj tree.")
parser.add_argument(
"-L",
"--level",
type=int,
default=None,
help="Limit the depth of the tree.",
)
parser.add_argument(
"root",
type=str,
nargs="?",
default=None,
help="Optional root obj to dump.",
)
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
if args.root:
root = gdb.parse_and_eval(args.root)
root = LVObject(Value(root))
self.dump_obj(root, limit=args.level)
else:
# dump all displays
depth = 0
for disp in curr_inst().displays():
print(f"Display {hex(disp)}")
for screen in disp.screens:
print(f'{" " * (depth + 1)}Screen@{hex(screen)}')
self.dump_obj(screen, depth=depth + 1, limit=args.level)

View File

@@ -0,0 +1,75 @@
import argparse
import gdb
class Debugger(gdb.Command):
"""Start debugpy server or connect to a debug server, so we can debug python code from IDE like PyCharm/VSCode"""
def __init__(self, host="localhost", port=11451):
self.__host = host
self.__port = port
super().__init__("debugger", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
parser = argparse.ArgumentParser(description=Debugger.__doc__)
parser.add_argument(
"--host",
type=str,
help="Server listening host",
)
parser.add_argument(
"-p",
"--port",
type=int,
help="Server listening port",
)
parser.add_argument(
"-t",
"--type",
choices=["pycharm", "vscode", "eclipse"],
help="Debugger type",
required=True,
)
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
if args.host:
self.__host = args.host
if args.port:
self.__port = args.port
if args.type == "pycharm":
self.connect_to_pycharm()
elif args.type == "vscode":
self.connect_to_vscode()
elif args.type == "eclipse":
self.connect_to_eclipse()
def connect_to_pycharm(self):
try:
import pydevd_pycharm
except ImportError:
print("pydevd_pycharm module not found. Please install it using pip.")
return
pydevd_pycharm.settrace(
self.__host, port=self.__port, stdoutToServer=True, stderrToServer=True
)
def connect_to_vscode(self):
try:
import debugpy
except ImportError:
print("debugpy module not found. Please install it using pip.")
return
debugpy.listen((self.__host, self.__port))
debugpy.wait_for_client()
def connect_to_eclipse(self):
print("Eclipse is not implemented yet")

View File

@@ -0,0 +1,5 @@
from .lv_display import DumpDisplayBuf
__all__ = [
"DumpDisplayBuf",
]

View File

@@ -0,0 +1,54 @@
import argparse
import gdb
from lvglgdb.lvgl import curr_inst
from lvglgdb.lvgl import LVDrawBuf
class DumpDisplayBuf(gdb.Command):
"""dump display buf to image"""
def __init__(self):
super(DumpDisplayBuf, self).__init__(
"dump display", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def invoke(self, args, from_tty):
parser = argparse.ArgumentParser(description="Dump display draw buffer.")
parser.add_argument(
"-p",
"--prefix",
type=str,
default="",
help="prefix of dump file path",
)
parser.add_argument(
"-f",
"--format",
type=str,
choices=["bmp", "png"],
default=None,
help="dump file format (bmp or png)",
)
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
display = curr_inst().disp_default()
if not display:
print("Error: Invalid display pointer")
return
buffers = {
"buf_1": display.buf_1,
"buf_2": display.buf_2,
}
for buf_name, buf_ptr in buffers.items():
if buf_ptr is not None:
draw_buf = LVDrawBuf(buf_ptr)
filename = f"{args.prefix}{buf_name}.{args.format.lower() if args.format else 'bmp'}"
draw_buf.data_dump(filename, args.format)
else:
print(f"Warning: {buf_name} buffer is None, skipping.")

View File

@@ -0,0 +1,5 @@
from .lv_draw import InfoDrawUnit
__all__ = [
"InfoDrawUnit",
]

View File

@@ -0,0 +1,50 @@
import gdb
from lvglgdb.value import Value
from lvglgdb.lvgl import curr_inst
class InfoDrawUnit(gdb.Command):
"""dump draw unit info"""
def __init__(self):
super(InfoDrawUnit, self).__init__(
"info draw_unit", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def dump_draw_unit(self, draw_unit: Value):
# Dereference to get the string content of the name from draw_unit
name = draw_unit.name.string()
# Print draw_unit information and the name
print(f"Draw Unit: {draw_unit}, Name: {name}")
# Handle different draw_units based on the name
def lookup_type(name):
try:
return gdb.lookup_type(name)
except gdb.error:
return None
types = {
"DMA2D": lookup_type("lv_draw_dma2d_unit_t"),
"NEMA_GFX": lookup_type("lv_draw_nema_gfx_unit_t"),
"NXP_PXP": lookup_type("lv_draw_pxp_unit_t"),
"NXP_VGLITE": lookup_type("lv_draw_vglite_unit_t"),
"OPENGLES": lookup_type("lv_draw_opengles_unit_t"),
"DAVE2D": lookup_type("lv_draw_dave2d_unit_t"),
"SDL": lookup_type("lv_draw_sdl_unit_t"),
"SW": lookup_type("lv_draw_sw_unit_t"),
"VG_LITE": lookup_type("lv_draw_vg_lite_unit_t"),
}
type = types.get(name, lookup_type("lv_draw_unit_t"))
print(
draw_unit.cast(type, ptr=True)
.dereference()
.format_string(pretty_structs=True, symbols=True)
)
def invoke(self, args, from_tty):
for unit in curr_inst().draw_units():
self.dump_draw_unit(unit)

View File

@@ -0,0 +1,5 @@
from .nuttx import Lvglobal
__all__ = [
"Lvglobal",
]

View File

@@ -0,0 +1,5 @@
from .nuttx import Lvglobal
__all__ = [
"Lvglobal",
]

View File

@@ -0,0 +1,55 @@
# GDB script to get lvgl global pointer in NuttX.
import argparse
import sys
import gdb
class Lvglobal(gdb.Command):
"""Set which lvgl instance to inspect by finding lv_global pointer in task TLS data."""
def __init__(self):
super(Lvglobal, self).__init__("lvglobal", gdb.COMMAND_USER)
def set_lvgl_instance(self, inst):
from lvglgdb.lvgl import curr_inst
curr_inst().reset()
curr_inst().ensure_init(inst)
def invoke(self, arg, from_tty):
parser = argparse.ArgumentParser(description=self.__doc__)
parser.add_argument("-p", "--pid", type=int, help="Optional process ID")
try:
args = parser.parse_args(gdb.string_to_argv(arg))
except SystemExit:
return
try:
from nxgdb import utils
except ImportError:
print("nxgdb is not installed, can't find lvgl global pointer.")
return
lv_global = utils.gdb_eval_or_none("lv_global")
if lv_global:
print(f"Found single instance lv_global@{hex(lv_global.address)}")
self.set_lvgl_instance(lv_global)
return
# find the lvgl global pointer in tls
if not args.pid:
print("LVGL is in multi-process mode; please provide --pid.")
return
lv_key = utils.gdb_eval_or_none("lv_nuttx_tlskey")
if lv_key is None:
lv_key = utils.gdb_eval_or_none("lv_global_default::index")
if lv_key is None:
print("Can't find lvgl tls key in multi-process mode.")
return
lv_global = utils.get_task_tls(args.pid, lv_key)
if lv_global:
print(f"Found lv_global@{hex(lv_global)}")
self.set_lvgl_instance(lv_global)
else:
print(f"\nCan't find lv_global with tlskey@{hex(lv_key)}.")

View File

@@ -0,0 +1,4 @@
from .lv_style import InfoStyle
from .lv_cache import DumpCache
__all__ = ["InfoStyle", "DumpCache"]

View File

@@ -0,0 +1,40 @@
import argparse
import gdb
class DumpCache(gdb.Command):
"""dump cache info for specified cache"""
def __init__(self):
super(DumpCache, self).__init__(
"dump cache", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def invoke(self, args, from_tty):
parser = argparse.ArgumentParser(description="Dump lvgl cache info.")
parser.add_argument(
"cache",
type=str,
choices=["image", "image_header"],
default="image",
help="cache to dump.",
)
from lvglgdb import curr_inst
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
cache = None
if args.cache == "image":
cache = curr_inst().image_cache()
elif args.cache == "image_header":
cache = curr_inst().image_header_cache()
if not cache:
print("Invalid cache: ", args.cache)
return
cache.print_entries()

View File

@@ -0,0 +1,40 @@
import argparse
import gdb
from lvglgdb.value import Value
from lvglgdb.lvgl import LVObject
from lvglgdb.lvgl import dump_style_info
class InfoStyle(gdb.Command):
"""dump obj style value for specified obj"""
def __init__(self):
super(InfoStyle, self).__init__(
"info style", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def invoke(self, args, from_tty):
parser = argparse.ArgumentParser(description="Dump lvgl obj local style.")
parser.add_argument(
"obj",
type=str,
help="obj to show style.",
)
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
obj = gdb.parse_and_eval(args.obj)
if not obj:
print("Invalid obj: ", args.obj)
return
obj = Value(obj)
# show all styles applied to this obj
for style in LVObject(obj).styles:
print(" ", end="")
dump_style_info(style)

View File

@@ -0,0 +1,43 @@
from .core import LVObject, curr_inst, dump_obj_info
from .display import LVDisplay
from .draw import LVDrawBuf
from .misc import (
LVList,
dump_style_info,
LVRedBlackTree,
dump_rb_info,
LVCache,
dump_cache_info,
LVCacheEntry,
dump_cache_entry_info,
LVCacheLRURB,
dump_lru_rb_cache_info,
LVCacheLRURBIterator,
LVCacheIteratorBase,
LVImageCache,
LVImageHeaderCache,
create_cache_iterator,
)
__all__ = [
"LVObject",
"LVDisplay",
"LVDrawBuf",
"curr_inst",
"LVList",
"dump_style_info",
"dump_obj_info",
"LVRedBlackTree",
"dump_rb_info",
"LVCache",
"dump_cache_info",
"LVCacheEntry",
"dump_cache_entry_info",
"LVCacheLRURB",
"dump_lru_rb_cache_info",
"LVCacheLRURBIterator",
"LVCacheIteratorBase",
"LVImageCache",
"LVImageHeaderCache",
"create_cache_iterator",
]

View File

@@ -0,0 +1,8 @@
from .lv_obj import LVObject, dump_obj_info
from .lv_global import curr_inst
__all__ = [
"LVObject",
"curr_inst",
"dump_obj_info",
]

View File

@@ -0,0 +1,117 @@
import gdb
from typing import Optional, Union, Iterator, TYPE_CHECKING
from lvglgdb.value import Value
from ..misc.lv_ll import LVList
"""
LVGL global instance
Note:
lv_global is the global instance of LVGL.
It will contain other components like display, draw unit, etc.
So if you want add a new component, please add it in this class.
And just import your component when you need it. Not expose to the whole file.
"""
__all__ = ["curr_inst"]
"""
Type hint for IDE, just imported at typechecking phase not in runtime.
"""
if TYPE_CHECKING:
from ..display.lv_display import LVDisplay
class LVGL:
"""LVGL instance"""
def __init__(self, lv_global: Value):
self.lv_global = lv_global.cast("lv_global_t", ptr=True)
def displays(self) -> "Iterator[LVDisplay]":
ll = self.lv_global.disp_ll
if not ll:
return
from ..display.lv_display import LVDisplay
for disp in LVList(ll, "lv_display_t"):
yield LVDisplay(disp)
def disp_default(self):
from ..display.lv_display import LVDisplay
disp_default = self.lv_global.disp_default
return LVDisplay(disp_default) if disp_default else None
def screen_active(self):
disp = self.lv_global.disp_default
return disp.act_scr if disp else None
def draw_units(self):
unit = self.lv_global.draw_info.unit_head
# Iterate through all draw units
while unit:
yield unit
unit = unit.next
def image_cache(self):
from ..misc.lv_image_cache import LVImageCache
return LVImageCache(self.lv_global.img_cache)
def image_header_cache(self):
from ..misc.lv_image_header_cache import LVImageHeaderCache
return LVImageHeaderCache(self.lv_global.img_header_cache)
class _LVGLSingleton:
__slots__ = ("_lvgl", "_ready")
def __init__(self) -> None:
self._lvgl: Optional[object] = None
self._ready = False
def ensure_init(self, lv_global: Union[gdb.Value, Value, None] = None) -> bool:
if self._ready:
return True
if lv_global is None:
try:
lv_global = Value(gdb.parse_and_eval("lv_global").address)
except gdb.error as e:
print(f"Failed to get lv_global: {e}")
return False
elif not isinstance(lv_global, Value):
lv_global = Value(lv_global).cast("lv_global_t", ptr=True)
if not lv_global.inited:
print(
"\x1b[31mlvgl is not initialized yet. "
"Please call `ensure_init()` later.\x1b[0m"
)
return False
self._lvgl = LVGL(lv_global)
self._ready = True
return True
def __getattr__(self, name: str):
if not self._ready and not self.ensure_init():
raise RuntimeError("LVGL singleton not ready")
return getattr(self._lvgl, name)
def reset(self) -> None:
self._lvgl = None
self._ready = False
__curr_inst = _LVGLSingleton()
def curr_inst() -> _LVGLSingleton:
"""Get the global instance of LVGL"""
return __curr_inst

View File

@@ -0,0 +1,74 @@
from lvglgdb.value import Value
class LVObject(Value):
"""LVGL object"""
def __init__(self, obj: Value):
super().__init__(obj.cast("lv_obj_t", ptr=True))
@property
def class_name(self):
name = self.class_p.name
if name:
return name.string()
return self.class_p.format_string(symbols=True, address=True, styling=True)
@property
def x1(self):
return int(self.coords.x1)
@property
def y1(self):
return int(self.coords.y1)
@property
def x2(self):
return int(self.coords.x2)
@property
def y2(self):
return int(self.coords.y2)
@property
def child_count(self):
return self.spec_attr.child_cnt if self.spec_attr else 0
@property
def children(self):
if not self.spec_attr:
return
for i in range(self.child_count):
child = self.spec_attr.children[i]
yield LVObject(child)
@property
def styles(self):
LV_STYLE_PROP_INV = 0
LV_STYLE_PROP_ANY = 0xFF
count = self.style_cnt
if count == 0:
return
styles = self.super_value("styles")
for i in range(count):
style = styles[i].style
prop_cnt = style.prop_cnt
values_and_props = style.values_and_props.cast(
"lv_style_const_prop_t", ptr=True
)
for j in range(prop_cnt):
prop = values_and_props[j].prop
if prop == LV_STYLE_PROP_INV or prop == LV_STYLE_PROP_ANY:
continue
yield values_and_props[j]
def get_child(self, index: int):
return self.spec_attr.children[index] if self.spec_attr else None
def dump_obj_info(obj: LVObject):
clzname = obj.class_name
coords = f"{obj.x1},{obj.y1},{obj.x2},{obj.y2}"
print(f"{clzname}@{hex(obj)} {coords}")

View File

@@ -0,0 +1,5 @@
from .lv_display import LVDisplay
__all__ = [
"LVDisplay",
]

View File

@@ -0,0 +1,45 @@
from ..core.lv_obj import LVObject
from ..draw.lv_draw_buf import LVDrawBuf
from lvglgdb.value import Value
class LVDisplay(Value):
"""LVGL display"""
def __init__(self, disp: Value):
super().__init__(disp)
@property
def hor_res(self) -> int:
"""Get horizontal resolution in pixels"""
return int(self.super_value("hor_res"))
@property
def ver_res(self) -> int:
"""Get vertical resolution in pixels"""
return int(self.super_value("ver_res"))
@property
def screens(self):
screens = self.super_value("screens")
for i in range(self.screen_cnt):
yield LVObject(screens[i])
# Buffer-related properties
@property
def buf_1(self):
"""Get first draw buffer (may be None)"""
buf_ptr = self.super_value("buf_1")
return LVDrawBuf(buf_ptr) if buf_ptr else None
@property
def buf_2(self):
"""Get second draw buffer (may be None)"""
buf_ptr = self.super_value("buf_2")
return LVDrawBuf(buf_ptr) if buf_ptr else None
@property
def buf_act(self):
"""Get currently active draw buffer (may be None)"""
buf_ptr = self.super_value("buf_act")
return LVDrawBuf(buf_ptr) if buf_ptr else None

View File

@@ -0,0 +1,5 @@
from .lv_draw_buf import LVDrawBuf
__all__ = [
"LVDrawBuf",
]

View File

@@ -0,0 +1,205 @@
from pathlib import Path
from typing import Optional
import gdb
import numpy as np
from PIL import Image
from lvglgdb.value import Value
class LVDrawBuf(Value):
"""LVGL draw buffer"""
_color_formats = {
"RGB565": None,
"RGB888": None,
"ARGB8888": None,
"XRGB8888": None,
}
_bpp = {
"RGB565": 16,
"RGB888": 24,
"ARGB8888": 32,
"XRGB8888": 32,
}
def __init__(self, draw_buf: Value):
super().__init__(draw_buf)
self._init_color_formats()
def _init_color_formats(self):
"""init color formats from enum"""
for fmt in self._color_formats.keys():
if self._color_formats[fmt] is None:
try:
self._color_formats[fmt] = int(
gdb.parse_and_eval(f"LV_COLOR_FORMAT_{fmt}")
)
except gdb.error:
print(f"Warning: Failed to get LV_COLOR_FORMAT_{fmt}")
self._color_formats[fmt] = -1
def color_format_info(self) -> dict:
"""get color format info"""
header = self.super_value("header")
cf = int(header["cf"])
fmt_name = "UNKNOWN"
for name, val in self._color_formats.items():
if val == cf:
fmt_name = name
break
return {"value": cf, "name": fmt_name, "bpp": self._bpp.get(fmt_name, 0)}
@property
def header(self) -> dict:
"""Get the image header information as a dictionary"""
header = self.super_value("header")
return {
"w": header["w"],
"h": header["h"],
"stride": header["stride"],
"cf": header["cf"], # Color format
}
@property
def data_size(self) -> int:
"""Get the total buffer size in bytes"""
return self.super_value("data_size")
@property
def data(self) -> bytes:
"""Get the buffer data ptr"""
return self.super_value("data")
def data_dump(self, filename: str, format: str = None) -> bool:
"""
Dump the buffer data to an image file.
Args:
filename: Output file path
format: Image format (None for auto-detection from filename)
Returns:
bool: True if successful, False otherwise
"""
try:
# Validate input parameters
if not filename:
raise ValueError("Output filename cannot be empty")
# Get buffer metadata
header = self.super_value("header")
stride = int(header["stride"])
height = int(header["h"])
cf_info = self.color_format_info()
data_ptr = self.super_value("data")
data_size = int(self.super_value("data_size"))
width = (stride * 8) // cf_info["bpp"] if cf_info["bpp"] else 0
expected_data_size = stride * height
# Validate buffer data
if not data_ptr:
raise ValueError("Data pointer is NULL")
if width <= 0 or height <= 0:
raise ValueError(f"Invalid dimensions: {width}x{height}")
if data_size <= 0:
raise ValueError(f"Invalid data size: {data_size}")
if data_size < expected_data_size:
raise ValueError(
f"Data size mismatch: expected {expected_data_size}, got {data_size}"
)
elif data_size > expected_data_size:
gdb.write(
f"\033[93mData size mismatch: expected {expected_data_size}, got {data_size}\033[0m\n"
)
# Read pixel data
pixel_data = (
gdb.selected_inferior()
.read_memory(int(data_ptr), expected_data_size)
.tobytes()
)
if not pixel_data:
raise ValueError("Failed to read pixel data")
# Process based on color format
img = self._convert_to_image(pixel_data, width, height, cf_info["value"])
if img is None:
return False
# Determine output format
output_format = (
format.upper() if format else Path(filename).suffix[1:].upper() or "BMP"
)
# Save image
img.save(filename, format=output_format)
print(
f"Successfully saved {cf_info['name']} buffer as {output_format} to {filename}"
)
return True
except gdb.MemoryError:
print("Error: Failed to access memory")
return False
except ValueError as e:
print(f"Validation error: {str(e)}")
return False
except Exception as e:
print(f"Unexpected error: {str(e)}")
return False
def _convert_to_image(
self, pixel_data: bytes, width: int, height: int, color_format: int
) -> Optional[Image.Image]:
"""
Convert raw pixel data to PIL Image based on color format.
Args:
pixel_data: Raw pixel bytes
width: Image width
height: Image height
color_format: LVGL color format value
Returns:
PIL.Image or None if conversion fails
"""
try:
if color_format == self._color_formats["RGB565"]:
# Convert RGB565 to RGB888
arr = np.frombuffer(pixel_data, dtype=np.uint8)
arr = arr.reshape((height, width, 2))
rgb565 = np.frombuffer(pixel_data, dtype=np.uint16).reshape(
(height, width)
)
r = (((rgb565 & 0xF800) >> 11) << 3).astype(np.uint8)
g = ((rgb565 & 0x07E0) >> 3).astype(np.uint8)
b = ((rgb565 & 0x001F) << 3).astype(np.uint8)
return Image.fromarray(np.dstack((r, g, b)), "RGB")
elif color_format == self._color_formats["RGB888"]:
arr = np.frombuffer(pixel_data, dtype=np.uint8).reshape(
height, width, 3
)
rgb_arr = arr[:, :, [2, 1, 0]] # BGR -> RGB
return Image.fromarray(rgb_arr, "RGB")
elif color_format == self._color_formats["ARGB8888"]:
arr = np.frombuffer(pixel_data, dtype=np.uint8).reshape(-1, 4)
rgba = np.column_stack((arr[:, 1:4], arr[:, 0])) # ARGB -> RGBA
return Image.frombytes("RGBA", (width, height), rgba.tobytes())
elif color_format == self._color_formats["XRGB8888"]:
arr = np.frombuffer(pixel_data, dtype=np.uint8).reshape(-1, 4)
rgb = arr[:, [2, 1, 0]] # BGR -> RGB
return Image.fromarray(rgb.reshape(height, width, 3), "RGB")
raise ValueError(f"Unsupported color format: 0x{color_format:x}")
except Exception as e:
print(f"Conversion error: {str(e)}")
return None

View File

@@ -0,0 +1,194 @@
# LVGL GDB Scripts - Misc Module Extensions
This directory contains GDB debugging scripts for LVGL's miscellaneous data structures, including red-black trees and
cache systems.
## Objects
### 1. lv_rb.py - Red-Black Tree Support
Provides debugging support for LVGL's red-black tree (`lv_rb_t`) implementation.
**Classes:**
- `LVRedBlackTree` - Iterator for red-black tree nodes
**Functions:**
- `dump_rb_info(rb, datatype)` - Dump basic red-black tree information
**Usage:**
```gdbscript
# In GDB:
python tree = LVRedBlackTree(some_rb_tree)
python for data in tree: print(data)
python dump_rb_info(some_rb_tree, datatype)
```
### 2. lv_cache.py - Cache System Support
Provides debugging support for LVGL's cache system (`lv_cache_t`).
**Classes:**
- `LVCache` - Wrapper for cache objects
**Functions:**
- `dump_cache_info(cache, datatype)` - Dump cache information
**Usage:**
```gdbscript
# In GDB:
python cache = LVCache(some_cache)
python print(f"Cache usage: {cache.get_usage_percentage()}%")
python dump_cache_info(some_cache, datatype)
```
### 3. lv_cache_entry.py - Cache Entry Support
Provides debugging support for cache entries (`lv_cache_entry_t`).
**Classes:**
- `LVCacheEntry` - Wrapper for cache entries
**Functions:**
- `dump_cache_entry_info(entry, datatype)` - Dump cache entry information
**Usage:**
```gdbscript
# In GDB:
python entry = LVCacheEntry(some_entry)
python print(f"Ref count: {entry.get_ref_count()}")
python dump_cache_entry_info(some_entry, datatype)
```
### 4. lv_cache_lru_rb.py - LRU Cache with RB Tree Support
Specialized support for LRU-based caches using red-black trees.
**Classes:**
- `LVCacheLRURB` - Specialized cache wrapper for LRU RB implementations
**Functions:**
- `dump_lru_rb_cache_info(cache)` - Dump LRU RB cache information
### 5. lv_image_cache.py and lv_image_header_cache.py - Image Cache Support
Provides debugging support for LVGL's image cache (`lv_image_cache_t`).
**Classes:**
- `LVImageCache` - Wrapper for image cache objects
**Usage:**
```gdbscript
# In GDB:
python curr_inst().image_cache().print_info()
python curr_inst().image_cache().print_entries()
```
## Integration
All these modules are automatically imported and available when using the LVGL GDB scripts. You can access them through
the main `lvgl` module:
```gdbscript
# In GDB:
python from lvglgdb import LVRedBlackTree, LVCache, dump_rb_info, dump_cache_info
```
## Type Conversion Support
All wrapper classes support multiple input types:
- `gdb.Value` objects (from GDB expressions)
- `Value` objects (from lvglgdb.value module)
- Raw pointers as integers (e.g., `0x12345678`)
This allows flexible usage patterns:
```gdbscript
# From GDB variable
entry = LVCacheEntry(my_entry)
# From raw memory address
entry = LVCacheEntry(0x7fff12345678)
# From GDB expression result
entry = LVCacheEntry(gdb.parse_and_eval("*(lv_cache_entry_t*)0x7fff12345678"))
```
## Examples
### Debugging a Red-Black Tree
```gdbscript
# Create tree iterator from variable
tree = LVRedBlackTree(some_rb_variable)
# Create from raw pointer (0x12345678)
tree = LVRedBlackTree(0x12345678)
# Iterate through all nodes
for data_ptr in tree:
print(f"Data pointer: {data_ptr}")
# Get tree info
print(f"Tree has {len(tree)} nodes")
tree.print_info()
```
### Debugging a Cache
```gdbscript
# Create cache wrapper from variable
cache = LVCache(some_cache_variable)
# Create from raw pointer (0x12345678)
cache = LVCache(0x12345678)
# Check cache status
print(f"Cache usage: {cache.get_usage_percentage():.1f}%")
print(f"Cache enabled: {cache.is_enabled()}")
# Dump full info
cache.print_info()
```
### Debugging Cache Entries
```gdbscript
# Create entry wrapper from variable
entry = LVCacheEntry(some_entry_variable)
# Create from raw pointer (0x12345678)
entry = LVCacheEntry(0x12345678)
# Check entry status
print(f"Ref count: {entry.get_ref_count()}")
print(f"Invalid: {entry.is_invalid()}")
# Dump full info
entry.print_info()
```
## File Structure
- `lv_rb.py` - Red-black tree debugging support
- `lv_cache.py` - General cache debugging support
- `lv_cache_entry.py` - Cache entry debugging support
- `lv_cache_lru_rb.py` - LRU RB cache specialized support
- `lv_image_cache.py` - Image cache debugging support
- `lv_image_header_cache.py` - Image header cache debugging support
- `lv_cache_iter_base.py` - Cache iterator base class
- `lv_cache_iter_factory.py` - Cache iterator factory functions
- `__init__.py` - Module exports and initialization

View File

@@ -0,0 +1,28 @@
from .lv_ll import LVList
from .lv_style import dump_style_info
from .lv_rb import LVRedBlackTree, dump_rb_info
from .lv_cache import LVCache, dump_cache_info
from .lv_cache_entry import LVCacheEntry, dump_cache_entry_info
from .lv_cache_lru_rb import LVCacheLRURB, dump_lru_rb_cache_info, LVCacheLRURBIterator
from .lv_cache_iter_base import LVCacheIteratorBase
from .lv_cache_iter_factory import create_cache_iterator
from .lv_image_cache import LVImageCache
from .lv_image_header_cache import LVImageHeaderCache
__all__ = [
"LVList",
"dump_style_info",
"LVRedBlackTree",
"dump_rb_info",
"LVCache",
"dump_cache_info",
"LVCacheEntry",
"dump_cache_entry_info",
"LVCacheLRURB",
"dump_lru_rb_cache_info",
"LVCacheIteratorBase",
"LVCacheLRURBIterator",
"LVImageCache",
"LVImageHeaderCache",
"create_cache_iterator",
]

View File

@@ -0,0 +1,99 @@
from typing import Union, List, Optional, Dict
import gdb
from lvglgdb.value import Value
from .lv_cache_iter_factory import create_cache_iterator
class LVCache(Value):
"""LVGL cache wrapper - focuses on cache-level operations"""
def __init__(
self, cache: Union[Value, gdb.Value, int], datatype: Union[gdb.Type, str]
):
# Convert to Value first if needed
if isinstance(cache, int):
cache = Value(cache).cast("lv_cache_t", ptr=True)
if cache is None:
raise ValueError("Failed to cast pointer to lv_cache_t")
elif isinstance(cache, gdb.Value) and not isinstance(cache, Value):
cache = Value(cache)
elif not cache:
raise ValueError("Invalid cache")
self.datatype = (
gdb.lookup_type(datatype).pointer()
if isinstance(datatype, str)
else datatype
)
super().__init__(cache)
def print_info(self):
"""Dump cache information"""
print(f"Cache Info:")
print(f" Name: {self.name.as_string()}")
print(f" Node Size: {int(self.node_size)}")
print(f" Max Size: {int(self.max_size)}")
print(f" Current Size: {int(self.size)}")
print(f" Free Size: {int(self.max_size) - int(self.size)}")
print(f" Enabled: {bool(int(self.max_size) > 0)}")
# Try to identify cache type
try:
iterator = create_cache_iterator(self)
print(f" Iterator Type: {iterator.__class__.__name__}")
iterator.cache.print_info()
except gdb.error:
pass
def is_enabled(self):
"""Check if cache is enabled"""
return int(self.max_size) > 0
def get_usage_percentage(self):
"""Get cache usage percentage"""
if int(self.max_size) == 0:
return 0.0
return int(self.size) / int(self.max_size) * 100.0
def __iter__(self):
"""Create appropriate iterator based on cache class"""
return create_cache_iterator(self)
def items(self):
"""Get all cache entries as a list"""
entries = []
for entry in self:
entries.append(entry)
return entries
def print_entries(self, max_entries=10):
"""Print cache entries in readable format"""
cache_entries = self.items()
cache_entries_cnt = len(cache_entries)
print(f"Cache Entries ({cache_entries_cnt} total):")
count = 0
for i, entry in enumerate(cache_entries):
if count >= max_entries:
print(
f" ... showing first {max_entries} of {cache_entries_cnt} entries"
)
break
print(f" [{i}] {entry}")
count += 1
if count == 0:
print(" (empty)")
elif count < int(cache_entries_cnt):
print(f" ... {cache_entries_cnt - count} more entries not shown")
def dump_cache_info(
cache: Union[Value, gdb.Value, int], datatype: Union[gdb.Type, str]
):
"""Dump cache information"""
cache_obj = LVCache(cache, datatype)
cache_obj.print_info()

View File

@@ -0,0 +1,103 @@
from typing import Union
import gdb
from lvglgdb.value import Value
class LVCacheEntry(Value):
"""LVGL cache entry wrapper - focuses on entry-level operations"""
def __init__(
self, entry: Union[Value, gdb.Value, int], datatype: Union[gdb.Type, str]
):
# Convert to Value first if needed
if isinstance(entry, int):
entry = Value(entry).cast("lv_cache_entry_t", ptr=True)
if entry is None:
raise ValueError("Failed to cast pointer to lv_cache_entry_t")
elif isinstance(entry, gdb.Value) and not isinstance(entry, Value):
entry = Value(entry)
elif not entry:
raise ValueError("Invalid cache entry")
self.datatype = (
gdb.lookup_type(datatype).pointer()
if isinstance(datatype, str)
else datatype
)
super().__init__(entry)
@classmethod
def from_data_ptr(
cls, data_ptr: Union[Value, gdb.Value, int], datatype: Union[gdb.Type, str]
):
"""Create LVCacheEntry from data pointer"""
if data_ptr.type == gdb.lookup_type("void").pointer() and datatype is None:
raise ValueError("Data pointer is void*, datatype must be provided")
data_ptr = data_ptr.cast(datatype)
entry_ptr = int(data_ptr) + data_ptr.type.target().sizeof
return cls(entry_ptr, datatype)
def print_info(self):
"""Dump cache entry information"""
print(f"Cache Entry Info:")
print(f" Reference Count: {int(self.ref_cnt)}")
print(f" Node Size: {int(self.node_size)}")
print(f" Flags: {int(self.flags)}")
print(f" Invalid: {self.is_invalid()}")
print(f" Disable Delete: {self.is_disabled_delete()}")
# Try to get cache info if available
try:
cache = self.cache
if cache:
print(f" Cache: {cache}")
except:
pass
def get_data(self):
"""Get entry data pointer"""
data_ptr = Value(int(self) - self.get_node_size()).cast(self.datatype)
return data_ptr
def is_invalid(self):
"""Check if entry is invalid"""
return bool(int(self.flags) & 1) # LV_CACHE_ENTRY_FLAG_INVALID
def is_disabled_delete(self):
"""Check if entry has disable delete flag"""
return bool(int(self.flags) & 2) # LV_CACHE_ENTRY_FLAG_DISABLE_DELETE
def get_ref_count(self):
"""Get reference count"""
return int(self.ref_cnt)
def get_node_size(self):
"""Get node size"""
return int(self.node_size)
def get_flags(self):
"""Get flags"""
return int(self.flags)
def __str__(self):
"""Provide better string representation for debugging"""
try:
data = self.get_data()
return f"CacheEntry(ref_cnt={self.get_ref_count()}, valid={not self.is_invalid()}, data={data.dereference()})"
except gdb.error:
pass
return super().__str__()
def dump_cache_entry_info(
entry: Union[Value, gdb.Value, int], datatype: Union[gdb.Type, str]
):
"""Dump cache entry information"""
entry_obj = LVCacheEntry(entry, datatype)
entry_obj.print_info()

View File

@@ -0,0 +1,36 @@
from typing import List, Optional
import gdb
from lvglgdb.value import Value
class LVCacheIteratorBase:
"""Base class for cache iterators"""
def __init__(self, cache):
self.cache = cache
self._entries: List[Value] = []
self._current_index = 0
self._collect_entries()
def __iter__(self):
return self
def __next__(self):
"""Get next cache entry"""
if self._current_index < len(self._entries):
entry = self._entries[self._current_index]
self._current_index += 1
return entry
else:
raise StopIteration
def __len__(self):
return len(self._entries)
def __str__(self):
return f"{self.__class__.__name__}(cache={self.cache.name}, entries={len(self._entries)})"
def _collect_entries(self):
"""To be implemented by subclasses"""
raise NotImplementedError("Subclasses must implement _collect_entries")

View File

@@ -0,0 +1,32 @@
import gdb
import re
from .lv_cache_iter_base import LVCacheIteratorBase
def create_cache_iterator(cache) -> LVCacheIteratorBase | None:
"""Factory function to create appropriate iterator for cache class"""
from .lv_cache_lru_rb import LVCacheLRURB
def get_class(s):
if s == "lv_cache_class_lru_rb_size":
return LVCacheLRURB
elif s == "lv_cache_class_lru_rb_count":
return LVCacheLRURB
return None
try:
if cache.clz:
symbol = cache.clz.format_string(symbols=True, address=False)
m = re.match("<(.*)>", symbol)
if m:
symbol = m.group(1)
cache_class = get_class(symbol)
if cache_class:
return iter(cache_class(cache))
except Exception:
raise TypeError(f"Unsupported cache type {cache}")

View File

@@ -0,0 +1,104 @@
from typing import Union, List, Optional
import gdb
from lvglgdb.value import Value
from .lv_cache_iter_base import LVCacheIteratorBase
from .lv_rb import LVRedBlackTree
from .lv_cache_entry import LVCacheEntry
from .lv_cache import LVCache
class LVCacheLRURBIterator(LVCacheIteratorBase):
"""Iterator for LRU RB cache implementation - traverses linked list and red-black tree"""
def __init__(self, cache):
super().__init__(cache)
def _collect_entries(self):
"""Collect entries from LRU RB cache by traversing the linked list"""
try:
# Cast cache to lv_lru_rb_t_ to access internal structures
lru_cache = self.cache.cast("lv_lru_rb_t_", ptr=True)
if not lru_cache:
return
# Access the linked list
rb = lru_cache.rb
if not rb or not rb.root:
return
rb = LVRedBlackTree(rb)
for node in rb:
self._entries.append(
LVCacheEntry.from_data_ptr(node, self.cache.datatype)
)
except Exception as e:
print(f"Error in _collect_lru_entries: {e}")
import traceback
traceback.print_exc()
class LVCacheLRURB(LVCache):
"""LVGL LRU-based cache using red-black tree iterator"""
def __init__(self, cache: Union[Value, gdb.Value, int]):
# Convert to Value first if needed
if isinstance(cache, int):
cache = Value(cache).cast("lv_cache_lru_rb_t", ptr=True)
if cache is None:
raise ValueError("Failed to cast pointer to lv_cache_lru_rb_t")
elif isinstance(cache, gdb.Value) and not isinstance(cache, Value):
cache = Value(cache)
elif not cache:
raise ValueError("Invalid cache")
self.cache_base = cache
super().__init__(cache, cache.datatype)
def print_info(self):
"""Dump LRU RB cache information"""
print(f"LRU RB Cache Info:")
# Try to get cache class info
try:
clz = self.clz
if clz:
print(f" Cache Class: {clz}")
# Check if it's LRU RB based
if "lru_rb" in str(clz).lower():
print(f" Type: LRU with Red-Black Tree")
except:
pass
def is_count_based(self):
"""Check if this is count-based LRU cache"""
try:
name = str(self.name)
return "count" in name.lower() or "lru_rb_count" in str(self.clz).lower()
except:
return False
def is_size_based(self):
"""Check if this is size-based LRU cache"""
try:
name = str(self.name)
return "size" in name.lower() or "lru_rb_size" in str(self.clz).lower()
except:
return False
def __iter__(self):
"""Create iterator for this LRU RB cache"""
return LVCacheLRURBIterator(self)
def items(self):
"""Get all cache entries as a list"""
entries = []
for entry in self:
entries.append(entry)
return entries
def dump_lru_rb_cache_info(cache: Union[Value, gdb.Value, int]):
"""Dump LRU RB cache information"""
cache_obj = LVCacheLRURB(cache)
cache_obj.print_info()

View File

@@ -0,0 +1,88 @@
from typing import Union
import gdb
from prettytable import PrettyTable
from lvglgdb.value import Value
from .lv_cache import LVCache
from .lv_cache_entry import LVCacheEntry
class LVImageCache(object):
def __init__(self, cache: Value):
self._cache = LVCache(cache, "lv_image_cache_data_t")
def print_info(self):
self._cache.print_info()
def print_entries(self):
"""Print image cache entries using prettytable format"""
table = PrettyTable()
table.field_names = [
"size",
"data_size",
"cf",
"rc",
"type",
"decoder",
"decoded",
"src",
]
table.align = "r" # Right align all columns by default
table.align["src"] = "l" # Left align source column
table.align["type"] = "c" # Center align type column
for entry in self._cache:
entry: LVCacheEntry
data_ptr = entry.get_data()
if not data_ptr:
continue
decoded = data_ptr.decoded
try:
header = decoded.header
w = int(header.w)
h = int(header.h)
cf = int(header.cf)
data_size = int(decoded.data_size) if decoded else 0
decoded_ptr = decoded.data if decoded else 0
decoder_name = data_ptr.decoder.name.as_string()
src_type = int(data_ptr.src_type)
src = data_ptr.src
ref_cnt = entry.get_ref_count()
size_str = f"{w}x{h}"
if src_type == 0: # LV_IMAGE_SRC_VARIABLE
src_str = src.format_string(
symbols=True, address=True, styling=True
)
type_str = "var"
elif src_type == 1: # LV_IMAGE_SRC_FILE
src_str = (
src.cast("char", ptr=True).as_string() if src else "(null)"
)
type_str = "file"
else: # Unknown type
src_str = f"{int(src):#x}" if src else "0x0"
type_str = "unkn"
table.add_row(
[
size_str,
f"{data_size}",
f"{cf}",
f"{ref_cnt}",
type_str,
decoder_name,
f"{int(decoded_ptr):#x}",
src_str,
]
)
except gdb.error as e:
table.add_row(["ERROR", "", "", "", "", "", "", str(e)])
continue
print(table)

View File

@@ -0,0 +1,74 @@
from typing import Union
import gdb
from prettytable import PrettyTable
from lvglgdb.value import Value
from .lv_cache import LVCache
from .lv_cache_entry import LVCacheEntry
class LVImageHeaderCache(object):
def __init__(self, cache: Value):
self._cache = LVCache(cache, "lv_image_header_cache_data_t")
def print_info(self):
self._cache.print_info()
def print_entries(self):
"""Print image cache entries using prettytable format"""
table = PrettyTable()
table.field_names = ["size", "cf", "rc", "type", "decoder", "src"]
table.align = "r" # Right align all columns by default
table.align["src"] = "l" # Left align source column
table.align["type"] = "c" # Center align type column
for entry in self._cache:
entry: LVCacheEntry
data_ptr = entry.get_data()
if not data_ptr:
continue
try:
header = data_ptr.header
w = int(header.w)
h = int(header.h)
cf = int(header.cf)
src_type = int(data_ptr.src_type)
src = data_ptr.src
decoder_name = data_ptr.decoder.name.as_string()
ref_cnt = entry.get_ref_count()
size_str = f"{w}x{h}"
if src_type == 0: # LV_IMAGE_SRC_VARIABLE
src_str = src.format_string(
symbols=True, address=True, styling=True
)
type_str = "var"
elif src_type == 1: # LV_IMAGE_SRC_FILE
src_str = (
src.cast("char", ptr=True).as_string() if src else "(null)"
)
type_str = "file"
else: # Unknown type
src_str = f"{int(src):#x}" if src else "0x0"
type_str = "unkn"
table.add_row(
[
size_str,
f"{cf}",
f"{ref_cnt}",
type_str,
decoder_name,
src_str,
]
)
except gdb.error as e:
table.add_row(["ERROR", "", "", "", "", str(e)])
continue
print(table)

View File

@@ -0,0 +1,53 @@
from typing import Union
import gdb
from lvglgdb.value import Value
class LVList(Value):
"""LVGL linked list iterator"""
def __init__(self, ll: Value, nodetype: Union[gdb.Type, str] = None):
if not ll:
raise ValueError("Invalid linked list")
super().__init__(ll)
self.nodetype = (
gdb.lookup_type(nodetype).pointer()
if isinstance(nodetype, str)
else nodetype
)
self.lv_ll_node_t = gdb.lookup_type("lv_ll_node_t").pointer()
self.current = self.head
self._next_offset = self.n_size + self.lv_ll_node_t.sizeof
self._prev_offset = self.n_size
def _next(self, node):
next_value = Value(int(node) + self._next_offset)
return next_value.cast(self.lv_ll_node_t, ptr=True).dereference()
def _prev(self, node):
prev_value = Value(int(node) + self._prev_offset)
return prev_value.cast(self.lv_ll_node_t, ptr=True).dereference()
def __iter__(self):
return self
def __next__(self):
if not self.current:
raise StopIteration
nodetype = self.nodetype if self.nodetype else self.lv_ll_node_t
node = self.current.cast(nodetype)
self.current = self._next(self.current)
return node
@property
def len(self):
len = 0
node = self.head
while node:
len += 1
node = self._next(node)
return len

View File

@@ -0,0 +1,183 @@
from typing import Union
import gdb
from lvglgdb.value import Value
class LVRedBlackTree(Value):
"""LVGL red-black tree iterator"""
def __init__(
self, rb: Union[Value, gdb.Value, int], datatype: Union[gdb.Type, str] = None
):
# Convert to Value first if needed
if isinstance(rb, int):
rb = Value(rb).cast("lv_rb_t", ptr=True)
if rb is None:
raise ValueError("Failed to cast pointer to lv_rb_t")
elif isinstance(rb, gdb.Value) and not isinstance(rb, Value):
rb = Value(rb)
elif not rb:
raise ValueError("Invalid red-black tree")
super().__init__(rb)
self.lv_rb_node_t = gdb.lookup_type("lv_rb_node_t").pointer()
self.datatype = (
gdb.lookup_type(datatype).pointer()
if isinstance(datatype, str)
else datatype
)
def minimum_from(self, node):
"""Find minimum node from given node"""
if not node:
return None
current = node
while current.left:
current = current.left
return current
def maximum_from(self, node):
"""Find maximum node from given node"""
if not node:
return None
current = node
while current.right:
current = current.right
return current
def minimum(self):
"""Find minimum node in the tree"""
root = self.root
return self.minimum_from(root)
def maximum(self):
"""Find maximum node in the tree"""
root = self.root
return self.maximum_from(root)
def __iter__(self):
"""Create a new iterator for this tree"""
return LVRedBlackTreeIterator(self)
def __len__(self):
"""Get the number of nodes in the tree"""
count = 0
for _ in self:
count += 1
return count
def get_data(self, node):
"""Get typed data from node, with optional type conversion"""
if not node or not node.data:
return None
data = node.data
if self.datatype:
return data.cast(self.datatype)
return data
def format_data(self, data):
"""Format data for display - simple GDB style"""
if data is None:
return "None"
try:
ptr_addr = f"0x{int(data):x}"
except:
return str(data)
if self.datatype and data:
try:
struct_data = data.dereference()
return f"{ptr_addr} -> {struct_data}"
except:
pass
return ptr_addr
def print_info(self):
"""Dump basic tree information"""
print(f"Red-Black Tree Info:")
print(f" Size: {int(self.size)}")
print(f" Node Count: {len(self)}")
print(f" Root: {self.root}")
if self.root:
root_color = "Red" if int(self.root.color) == 0 else "Black"
print(f" Root Color: {root_color}")
if self.datatype:
print(f" Data Type: {self.datatype}")
def print_tree(self, max_items=10):
"""Print tree data in a readable format"""
print(f"Red-Black Tree Contents ({len(self)} total items):")
count = 0
for i, data in enumerate(self):
if count >= max_items:
print(f" ... showing first {max_items} of {len(self)} items")
break
formatted = self.format_data(data)
print(f" [{i}] {formatted}")
count += 1
if count == 0:
print(" (empty)")
elif count < len(self):
print(f" ... {len(self) - count} more items not shown")
class LVRedBlackTreeIterator:
"""Iterator for LVRedBlackTree that supports multiple traversals"""
def __init__(self, tree: LVRedBlackTree):
self.tree = tree
self.current = tree.minimum()
def __iter__(self):
return self
def __next__(self):
if not self.current:
raise StopIteration
data = self.tree.get_data(self.current)
# Move to next node (in-order traversal)
if self.current.right:
self.current = self.tree.minimum_from(self.current.right)
else:
parent = self.current.parent
while parent and self.current == parent.right:
self.current = parent
parent = parent.parent
self.current = parent
return data
def __str__(self):
"""Better string representation for iterator"""
current = self.current
if not current:
return "LVRedBlackTreeIterator(ended)"
try:
data = self.tree.get_data(current)
if data:
return f"LVRedBlackTreeIterator(current={data})"
except:
pass
return f"LVRedBlackTreeIterator(current=0x{int(current):x})"
def dump_rb_info(
rb: Union[Value, gdb.Value, int], datatype: Union[gdb.Type, str] = None
):
"""Dump red-black tree information"""
tree = LVRedBlackTree(rb, datatype=datatype)
tree.print_info()

View File

@@ -0,0 +1,7 @@
from lvglgdb.value import Value
def dump_style_info(style: Value):
prop = int(style.prop)
value = style.value
print(f"{prop} = {value}")

View File

@@ -0,0 +1,63 @@
import gdb
from typing import Optional, Union
class Value(gdb.Value):
def __init__(self, value: Union[gdb.Value, "Value"]):
super().__init__(value)
def __getitem__(self, key):
try:
value = super().__getitem__(key)
except gdb.error:
value = super().__getattr__(key)
return Value(value)
def __getattr__(self, key):
if hasattr(super(), key):
return Value(super().__getattribute__(key))
return Value(super().__getitem__(key))
def cast(
self, type_name: Union[str, gdb.Type], ptr: bool = False
) -> Optional["Value"]:
try:
gdb_type = (
gdb.lookup_type(type_name) if isinstance(type_name, str) else type_name
)
if ptr:
gdb_type = gdb_type.pointer()
return Value(super().cast(gdb_type))
except gdb.error:
return None
def super_value(self, attr: str) -> "Value":
return self[attr]
def as_string(self):
"""Convert to string if possible"""
try:
return self.string()
except gdb.error:
return str(self)
def __str__(self):
"""Provide better string representation for debugging"""
try:
ptr_val = int(self)
return f"({self.type})0x{ptr_val:x}"
except gdb.error:
pass
return super().__str__()
def __repr__(self):
"""Provide detailed representation"""
try:
content = self.dereference()
return f"Value({self.__str__()}: {content})"
except gdb.error:
pass
return f"Value({self.__str__()})"

View File

@@ -0,0 +1,17 @@
[project]
name = "lvglgdb"
version = "0.1.1"
description = "LVGL GDB scripts"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"numpy~=2.2.6",
"Pillow~=11.3.0",
"prettytable~=3.16.0",
]
[project.urls]
Homepage = "https://lvgl.io"
Documentation = "https://docs.lvgl.io/master/debugging/gdb_plugin.html"
Repository = "https://github.com/lvgl/lvgl"

View File

@@ -0,0 +1,3 @@
numpy~=2.2.6
Pillow~=11.3.0
prettytable~=3.16.0

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,336 @@
import os
import sys
import argparse
import shutil
import tempfile
import json
import subprocess
base_path = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, base_path)
project_dir = os.path.abspath(os.path.join(base_path, '..', '..'))
docs_path = os.path.join(project_dir, 'docs')
sys.path.insert(0, docs_path)
import create_fake_lib_c # NOQA
import pycparser_monkeypatch # NOQA
import pycparser # NOQA
doxyfile_filename = 'Doxyfile'
DEVELOP = False
intermediate_dir = tempfile.mkdtemp(suffix='.lvgl_json')
def run(output_path, lv_conf_file, output_to_stdout, target_header, filter_private, no_docstrings, *compiler_args):
pycparser_monkeypatch.FILTER_PRIVATE = filter_private
lvgl_dir = project_dir
lvgl_src_dir = os.path.join(lvgl_dir, 'src')
int_lvgl_dir = os.path.join(intermediate_dir, 'lvgl')
lv_conf_dest_file = os.path.join(intermediate_dir, 'lv_conf.h')
target_header_base_name = (
os.path.splitext(os.path.split(target_header)[-1])[0]
)
try:
os.mkdir(int_lvgl_dir)
shutil.copytree(lvgl_src_dir, os.path.join(int_lvgl_dir, 'src'))
shutil.copyfile(os.path.join(lvgl_dir, 'lvgl.h'), os.path.join(int_lvgl_dir, 'lvgl.h'))
pp_file = os.path.join(intermediate_dir, target_header_base_name + '.pp')
if lv_conf_file is None:
lv_conf_templ_file = os.path.join(lvgl_dir, 'lv_conf_template.h')
with open(lv_conf_templ_file, 'rb') as f:
lines = f.read().decode('utf-8').split('\n')
for i, line in enumerate(lines):
if line.startswith('#if 0'):
lines[i] = '#if 1'
else:
for item in (
'LV_USE_LOG',
'LV_USE_OBJ_ID',
'LV_USE_OBJ_ID_BUILTIN',
'LV_USE_FLOAT',
'LV_USE_BIDI',
'LV_USE_LODEPNG',
'LV_USE_LIBPNG',
'LV_USE_BMP',
'LV_USE_TJPGD',
'LV_USE_LIBJPEG_TURBO',
'LV_USE_LIBWEBP',
'LV_USE_GIF',
'LV_BIN_DECODER_RAM_LOAD',
'LV_USE_RLE',
'LV_USE_QRCODE',
'LV_USE_BARCODE',
'LV_USE_TINY_TTF',
'LV_USE_GRIDNAV',
'LV_USE_FRAGMENT',
'LV_USE_IMGFONT',
'LV_USE_SNAPSHOT',
'LV_USE_FREETYPE'
):
if line.startswith(f'#define {item} '):
lines[i] = f'#define {item} 1'
break
with open(lv_conf_dest_file, 'wb') as f:
f.write('\n'.join(lines).encode('utf-8'))
else:
shutil.copyfile(lv_conf_file, lv_conf_dest_file)
include_dirs = [intermediate_dir, project_dir]
if sys.platform.startswith('win'):
import get_sdl2
try:
import pyMSVC # NOQA
except ImportError:
sys.stderr.write(
'\nThe pyMSVC library is missing, '
'please run "pip install pyMSVC" to install it.\n'
)
sys.stderr.flush()
sys.exit(-500)
env = pyMSVC.setup_environment() # NOQA
cpp_cmd = ['cl', '/std:c11', '/nologo', '/P']
output_pp = f'/Fi"{pp_file}"'
sdl2_include, _ = get_sdl2.get_sdl2(intermediate_dir)
include_dirs.append(sdl2_include)
include_path_env_key = 'INCLUDE'
elif sys.platform.startswith('darwin'):
include_path_env_key = 'C_INCLUDE_PATH'
cpp_cmd = [
'clang', '-std=c11', '-E', '-DINT32_MIN=0x80000000',
]
output_pp = f' >> "{pp_file}"'
else:
include_path_env_key = 'C_INCLUDE_PATH'
cpp_cmd = [
'gcc', '-std=c11', '-E', '-Wno-incompatible-pointer-types',
]
output_pp = f' >> "{pp_file}"'
fake_libc_path = create_fake_lib_c.run(intermediate_dir)
if include_path_env_key not in os.environ:
os.environ[include_path_env_key] = ''
os.environ[include_path_env_key] = (
f'{fake_libc_path}{os.pathsep}{os.environ[include_path_env_key]}'
)
if 'PATH' not in os.environ:
os.environ['PATH'] = ''
os.environ['PATH'] = (
f'{fake_libc_path}{os.pathsep}{os.environ["PATH"]}'
)
cpp_cmd.extend(compiler_args)
cpp_cmd.extend([
'-DLV_LVGL_H_INCLUDE_SIMPLE',
'-DLV_CONF_INCLUDE_SIMPLE',
'-DLV_USE_DEV_VERSION'
])
cpp_cmd.extend(['-DPYCPARSER', f'"-I{fake_libc_path}"'])
cpp_cmd.extend([f'"-I{item}"' for item in include_dirs])
cpp_cmd.append(f'"{target_header}"')
if sys.platform.startswith('win'):
cpp_cmd.insert(len(cpp_cmd) - 2, output_pp)
else:
cpp_cmd.append(output_pp)
cpp_cmd = ' '.join(cpp_cmd)
p = subprocess.Popen(
cpp_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=os.environ,
shell=True
)
out, err = p.communicate()
exit_code = p.returncode
if not os.path.exists(pp_file):
sys.stdout.write(out.decode('utf-8').strip() + '\n')
sys.stdout.write('EXIT CODE: ' + str(exit_code) + '\n')
sys.stderr.write(err.decode('utf-8').strip() + '\n')
sys.stdout.flush()
sys.stderr.flush()
raise RuntimeError('Unknown Failure')
with open(pp_file, 'r') as f:
pp_data = f.read()
cparser = pycparser.CParser()
ast = cparser.parse(pp_data, target_header)
doxyfile_src_file = os.path.join(docs_path, doxyfile_filename)
ast.setup_docs(no_docstrings, lvgl_src_dir,
intermediate_dir, doxyfile_src_file, output_to_stdout)
if not output_to_stdout and output_path is None:
if not DEVELOP:
shutil.rmtree(intermediate_dir)
return ast
elif output_to_stdout:
# stdout.reset()
print(json.dumps(ast.to_dict(), indent=4))
else:
if not os.path.exists(output_path):
os.makedirs(output_path)
output_path = os.path.join(output_path, target_header_base_name + '.json')
with open(output_path, 'w') as f:
f.write(json.dumps(ast.to_dict(), indent=4))
except Exception as err:
try:
print(cpp_cmd) # NOQA
print()
except: # NOQA
pass
for key, value in os.environ.items():
print(key + ':', value)
print()
import traceback
traceback.print_exc()
print()
exceptions = [
ArithmeticError,
AssertionError,
AttributeError,
EOFError,
FloatingPointError,
GeneratorExit,
ImportError,
IndentationError,
IndexError,
KeyError,
KeyboardInterrupt,
LookupError,
MemoryError,
NameError,
NotImplementedError,
OverflowError,
ReferenceError,
RuntimeError,
StopIteration,
SyntaxError,
TabError,
SystemExit,
TypeError,
UnboundLocalError,
UnicodeError,
UnicodeEncodeError,
UnicodeDecodeError,
UnicodeTranslateError,
ValueError,
ZeroDivisionError,
SystemError
]
if isinstance(err, OSError):
error = err.errno
else:
if type(err) in exceptions:
error = ~exceptions.index(type(err))
else:
error = -100
else:
error = 0
if DEVELOP:
print('temporary file path:', intermediate_dir)
else:
shutil.rmtree(intermediate_dir)
sys.exit(error)
if __name__ == '__main__':
parser = argparse.ArgumentParser('-')
parser.add_argument(
'--output-path',
dest='output_path',
help=(
'output directory for JSON file. If one is not '
'supplied then it will be output stdout'
),
action='store',
default=None
)
parser.add_argument(
'--lvgl-config',
dest='lv_conf',
help=(
'path to lv_conf.h (including file name), if this is not set then '
'a config file will be generated that has everything turned on.'
),
action='store',
default=None
)
parser.add_argument(
'--develop',
dest='develop',
help='this option leaves the temporary folder in place.',
action='store_true',
)
parser.add_argument(
"--target-header",
dest="target_header",
help=(
"path to a custom header file. When using this to supply a custom"
"header file you MUST insure that any LVGL includes are done so "
"they are relitive to the LVGL repository root folder.\n\n"
'#include "src/lvgl_private.h"\n\n'
"If you have includes to header files that are not LVGL then you "
"will need to add the include locations for those header files "
"when running this script. It is done using the same manner that "
"is used when calling a C compiler\n\n"
"You need to provide the absolute path to the header file when "
"using this feature."
),
action="store",
default=os.path.join(intermediate_dir, "lvgl", "lvgl.h")
)
parser.add_argument(
'--filter-private',
dest='filter_private',
help='Internal Use',
action='store_true',
)
parser.add_argument(
'--no-docstrings',
dest='no_docstrings',
help='Internal Use',
action='store_true',
)
args, extra_args = parser.parse_known_args()
DEVELOP = args.develop
run(args.output_path, args.lv_conf, args.output_path is None, args.target_header, args.filter_private, args.no_docstrings, *extra_args)

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
import zipfile
import io
import os
SDL2_URL = 'https://github.com/libsdl-org/SDL/releases/download/release-2.26.5/SDL2-devel-2.26.5-VC.zip' # NOQA
def get_path(name: str, p: str) -> str:
for file in os.listdir(p):
file = os.path.join(p, file)
if file.endswith(name):
return file
if os.path.isdir(file):
if res := get_path(name, file):
return res
def get_sdl2(path, url=SDL2_URL):
import requests # NOQA
stream = io.BytesIO()
with requests.get(url, stream=True) as r:
r.raise_for_status()
content_length = int(r.headers['Content-Length'])
chunks = 0
# print()
# sys.stdout.write('\r' + str(chunks) + '/' + str(content_length))
# sys.stdout.flush()
for chunk in r.iter_content(chunk_size=1024):
stream.write(chunk)
chunks += len(chunk)
# sys.stdout.write('\r' + str(chunks) + '/' + str(content_length))
# sys.stdout.flush()
# print()
stream.seek(0)
zf = zipfile.ZipFile(stream)
for z_item in zf.infolist():
for ext in ('.h', '.dll', '.lib'):
if not z_item.filename.endswith(ext):
continue
zf.extract(z_item, path=path)
break
include_path = get_path('include', path)
lib_path = get_path('lib\\x64', path)
dll_path = get_path('SDL2.dll', lib_path)
sdl_include_path = os.path.split(include_path)[0]
if not os.path.exists(os.path.join(sdl_include_path, 'SDL2')):
os.rename(include_path, os.path.join(sdl_include_path, 'SDL2'))
zf.close()
stream.close()
return os.path.abspath(sdl_include_path), dll_path

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
pycparser>=2.22
pyMSVC>=0.5.3; platform_system == "Windows"
Sphinx
breathe
imagesize
importlib-metadata
sphinx-rtd-theme
sphinx-sitemap
sphinxcontrib-applehelp
sphinxcontrib-devhelp
sphinxcontrib-htmlhelp
sphinxcontrib-jsmath
sphinxcontrib-qthelp
sphinxcontrib-serializinghtml
sphinx-rtd-dark-mode
typing-extensions

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
#
# Generate the cmake variables CONFIG_LV_USE_* or CONFIG_LV_BUILD_* from the
# preprocessed lv_conf_internal.h
#
# Author: David TRUAN (david.truan@edgemtech.ch)
# Author: Erik Tagirov (erik.tagirov@edgemtech.ch)
#
import os
import argparse
import re
def fatal(msg):
print()
print("ERROR! " + msg)
exit(1)
def get_args():
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=""
"Convert the expanded lv_conf_internal.h to cmake variables."
"It converts all LV_USE_*, LV_BUILD_* configurations."
)
parser.add_argument('--input', type=str, required=True, nargs='?',
help='Path of the macro expanded lv_conf_internal.h, which should be generated during a cmake build')
parser.add_argument('--output', type=str, required=True, nargs='?',
help='Path of the output file, where the cmake variables declaration will be written (ex: build/lv_conf.cmake)')
parser.add_argument("--kconfig", action="store_true", help="Enable kconfig flag")
parser.add_argument("--debug", action="store_true", required=False, help="Show unhandled expressions")
parser.add_argument("--parentscope", action="store_true", required=False, help="Additionally set the variables in the parent scope")
args = parser.parse_args()
# The input must exist
if not os.path.exists(args.input):
fatal(f"Input {args.input} not found")
return args
def write_set_cmd(fout, expr, is_parent_scope):
fout.write(f'set({expr})\n')
# This makes the variable usable from the top level directory
if is_parent_scope == True:
fout.write(f'set({expr} PARENT_SCOPE)\n')
def generate_cmake_variables(path_input: str, path_output: str, kconfig: bool, debug: bool, is_parent_scope: bool):
fin = open(path_input)
fout = open(path_output, "w", newline='')
# If we use Kconfig, we must check the CONFIG_LV_USE_* and
# CONFIG_LV_BUILD_* defines
if kconfig:
CONFIG_PATTERN="^#define +(CONFIG_LV_USE|CONFIG_LV_BUILD|CONFIG_LV_[0-9A-Z_]+_USE)"
CONFIG_PREFIX=""
# Otherwise check the LV_USE_* and LV_BUILD_* defines
else:
CONFIG_PATTERN="^#define +(LV_USE|LV_BUILD|LV_[0-9A-Z_]+_USE)"
CONFIG_PREFIX="CONFIG_"
# Using the expanded lv_conf_internal, we don't have to deal with regexp,
# as all the #define will be aligned on the left with a single space before the value
for line in fin.read().splitlines():
# Treat the LV_USE_STDLIB_* configs in a special way, as we need
# to convert the define to full config with 1 value when enabled
if re.search(f'{CONFIG_PATTERN}_STDLIB', line):
parts = line.split()
if len(parts) < 3:
continue
name = parts[1]
value = parts[2].strip()
type = value.split("LV_STDLIB_")[1]
name = name.replace("STDLIB", type)
write_set_cmd(fout, f'{CONFIG_PREFIX}{name} 1', is_parent_scope)
# Treat the LV_USE_OS config in a special way, as we need
# to convert the define to full config with 1 value when enabled
if re.search(f'{CONFIG_PATTERN}_OS', line):
parts = line.split()
if len(parts) < 3:
continue
name = parts[1]
value = parts[2].strip()
type = value.split("LV_OS")[1]
name += type
write_set_cmd(fout, f'{CONFIG_PREFIX}{name} 1', is_parent_scope)
# For the rest of the configs, simply add CONFIG_ and write the name of the define
# all LV_USE_* or LV_BUILD_* configs where the value is 0 or 1,
# as these are the ones that are needed in cmake
# To detect the configuration of LVGL to perform conditional compilation/linking
elif re.search(f'{CONFIG_PATTERN}.* +[01] *$', line):
parts = line.split()
if len(parts) < 3:
continue
name = parts[1]
value = parts[2].strip()
write_set_cmd(fout, f'{CONFIG_PREFIX}{name} {value}', is_parent_scope)
else:
# Useful for debugging expressions that are unhandled,
# if the script fails in 'unexpected ways'
if debug == True:
print(f"DBG: Skipping expression: '{line} - not handled'")
if __name__ == '__main__':
args = get_args()
generate_cmake_variables(args.input, args.output, args.kconfig, args.debug, args.parentscope)

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
import os
import re
import argparse
from pathlib import Path
DIR_SCRIPTS = os.path.dirname(__file__)
REPO_ROOT = os.path.join(DIR_SCRIPTS, "..")
DIR_CWD = os.getcwd()
def fatal(msg):
print()
print("ERROR! " + msg)
exit(1)
def get_args():
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=""
"Update the configuration file of your existing project based on default parameters and latest LVGL template. Eg.:\n"
" python3 generate_lv_conf.py ./src \n"
" python3 generate_lv_conf.py --template modules/lvgl/lv_conf_template.h ./my_config_folder"
)
parser.add_argument('--template', type=str, default=REPO_ROOT, nargs='?',
help='Path of "lv_conf_template.h" file or the folder containing it\n(optional, 1 folder above the python script by default)')
parser.add_argument('--config', type=str, default=None, nargs='?',
help='Path of "lv_conf.h" file (optional)')
parser.add_argument('--defaults', type=str, default=None, nargs='?',
help='Path of "lv_conf.defaults" file (optional)')
parser.add_argument('target', metavar='target', type=str, default=DIR_CWD, nargs='?',
help='Folder containing "lv_conf.h" and "lv_conf.defaults" files\n(optional, current work folder by default)')
args = parser.parse_args()
if os.path.isdir(args.template):
args.template = os.path.join(args.template, "lv_conf_template.h")
if not args.config:
args.config = os.path.join(args.target, "lv_conf.h")
if not args.defaults:
args.defaults = os.path.join(args.target, "lv_conf.defaults")
if not os.path.exists(args.template):
fatal(f"Template file not found at {args.template}")
if not os.path.exists(args.config):
print(f"{args.config} not found. Generating it")
Path(args.config).touch()
if not os.path.exists(args.defaults):
fatal(f"User defaults not found at {args.defaults}")
return args
def parse_defaults(path: str):
defaults = {}
with open(path, 'r', encoding='utf-8') as file:
for line in file.readlines():
if len(line.strip()) == 0 or line.startswith('#'):
continue
search_result = re.search(r'([A-Z0-9_]+)\s+(.+)', line)
if not search_result:
print(f"WARNING: Ignoring line {line}")
continue
groups = search_result.groups()
defaults[groups[0]] = groups[1]
return defaults
def generate_config(path_destination: str, path_source: str, defaults: dict):
with open(path_source, 'r', encoding='utf-8') as f_src:
src_lines = f_src.readlines()
keys_used = set()
dst_lines = []
for src_line in src_lines:
res = re.search(r'#define\s+([A-Z0-9_]+)', src_line)
key = res.groups()[0] if res else None
if key in defaults.keys():
value = defaults[key]
pattern = r'(#define\s+[A-Z0-9_]+[^\S\r\n]*)[^\n]*'
has_space = re.search(pattern, src_line).groups()[0][-1].isspace()
repl = r'\g<1>' + value if has_space else r'\g<1> ' + value
dst_line, _ = re.subn(pattern, repl, src_line)
print(f"Applying: {key} = {value}")
keys_used.add(key)
elif 'Set this to "1" to enable content' in src_line:
dst_line = '#if 1 /* Enable content */'
else:
dst_line = src_line
dst_lines.append(dst_line)
if len(keys_used) != len(defaults):
unused_keys = [k for k in defaults.keys() if k not in keys_used]
print('WARNING: The following keys are deprecated:\n ' + '\n '.join(unused_keys))
with open(path_destination, 'w', encoding='utf-8') as f_dst:
for dst_line in dst_lines:
f_dst.write(dst_line)
if __name__ == '__main__':
args = get_args()
print("Template:", args.template)
print("User config:", args.config)
print("User defaults:", args.defaults)
defaults = parse_defaults(args.defaults)
print(f"Loaded {len(defaults)} defaults")
generate_config(args.config, args.template, defaults)
print()
print('New config successfully generated!')

View File

@@ -0,0 +1,15 @@
#!/bin/bash
echo "/* Autogenerated */"
echo '#include <stddef.h>'
echo '#include "examplelist.h"'
TMPFILE=$(mktemp)
find examples demos -name \*.h | xargs grep -hE "^void lv_(example|demo)" | sed 's/(/ /g' | awk '{print $2}' > $TMPFILE
cat $TMPFILE | while read -r line; do
echo "extern void ${line}(void);"
done
echo "const struct lv_ci_example lv_ci_example_list[] = {"
cat $TMPFILE | while read -r line; do
echo " { \"$line\", $line },";
done
echo " { NULL, NULL }"
echo "};"

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
from LVGLImage import LVGLImage
import argparse
import logging
import os
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser(description="LVGL Binary Image Viewer")
parser.add_argument("file", help="the .bin image file")
args = parser.parse_args()
name, ext = os.path.splitext(args.file)
if ext != ".bin":
raise ValueError("Only support LVGL .bin image file")
output = name + ".png"
img = LVGLImage().from_bin(args.file)
img.to_png(output)
logging.info(f"convert {args.file} to {output}")
if os.name == "posix":
os.system(f"open {output}")
else:
try:
from PIL import Image
except ImportError:
raise ImportError("Need pillow package, do `pip3 install pillow`")
image = Image.open(output)
image.show(title=output)

View File

@@ -0,0 +1,10 @@
#!/bin/sh
# https://github.com/facebook/infer
#
# Install:
# VERSION=0.17.0; \
# curl -sSL "https://github.com/facebook/infer/releases/download/v$VERSION/infer-linux64-v$VERSION.tar.xz" \
# | sudo tar -C /opt -xJ && \
# sudoln -s "/opt/infer-linux64-v$VERSION/bin/infer" /usr/local/bin/infer
infer run -- make -j8

View File

@@ -0,0 +1,4 @@
vcpkg install vcpkg-tool-ninja libpng freetype opengl glfw3
if %errorlevel% neq 0 exit /b %errorlevel%
pip install pypng lz4 kconfiglib
if %errorlevel% neq 0 exit /b %errorlevel%

View File

@@ -0,0 +1,15 @@
#!/bin/sh
# Install Linux package prerequisites needed for LVGL development
# and testing. Some less-common development packages are not included
# here, such as MicroPython and PC simulator packages.
#
# Note: This script is run by the CI workflows.
SCRIPT_PATH=$(readlink -f $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
sudo dpkg --add-architecture i386
sudo apt update
cat $SCRIPT_DIR/prerequisites-apt.txt | xargs sudo apt install -y
pip3 install --user -r $SCRIPT_DIR/prerequisites-pip.txt

View File

@@ -0,0 +1,16 @@
#!/bin/sh
# Check if the script is being run as root
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root or with sudo" >&2
exit 1
fi
rm -rf astyle
git clone https://github.com/lvgl/astyle.git
cd astyle/build/gcc
git checkout v3.4.12
make -j
make install
cd ../../..
rm -rf astyle

View File

@@ -0,0 +1,11 @@
#!/bin/sh
rm -rf pngquant
git clone https://github.com/kornelski/pngquant
cd pngquant
git checkout 2.17.0
./configure
make -j
sudo make install
cd ..
rm -rf pngquant

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env python3
##################################################################
# sjpeg converter script version 1.0
# Dependencies: (PYTHON-3)
##################################################################
SJPG_FILE_FORMAT_VERSION = "V1.00" #
JPEG_SPLIT_HEIGHT = 16
##################################################################
import math, os, sys, time
from PIL import Image
OUTPUT_FILE_NAME = ""
INPUT_FILE = ""
if len(sys.argv) == 2:
INPUT_FILE = sys.argv[1]
OUTPUT_FILE_NAME = INPUT_FILE.split("/")[-1].split("\\")[-1].split(".")[0]
else:
print("usage:\n\t python " + sys.argv[0] + " input_file.jpg")
sys.exit(0)
try:
im = Image.open(INPUT_FILE)
except:
print("\nFile not found!")
sys.exit(0)
print("\nConversion started...\n")
start_time = time.time()
width, height = im.size
print("Input:")
print("\t" + INPUT_FILE)
print("\tRES = " + str(width) + " x " + str(height) + '\n')
lenbuf = []
block_size = JPEG_SPLIT_HEIGHT;
splits = math.ceil(height/block_size)
c_code = '''//LVGL SJPG C ARRAY\n#include "lvgl/lvgl.h"\n\nconst uint8_t ''' + OUTPUT_FILE_NAME + '''_map[] = {\n'''
sjpeg_data = bytearray()
sjpeg = bytearray()
row_remaining = height;
for i in range(splits):
if row_remaining < block_size:
crop = im.crop((0, i*block_size, width, row_remaining + i*block_size))
else:
crop = im.crop((0, i*block_size, width, block_size + i*block_size))
row_remaining = row_remaining - block_size;
crop.save(str(i)+".jpg", quality=90)
for i in range(splits):
f = open(str(i)+".jpg", "rb")
a = f.read()
f.close()
sjpeg_data = sjpeg_data + a
lenbuf.append(len(a))
header = bytearray()
#4 BYTES
header = header + bytearray("_SJPG__".encode("UTF-8"));
#6 BYTES VERSION
header = header + bytearray(("\x00" + SJPG_FILE_FORMAT_VERSION + "\x00").encode("UTF-8"));
#WIDTH 2 BYTES
header = header + width.to_bytes(2, byteorder='little');
#HEIGHT 2 BYTES
header = header + height.to_bytes(2, byteorder='little');
#NUMBER OF ITEMS 2 BYTES
header = header + splits.to_bytes(2, byteorder='little');
#NUMBER OF ITEMS 2 BYTES
header = header + int(JPEG_SPLIT_HEIGHT).to_bytes(2, byteorder='little');
for item_len in lenbuf:
# WIDTH 2 BYTES
header = header + item_len.to_bytes(2, byteorder='little');
data = bytearray()
sjpeg = header + sjpeg_data;
if 1:
for i in range(len(lenbuf)):
os.remove(str(i) + ".jpg")
f = open(OUTPUT_FILE_NAME+".sjpg","wb");
f.write(sjpeg)
f.close()
new_line_threshold = 0
for i in range(len(sjpeg)):
c_code = c_code + "\t" + str(hex(sjpeg[i])) + ","
new_line_threshold = new_line_threshold + 1
if (new_line_threshold >= 16):
c_code = c_code + "\n"
new_line_threshold = 0
c_code = c_code + "\n};\n\nlv_image_dsc_t "
c_code = c_code + OUTPUT_FILE_NAME + " = {\n"
c_code = c_code + "\t.header.always_zero = 0,\n"
c_code = c_code + "\t.header.w = " + str(width) + ",\n"
c_code = c_code + "\t.header.h = " + str(height) + ",\n"
c_code = c_code + "\t.data_size = " + str(len(sjpeg)) + ",\n"
c_code = c_code + "\t.header.cf = LV_IMG_CF_RAW,\n"
c_code = c_code + "\t.data = " + OUTPUT_FILE_NAME+"_map" + ",\n};"
f = open(OUTPUT_FILE_NAME + '.c', 'w')
f.write(c_code)
f.close()
time_taken = (time.time() - start_time)
print("Output:")
print("\tTime taken = " + str(round(time_taken,2)) + " sec")
print("\tbin size = " + str(round(len(sjpeg)/1024, 1)) + " KB" )
print("\t" + OUTPUT_FILE_NAME + ".sjpg\t(bin file)" + "\n\t" + OUTPUT_FILE_NAME + ".c\t\t(c array)")
print("\nAll good!")

View File

@@ -0,0 +1,346 @@
#!/usr/bin/env python3
# Originally modified from:
# https://github.com/zephyrproject-rtos/zephyr/blob/main/scripts/kconfig/kconfig.py
# SPDX-License-Identifier: ISC
# Writes/updates the lvgl/.config configuration file by merging configuration
# files passed as arguments
#
# When fragments haven't changed, lvgl/.config is both the input and the
# output, which just updates it. This is handled in the CMake files.
#
# Also does various checks (most via Kconfiglib warnings).
import argparse
import os
import re
import sys
import textwrap
# Lvgl doesn't use tristate symbols. They're supported here just to make the
# script a bit more generic.
from kconfiglib import (
Kconfig,
split_expr,
expr_value,
expr_str,
BOOL,
TRISTATE,
TRI_TO_STR,
AND,
OR,
)
def main():
print(sys.argv)
args = parse_args()
print("Parsing " + args.kconfig_file)
kconf = Kconfig(args.kconfig_file, warn_to_stderr=False, suppress_traceback=True)
if args.handwritten_input_configs:
# Warn for assignments to undefined symbols, but only for handwritten
# fragments, to avoid warnings-turned-errors when using an old
# configuration file together with updated Kconfig files
kconf.warn_assign_undef = True
# prj.conf may override settings from the board configuration, so
# disable warnings about symbols being assigned more than once
kconf.warn_assign_override = False
kconf.warn_assign_redun = False
if args.forced_input_configs:
# Do not warn on a redundant config.
# The reason is that a regular .config will be followed by the forced
# config which under normal circumstances should be identical to the
# configured setting.
# Only if user has modified to a value that gets overruled by the forced
# a warning shall be issued.
kconf.warn_assign_redun = False
# Load files
print(kconf.load_config(args.configs_in[0]))
for config in args.configs_in[1:]:
# replace=False creates a merged configuration
print(kconf.load_config(config, replace=False))
if args.handwritten_input_configs:
# Check that there are no assignments to promptless symbols, which
# have no effect.
#
# This only makes sense when loading handwritten fragments and not when
# loading lvgl/.config, because lvgl/.config is configuration
# output and also assigns promptless symbols.
check_no_promptless_assign(kconf)
# Print warnings for symbols that didn't get the assigned value. Only
# do this for handwritten input too, to avoid likely unhelpful warnings
# when using an old configuration and updating Kconfig files.
check_assigned_sym_values(kconf)
check_assigned_choice_values(kconf)
if kconf.syms.get("WARN_DEPRECATED", kconf.y).tri_value == 2:
check_deprecated(kconf)
if kconf.syms.get("WARN_EXPERIMENTAL", kconf.y).tri_value == 2:
check_experimental(kconf)
# Hack: Force all symbols to be evaluated, to catch warnings generated
# during evaluation. Wait till the end to write the actual output files, so
# that we don't generate any output if there are warnings-turned-errors.
#
# Kconfiglib caches calculated symbol values internally, so this is still
# fast.
kconf.write_config(os.devnull)
warn_only = r"warning:.*set more than once."
if kconf.warnings:
if args.forced_input_configs:
error_out = False
else:
error_out = True
# Put a blank line between warnings to make them easier to read
for warning in kconf.warnings:
print("\n" + warning, file=sys.stderr)
if not error_out and not re.search(warn_only, warning):
# The warning is not a warn_only, fail the Kconfig.
error_out = True
# Turn all warnings into errors, so that e.g. assignments to undefined
# Kconfig symbols become errors.
#
# A warning is generated by this script whenever a symbol gets a
# different value than the one it was assigned. Keep that one as just a
# warning for now.
if error_out:
err("Aborting due to Kconfig warnings")
# Write the merged configuration and the C header
print(kconf.write_config(args.config_out))
print(kconf.write_autoconf(args.header_out))
# Write the list of parsed Kconfig files to a file
write_kconfig_filenames(kconf, args.kconfig_list_out)
def check_no_promptless_assign(kconf):
# Checks that no promptless symbols are assigned
for sym in kconf.unique_defined_syms:
if sym.user_value is not None and promptless(sym):
err(
f"""\
{sym.name_and_loc} is assigned in a configuration file, but is not directly
user-configurable (has no prompt). It gets its value indirectly from other
symbols. """
+ SYM_INFO_HINT.format(sym)
)
def check_assigned_sym_values(kconf):
# Verifies that the values assigned to symbols "took" (matches the value
# the symbols actually got), printing warnings otherwise. Choice symbols
# are checked separately, in check_assigned_choice_values().
for sym in kconf.unique_defined_syms:
if sym.choice:
continue
user_value = sym.user_value
if user_value is None:
continue
# Tristate values are represented as 0, 1, 2. Having them as "n", "m",
# "y" is more convenient here, so convert.
if sym.type in (BOOL, TRISTATE):
user_value = TRI_TO_STR[user_value]
if user_value != sym.str_value:
msg = (
f"{sym.name_and_loc} was assigned the value '{user_value}'"
f" but got the value '{sym.str_value}'. "
)
# List any unsatisfied 'depends on' dependencies in the warning
mdeps = missing_deps(sym)
if mdeps:
expr_strs = []
for expr in mdeps:
estr = expr_str(expr)
if isinstance(expr, tuple):
# Add () around dependencies that aren't plain symbols.
# Gives '(FOO || BAR) (=n)' instead of
# 'FOO || BAR (=n)', which might be clearer.
estr = f"({estr})"
expr_strs.append(f"{estr} " f"(={TRI_TO_STR[expr_value(expr)]})")
msg += (
"Check these unsatisfied dependencies: "
+ ", ".join(expr_strs)
+ ". "
)
warn(msg + SYM_INFO_HINT.format(sym))
def missing_deps(sym):
# check_assigned_sym_values() helper for finding unsatisfied dependencies.
#
# Given direct dependencies
#
# depends on <expr> && <expr> && ... && <expr>
#
# on 'sym' (which can also come from e.g. a surrounding 'if'), returns a
# list of all <expr>s with a value less than the value 'sym' was assigned
# ("less" instead of "not equal" just to be general and handle tristates,
# even though lvgl doesn't use them).
#
# For string/int/hex symbols, just looks for <expr> = n.
#
# Note that <expr>s can be something more complicated than just a symbol,
# like 'FOO || BAR' or 'FOO = "string"'.
deps = split_expr(sym.direct_dep, AND)
if sym.type in (BOOL, TRISTATE):
return [dep for dep in deps if expr_value(dep) < sym.user_value]
# string/int/hex
return [dep for dep in deps if expr_value(dep) == 0]
def check_assigned_choice_values(kconf):
# Verifies that any choice symbols that were selected (by setting them to
# y) ended up as the selection, printing warnings otherwise.
#
# We check choice symbols separately to avoid warnings when two different
# choice symbols within the same choice are set to y. This might happen if
# a choice selection from a board defconfig is overridden in a prj.conf,
# for example. The last choice symbol set to y becomes the selection (and
# all other choice symbols get the value n).
#
# Without special-casing choices, we'd detect that the first symbol set to
# y ended up as n, and print a spurious warning.
for choice in kconf.unique_choices:
if choice.user_selection and choice.user_selection is not choice.selection:
warn(
f"""\
The choice symbol {choice.user_selection.name_and_loc} was selected (set =y),
but {choice.selection.name_and_loc if choice.selection else "no symbol"} ended
up as the choice selection. """
+ SYM_INFO_HINT.format(choice.user_selection)
)
# Hint on where to find symbol information. Used like
# SYM_INFO_HINT.format(sym).
SYM_INFO_HINT = """\
See https://docs.lvgl.io/master/integration/overview/configuration.html
look up {0.name} in the menuconfig/guiconfig interface. The Application
Development Primer, Setting Configuration Values, and Kconfig - Tips and Best
Practices sections of the manual might be helpful too.\
"""
def check_deprecated(kconf):
deprecated = kconf.syms.get("DEPRECATED")
dep_expr = kconf.n if deprecated is None else deprecated.rev_dep
if dep_expr is not kconf.n:
selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2]
for selector in selectors:
selector_name = split_expr(selector, AND)[0].name
warn(f"Deprecated symbol {selector_name} is enabled.")
def check_experimental(kconf):
experimental = kconf.syms.get("EXPERIMENTAL")
dep_expr = kconf.n if experimental is None else experimental.rev_dep
if dep_expr is not kconf.n:
selectors = [s for s in split_expr(dep_expr, OR) if expr_value(s) == 2]
for selector in selectors:
selector_name = split_expr(selector, AND)[0].name
warn(f"Experimental symbol {selector_name} is enabled.")
def promptless(sym):
# Returns True if 'sym' has no prompt. Since the symbol might be defined in
# multiple locations, we need to check all locations.
return not any(node.prompt for node in sym.nodes)
def write_kconfig_filenames(kconf, kconfig_list_path):
# Writes a sorted list with the absolute paths of all parsed Kconfig files
# to 'kconfig_list_path'. The paths are realpath()'d, and duplicates are
# removed. This file is used by CMake to look for changed Kconfig files. It
# needs to be deterministic.
with open(kconfig_list_path, "w") as out:
for path in sorted(
{
os.path.realpath(os.path.join(kconf.srctree, path))
for path in kconf.kconfig_filenames
}
):
print(path, file=out)
def parse_args():
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument(
"--handwritten-input-configs",
action="store_true",
help="Assume the input configuration fragments are "
"handwritten fragments and do additional checks "
"on them, like no promptless symbols being "
"assigned",
)
parser.add_argument(
"--forced-input-configs",
action="store_true",
help="Indicate the input configuration files are "
"followed by an forced configuration file."
"The forced configuration is used to forcefully "
"set specific configuration settings to a "
"pre-defined value and thereby remove any user "
" adjustments.",
)
parser.add_argument("kconfig_file", help="Top-level Kconfig file")
parser.add_argument("config_out", help="Output configuration file")
parser.add_argument("header_out", help="Output header file")
parser.add_argument(
"kconfig_list_out", help="Output file for list of parsed Kconfig files"
)
parser.add_argument(
"configs_in",
nargs="+",
help="Input configuration fragments. Will be merged " "together.",
)
return parser.parse_args()
def warn(msg):
# Use a large fill() width to try to avoid linebreaks in the symbol
# reference link, and add some extra newlines to set the message off from
# surrounding text (this usually gets printed as part of spammy CMake
# output)
print("\n" + textwrap.fill("warning: " + msg, 100) + "\n", file=sys.stderr)
def err(msg):
sys.exit("\n" + textwrap.fill("error: " + msg, 100) + "\n")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
import sys
try:
import kconfiglib
except ImportError:
print("Need kconfiglib package, do `pip3 install kconfiglib`")
sys.exit(1)
def verify_kconfig(kconfig_file):
kconf = kconfiglib.Kconfig(kconfig_file)
if kconf.warnings:
print("Warnings found:")
for warning in kconf.warnings:
print(warning)
sys.exit(1)
else:
print("No warnings found.")
def check_kconfig_spaces(file_path):
"""Check for space-based indentation in a Kconfig file"""
try:
# Read file content to check for spaces
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
space_indent_lines = []
for line_num, line in enumerate(lines, 1):
# Check for leading spaces (skip empty lines)
stripped_line = line.lstrip()
if stripped_line and len(line) > len(stripped_line):
# Extract the indentation part
indent = line[: -len(stripped_line)] if stripped_line else line
if " " in indent:
space_indent_lines.append((line_num, indent))
if space_indent_lines:
print(f"Space-based indentation found in file {file_path}:")
for line_num, indent in space_indent_lines:
print(f"Line {line_num}: Indent contains {indent.count(' ')} spaces")
sys.exit(1)
else:
print(f"No space-based indentation found in file {file_path}")
except Exception as e:
print(f"Error processing file {file_path}: {e}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <Kconfig_file>")
sys.exit(1)
verify_kconfig(sys.argv[1])
check_kconfig_spaces(sys.argv[1])

View File

@@ -0,0 +1,349 @@
#!/usr/bin/env python3
'''
Generates lv_conf_internal.h from lv_conf_template.h to provide default values
'''
import os
import sys
import re
SCRIPT_DIR = os.path.dirname(__file__)
LV_CONF_TEMPLATE = os.path.join(SCRIPT_DIR, "..", "lv_conf_template.h")
LV_CONF_INTERNAL = os.path.join(SCRIPT_DIR, "..", "src", "lv_conf_internal.h")
if sys.version_info < (3,6,0):
print("Python >=3.6 is required", file=sys.stderr)
exit(1)
def check_for_tabs(file_path):
errors = []
with open(file_path, 'r') as file:
for line_number, line in enumerate(file, 1):
if '\t' in line:
errors.append(f" {file_path}:{line_number}")
if errors:
print(f"Tabs found in the following files:", file=sys.stderr)
for error in errors:
print(error, file=sys.stderr)
print("Please replace tabs with spaces.", file=sys.stderr)
exit(1)
check_for_tabs(LV_CONF_TEMPLATE)
fin = open(LV_CONF_TEMPLATE)
fout = open(LV_CONF_INTERNAL, "w", newline='')
fout.write(
'''/**
* GENERATED FILE, DO NOT EDIT IT!
* @file lv_conf_internal.h
* This file ensures all defines of lv_conf.h have a default value.
*/
#ifndef LV_CONF_INTERNAL_H
#define LV_CONF_INTERNAL_H
/* clang-format off */
/* Config options */
#define LV_OS_NONE 0
#define LV_OS_PTHREAD 1
#define LV_OS_FREERTOS 2
#define LV_OS_CMSIS_RTOS2 3
#define LV_OS_RTTHREAD 4
#define LV_OS_WINDOWS 5
#define LV_OS_MQX 6
#define LV_OS_SDL2 7
#define LV_OS_CUSTOM 255
#define LV_STDLIB_BUILTIN 0
#define LV_STDLIB_CLIB 1
#define LV_STDLIB_MICROPYTHON 2
#define LV_STDLIB_RTTHREAD 3
#define LV_STDLIB_CUSTOM 255
#define LV_DRAW_SW_ASM_NONE 0
#define LV_DRAW_SW_ASM_NEON 1
#define LV_DRAW_SW_ASM_HELIUM 2
#define LV_DRAW_SW_ASM_RISCV_V 3
#define LV_DRAW_SW_ASM_CUSTOM 255
#define LV_NEMA_LIB_NONE 0
#define LV_NEMA_LIB_M33_REVC 1
#define LV_NEMA_LIB_M33_NEMAPVG 2
#define LV_NEMA_LIB_M55 3
#define LV_NEMA_LIB_M7 4
#define LV_NEMA_HAL_CUSTOM 0
#define LV_NEMA_HAL_STM32 1
#define LV_NANOVG_BACKEND_GL2 1
#define LV_NANOVG_BACKEND_GL3 2
#define LV_NANOVG_BACKEND_GLES2 3
#define LV_NANOVG_BACKEND_GLES3 4
/** Handle special Kconfig options. */
#ifndef LV_KCONFIG_IGNORE
#include "lv_conf_kconfig.h"
#if defined(CONFIG_LV_CONF_SKIP) && !defined(LV_CONF_SKIP)
#define LV_CONF_SKIP
#endif
#endif
/* If "lv_conf.h" is available from here try to use it later. */
#ifdef __has_include
#if __has_include("lv_conf.h")
#ifndef LV_CONF_INCLUDE_SIMPLE
#define LV_CONF_INCLUDE_SIMPLE
#endif
#endif
#endif
/* If lv_conf.h is not skipped, include it. */
#if !defined(LV_CONF_SKIP) || defined(LV_CONF_PATH)
#ifdef LV_CONF_PATH /* If there is a path defined for lv_conf.h, use it */
#include LV_CONF_PATH /* Note: Make sure to define custom CONF_PATH as a string */
#elif defined(LV_CONF_INCLUDE_SIMPLE) /* Or simply include lv_conf.h is enabled. */
#include "lv_conf.h"
#else
#include "../../lv_conf.h" /* Else assume lv_conf.h is next to the lvgl folder. */
#endif
#if !defined(LV_CONF_H) && !defined(LV_CONF_SUPPRESS_DEFINE_CHECK)
/* #include will sometimes silently fail when __has_include is used */
/* https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80753 */
#pragma message("Possible failure to include lv_conf.h, please read the comment in this file if you get errors")
#endif
#endif
#ifdef CONFIG_LV_COLOR_DEPTH
#define LV_KCONFIG_PRESENT
#endif
/*----------------------------------
* Start parsing lv_conf_template.h
-----------------------------------*/
'''
)
started = 0
for line in fin.read().splitlines():
if not started:
if '#define LV_CONF_H' in line:
started = 1
continue
else:
continue
if '/*--END OF LV_CONF_H--*/' in line: break
#Is there a #define in this line?
r = re.search(r'^([\s]*)#[\s]*(undef|define)[\s]+([^\s]+).*$', line) # \s means any white space character
if r:
indent = r[1]
name = r[3]
name = re.sub(r'\(.*?\)', '', name, count=1) #remove parentheses from macros. E.g. MY_FUNC(5) -> MY_FUNC
line = re.sub(r'[\s]*', '', line, count=1)
#If the value should be 1 (enabled) by default use a more complex structure for Kconfig checks because
#if a not defined CONFIG_... value should be interpreted as 0 and not the LVGL default
is_one = re.search(r'#[\s]*define[\s]*[A-Z0-9_]+[\s]+1([\s]*$|[\s]+)', line)
if is_one:
#1. Use the value if already set from lv_conf.h or anything else (i.e. do nothing)
#2. In Kconfig environment use the CONFIG_... value if set, else use 0
#3. In not Kconfig environment use the LVGL's default value
fout.write(
f'{indent}#ifndef {name}\n'
f'{indent} #ifdef LV_KCONFIG_PRESENT\n'
f'{indent} #ifdef CONFIG_{name.upper()}\n'
f'{indent} #define {name} CONFIG_{name.upper()}\n'
f'{indent} #else\n'
f'{indent} #define {name} 0\n'
f'{indent} #endif\n'
f'{indent} #else\n'
f'{indent} {line}\n'
f'{indent} #endif\n'
f'{indent}#endif\n'
)
else:
#1. Use the value if already set from lv_conf.h or anything else (i.e. do nothing)
#2. Use the Kconfig value if set
#3. Use the LVGL's default value
fout.write(
f'{indent}#ifndef {name}\n'
f'{indent} #ifdef CONFIG_{name.upper()}\n'
f'{indent} #define {name} CONFIG_{name.upper()}\n'
f'{indent} #else\n'
f'{indent} {line}\n'
f'{indent} #endif\n'
f'{indent}#endif\n'
)
elif re.search('^ *typedef .*;.*$', line):
continue #ignore typedefs to avoid redeclaration
else:
fout.write(f'{line}\n')
fout.write(
r'''
/*----------------------------------
* End of parsing lv_conf_template.h
-----------------------------------*/
/*Fix inconsistent name*/
#define LV_USE_ANIMIMAGE LV_USE_ANIMIMG
#ifndef __ASSEMBLY__
LV_EXPORT_CONST_INT(LV_DPI_DEF);
LV_EXPORT_CONST_INT(LV_DRAW_BUF_STRIDE_ALIGN);
LV_EXPORT_CONST_INT(LV_DRAW_BUF_ALIGN);
#endif
#undef LV_KCONFIG_PRESENT
/* Disable VGLite drivers if VGLite drawing is disabled */
#ifndef LV_USE_VG_LITE_DRIVER
#define LV_USE_VG_LITE_DRIVER 0
#endif
#ifndef LV_USE_VG_LITE_THORVG
#define LV_USE_VG_LITE_THORVG 0
#endif
/* Set some defines if a dependency is disabled. */
#if LV_USE_LOG == 0
#define LV_LOG_LEVEL LV_LOG_LEVEL_NONE
#define LV_LOG_TRACE_MEM 0
#define LV_LOG_TRACE_TIMER 0
#define LV_LOG_TRACE_INDEV 0
#define LV_LOG_TRACE_DISP_REFR 0
#define LV_LOG_TRACE_EVENT 0
#define LV_LOG_TRACE_OBJ_CREATE 0
#define LV_LOG_TRACE_LAYOUT 0
#define LV_LOG_TRACE_ANIM 0
#endif /*LV_USE_LOG*/
#if LV_USE_WAYLAND
/*Automatically detect wayland backend*/
#if LV_USE_OPENGLES
#define LV_WAYLAND_USE_EGL 1
#define LV_WAYLAND_USE_G2D 0
#define LV_WAYLAND_USE_SHM 0
#elif LV_USE_G2D
#define LV_WAYLAND_USE_EGL 0
#define LV_WAYLAND_USE_G2D 1
#define LV_WAYLAND_USE_SHM 0
#else
#define LV_WAYLAND_USE_EGL 0
#define LV_WAYLAND_USE_G2D 0
#define LV_WAYLAND_USE_SHM 1
#endif
#else
#define LV_WAYLAND_USE_G2D 0
#define LV_WAYLAND_USE_SHM 0
#define LV_WAYLAND_USE_EGL 0
#endif
#if LV_USE_LINUX_DRM
#if LV_USE_OPENGLES
#define LV_LINUX_DRM_USE_EGL 1
#else
#define LV_LINUX_DRM_USE_EGL 0
#endif /* LV_USE_OPENGLES */
#else
#define LV_LINUX_DRM_USE_EGL 0
#endif /*LV_USE_LINUX_DRM*/
#if LV_USE_SYSMON == 0
#define LV_USE_PERF_MONITOR 0
#define LV_USE_MEM_MONITOR 0
#define LV_SYSMON_PROC_IDLE_AVAILABLE 0
#endif /*LV_USE_SYSMON*/
#if LV_USE_PERF_MONITOR == 0
#define LV_USE_PERF_MONITOR_LOG_MODE 0
#endif /*LV_USE_PERF_MONITOR*/
#if LV_BUILD_DEMOS == 0
#define LV_USE_DEMO_WIDGETS 0
#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0
#define LV_USE_DEMO_BENCHMARK 0
#define LV_USE_DEMO_RENDER 0
#define LV_USE_DEMO_STRESS 0
#define LV_USE_DEMO_MUSIC 0
#define LV_USE_DEMO_VECTOR_GRAPHIC 0
#define LV_USE_DEMO_FLEX_LAYOUT 0
#define LV_USE_DEMO_MULTILANG 0
#define LV_USE_DEMO_EBIKE 0
#define LV_USE_DEMO_HIGH_RES 0
#define LV_USE_DEMO_SMARTWATCH 0
#define LV_USE_DEMO_GLTF 0
#endif /* LV_BUILD_DEMOS */
#ifndef LV_USE_LZ4
#if (LV_USE_LZ4_INTERNAL || LV_USE_LZ4_EXTERNAL)
#define LV_USE_LZ4 1
#else
#define LV_USE_LZ4 0
#endif
#endif
#ifndef LV_USE_THORVG
#if (LV_USE_THORVG_INTERNAL || LV_USE_THORVG_EXTERNAL)
#define LV_USE_THORVG 1
#else
#define LV_USE_THORVG 0
#endif
#endif
#if LV_USE_SDL && LV_USE_OPENGLES && (LV_USE_DRAW_OPENGLES || LV_USE_DRAW_NANOVG)
#define LV_SDL_USE_EGL 1
#else
#define LV_SDL_USE_EGL 0
#endif
#ifndef LV_USE_EGL
#if LV_LINUX_DRM_USE_EGL || LV_WAYLAND_USE_EGL || LV_SDL_USE_EGL
#define LV_USE_EGL 1
#else
#define LV_USE_EGL 0
#endif
#endif /* LV_USE_EGL */
#if LV_USE_OS
#if (LV_USE_FREETYPE || LV_USE_THORVG) && LV_DRAW_THREAD_STACK_SIZE < (32 * 1024)
#error "Increase LV_DRAW_THREAD_STACK_SIZE to at least 32KB for FreeType or ThorVG."
#endif
#if defined(LV_DRAW_THREAD_STACKSIZE) && !defined(LV_DRAW_THREAD_STACK_SIZE)
#warning "LV_DRAW_THREAD_STACKSIZE was renamed to LV_DRAW_THREAD_STACK_SIZE. Please update lv_conf.h or run menuconfig again."
#define LV_DRAW_THREAD_STACK_SIZE LV_DRAW_THREAD_STACKSIZE
#endif
#endif
/*Allow only upper case letters and '/' ('/' is a special case for backward compatibility)*/
#define LV_FS_IS_VALID_LETTER(l) ((l) == '/' || ((l) >= 'A' && (l) <= 'Z'))
/* If running without lv_conf.h, add typedefs with default value. */
#ifdef LV_CONF_SKIP
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) /*Disable warnings for Visual Studio*/
#define _CRT_SECURE_NO_WARNINGS
#endif
#endif /*defined(LV_CONF_SKIP)*/
#endif /*LV_CONF_INTERNAL_H*/
'''
)
fin.close()
fout.close()
check_for_tabs(LV_CONF_INTERNAL)

View File

@@ -0,0 +1,190 @@
import re
import argparse
import os
import traceback
ignored_file_prefixes = {
"lv_conf_template.h",
"lv_conf_cmsis.h",
"lv_test_perf_conf.h",
"lv_font_"
}
ignored_top_dirs = {
".git",
"docs"
}
h_filename_pattern = re.compile(r"lv_[\w.]*\.h")
h_pattern = re.compile(
r"\s*/\*\*.+?"
r"@file +(\S*).+?"
r"\*/\s+?#ifndef +(\w+_H)\s+#define +(\w+_H).+"
r"#endif */\* *([^*]+?) *\*/\s*",
flags=re.DOTALL
)
c_filename_pattern = re.compile(r"lv_[\w.]*\.c")
c_pattern = re.compile(
r"\s*/\*\*.+?"
r"@file +(\S*)",
flags=re.DOTALL
)
file_comment_help_f = """\
'{0}' should have at the top:
/**
* @file {1}
*
*/
instead, has:
/**
* @file {2}
*
*/
"""
guard_help_f = """\
'{0}' should have include guards like:
#ifndef {1}
#define {1}
...
#endif /*{1}*/
instead, has:
#ifndef {2}
#define {3}
...
#endif /*{4}*/
"""
# regex, arg parsing and other stuff skipped here.
def ignore_file(basename):
result = False
for pattern in ignored_file_prefixes:
if basename.startswith(pattern):
result = True
break
return result
def ignore_dir(path):
result = False
for top_dir in ignored_top_dirs:
sub_path = os.path.join('lvgl', top_dir)
if sub_path in path:
result = True
break
return result
def debug(*args_, **kwargs):
if args.verbose:
print(*args_, **kwargs)
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--fix", action="store_true", help="fix incorrect files")
arg_parser.add_argument("-q", "--quiet", action="store_true", help="don't print mismatch info")
arg_parser.add_argument("-v", "--verbose", action="store_true", help="print debug info")
args = arg_parser.parse_args()
cfg_project_dir = '..'
base_dir = os.path.abspath(os.path.dirname(__file__))
project_dir = os.path.abspath(os.path.join(base_dir, cfg_project_dir))
debug(f'ignore dirs [{ignored_top_dirs}]')
ok = True
for root, dirs, basenames in os.walk(project_dir):
if ignore_dir(root):
continue
debug(f'root [{root}]')
for basename in basenames:
if ignore_file(basename):
continue
file_ok = True
a_file_was_processed = False
path = os.path.join(root, basename)
# -------------------------------------------------------------
# H File?
# -------------------------------------------------------------
if h_filename_pattern.fullmatch(basename):
a_file_was_processed = True
debug(f'Opening H file [{path}]')
try:
with open(path, 'rb') as f:
cont = f.read().decode('utf-8')
except:
print(f'Error attempting UTF-8 decode on [{path}].')
traceback.print_exc()
continue
m = h_pattern.fullmatch(cont)
if m is not None:
debug(' Match.')
if m[1] != basename:
file_ok = False
if not args.quiet:
print(file_comment_help_f.format(path, basename, m[1]))
guard_name = f"LV_{basename[3:-2].upper()}_H"
if any(m[i] != guard_name for i in range(2, 4+1)):
file_ok = False
if not args.quiet:
print(guard_help_f.format(path, guard_name, m[2], m[3], m[4]))
replacements = (basename, guard_name, guard_name, guard_name)
else:
debug(f' No match with regex [{h_pattern.pattern}].')
# -------------------------------------------------------------
# C File?
# -------------------------------------------------------------
elif c_filename_pattern.fullmatch(basename):
a_file_was_processed = True
debug(f'Opening C file [{path}]')
try:
with open(path, 'rb') as f:
cont = f.read().decode('utf-8')
except:
print(f'Error attempting UTF-8 decode on [{path}].')
traceback.print_exc()
continue
m = c_pattern.match(cont)
if m is not None:
debug(' Match.')
if m[1] != basename:
file_ok = False
if not args.quiet:
print(file_comment_help_f.format(path, basename, m[1]))
replacements = (basename, )
else:
debug(f' No match with regex [{c_pattern.pattern}].')
if a_file_was_processed:
if not file_ok:
debug(f'NOT OK [{path}]')
ok = False
if args.fix:
for i in reversed(range(0, len(replacements))):
span = m.regs[i + 1]
repl = replacements[i]
cont = cont[:span[0]] + repl + cont[span[1]:]
with open(path, "w") as f:
f.write(cont)
else:
debug(f'OK [{path}]')
if not ok:
exit(1)

View File

@@ -0,0 +1,237 @@
"""
This script takes json and mpk input files and creates a PR comment in markdown format.
An mpk (msgpack) file contains the benchmark result history for a specific config.
Unpacked, this file will have the following format:
```json
[
{
"commit_hash": "<commit_hash>"
"scenes": [
{
"scene_name": "",
"avg_cpu": 0,
"avg_fps": 0,
"avg_time": 0,
"render_time": 0,
"flush_time": 0,
},
...
]
},
...
]
```
A json file contains the benchmark result for a specific commit:
```json
[
{
"scene_name": "",
"avg_cpu": 0,
"avg_fps": 0,
"avg_time": 0,
"render_time": 0,
"flush_time": 0,
}
...
]
```
This script reads the previous and new benchmark results, compares them and creates a comment.
Example comment:
```
Hi :wave:, thank you for your PR!
We've run benchmarks in an emulated environment. Here are the results:
#### ARM Emulated 32b - lv_conf_perf32b
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| All scenes avg. | 20 | 24 | 7 | 7 | 0 |
<details>
<summary>
Detailed Results Per Scene
</summary>
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| Empty screen | 11 | 25 | 0 | 0 | 0 |
| Moving wallpaper | 1 | 25 | 0 | 0 | 0 |
| Single rectangle | 0 | 25 | 0 | 0 | 0 |
| Multiple rectangles | 0 | 25 | 0 | 0 | 0 |
| Multiple RGB images | 0 | 25 | 0 | 0 | 0 |
| Multiple ARGB images | 22 (-1)| 25 | 1 | 1 | 0 |
| Rotated ARGB images | 47 (-1)| 24 | 20 | 20 | 0 |
| Multiple labels | 2 (-2)| 25 | 0 | 0 | 0 |
| Screen sized text | 30 (+1)| 24 (-1)| 11 (-1)| 11 (-1)| 0 |
| Multiple arcs | 19 (+4)| 24 | 7 | 7 | 0 |
| Containers | 1 (-1)| 25 | 0 | 0 | 0 |
| Containers with overlay | 87 (-2)| 21 | 44 | 44 | 0 |
| Containers with opa | 23 (+1)| 25 | 4 | 4 | 0 |
| Containers with opa_layer | 22 (+1)| 25 | 8 | 8 | 0 |
| Containers with scrolling | 25 | 25 | 10 | 10 | 0 |
| Widgets demo | 34 | 24 (-1)| 13 | 13 | 0 |
| All scenes avg. | 20 | 24 | 7 | 7 | 0 |
</details>
Disclaimer: These benchmarks were run in an emulated environment using QEMU with instruction counting mode.
The timing values represent relative performance metrics within this specific virtualized setup and should
not be interpreted as absolute real-world performance measurements. Values are deterministic and useful for
comparing different LVGL features and configurations, but may not correlate directly with performance on
physical hardware. The measurements are intended for comparative analysis only.
---
:robot: This comment was automatically generated by a bot.
```
"""
import argparse
import json
import os
import msgpack
DISCLAIMER = """
Disclaimer: These benchmarks were run in an emulated environment using QEMU with instruction counting mode.
The timing values represent relative performance metrics within this specific virtualized setup and should
not be interpreted as absolute real-world performance measurements. Values are deterministic and useful for
comparing different LVGL features and configurations, but may not correlate directly with performance on
physical hardware. The measurements are intended for comparative analysis only.
"""
def format_table(results: list[dict], prev_results: list[dict]):
table = "| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |\n"
table += "|------------|------------|---------|--------------|----------------|--------------|\n"
data_keys = ["avg_cpu", "avg_fps", "avg_time", "render_time", "flush_time"]
for scene_results, prev_scene_results in zip(results, prev_results):
delta_p = {
key: ((scene_results[key] - prev_scene_results[key])) for key in data_keys
}
table += f"| {scene_results['scene_name']} |"
for key in data_keys:
if delta_p[key] == 0:
table += f" {scene_results[key]} |"
else:
table += f" {scene_results[key]} ({delta_p[key]:+})|"
table += "\n"
return table
def main():
parser = argparse.ArgumentParser(
description="Process previous and new results, and output a comment file."
)
parser.add_argument(
"--previous",
type=str,
nargs="+",
required=False,
help="Path to the previous results file (supports multiple, e.g., results*.mpk)",
)
parser.add_argument(
"--new",
type=str,
nargs="+",
required=True,
help="Paths to new results files (supports multiple, e.g., results*.json)",
)
parser.add_argument(
"-o",
"--output",
type=str,
required=True,
help="Output file path (e.g., comment.md)",
)
args = parser.parse_args()
previous_results_paths = args.previous
results_paths = args.new
output_path = args.output
previous_results_map: dict[str, list[dict]] = {}
if previous_results_paths:
for results_path in previous_results_paths:
_, image_type, config = results_path.replace(".mpk", "").split("-")
with open(results_path, "rb") as f:
previousb = f.read()
rs: list = msgpack.unpackb(previousb)
# We store the filename so it's easier to match with the related results
previous_results_map[os.path.basename(results_path)] = rs
new_results: dict[str, list[dict]] = {}
for results_path in results_paths:
with open(results_path, "r") as f:
r: list[dict] = json.load(f)
# We store the filename so it's easier to match with the related results
new_results[os.path.basename(results_path)] = r
comment = "Hi :wave:, thank you for your PR!\n\n"
comment += "We've run benchmarks in an emulated environment."
comment += " Here are the results:\n\n"
for result_path, result in new_results.items():
mpk_path = result_path.replace(".json", ".mpk")
new_all_scene_avg = [
scene for scene in result if scene["scene_name"] == "All scenes avg."
]
prev_results = previous_results_map.get(mpk_path, [])
if len(prev_results) > 0:
prev_scenes = prev_results[-1]["scenes"]
prev_all_scene_avg = [
[
scene
for scene in prev_scenes
if scene["scene_name"] == "All scenes avg."
][0]
]
prev_results = prev_scenes
else:
# If there are no previous results, we use the current result as
# the previous aswell
# In this case, the difference will always be zero and we won't
# add any new information to the result table
prev_results = result
prev_all_scene_avg = new_all_scene_avg
_, image_type, config = result_path.replace(".json", "").split("-")
comment += f"#### ARM Emulated {image_type} - {config}\n\n"
comment += format_table(new_all_scene_avg, prev_all_scene_avg)
comment += "\n<details>"
comment += "\n<summary>"
comment += "\nDetailed Results Per Scene"
comment += "\n</summary>\n\n"
comment += format_table(result, prev_results)
comment += "\n\n</details>\n\n"
comment += DISCLAIMER
comment += "\n\n"
comment += "---"
comment += "\n\n"
comment += ":robot: This comment was automatically generated by a bot."
with open(output_path, "w") as f:
f.write(comment)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,101 @@
"""
This script parses a docker logs file for the lv_benchmark summary results
The input looks something like:
```
{"log":"SO3: starting the initial process ...\r\n","stream":"stdout","time":"2025-03-14T20:32:17.712134853Z"}
{"log":"\r\n","stream":"stdout","time":"2025-03-14T20:32:17.712146014Z"}
{"log":"\r\n","stream":"stdout","time":"2025-03-14T20:32:17.712150624Z"}
{"log":"Starting LVGL Benchmark\r\n","stream":"stdout","time":"2025-03-14T20:32:17.816299617Z"}
{"log":"Warning: The guest is now late by 0.0 to 1.0 seconds\r\n","stream":"stdout","time":"2025-03-14T20:32:19.129030996Z"}
{"log":"Warning: The guest is now late by 1.0 to 2.0 seconds\r\n","stream":"stdout","time":"2025-03-14T20:32:40.699491404Z"}
{"log":"Warning: The guest is now late by 2.0 to 3.0 seconds\r\n","stream":"stdout","time":"2025-03-14T20:33:02.966355127Z"}
{"log":"Warning: The guest is now late by 3.0 to 4.0 seconds\r\n","stream":"stdout","time":"2025-03-14T20:33:30.06251689Z"}
{"log":"Warning: The guest is now late by 4.0 to 5.0 seconds\r\n","stream":"stdout","time":"2025-03-14T20:33:38.253535255Z"}
{"log":"Warning: The guest is now late by 5.0 to 6.0 seconds\r\n","stream":"stdout","time":"2025-03-14T20:33:51.010250882Z"}
{"log":"Benchmark Summary (9.3.0 dev)\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.855785347Z"}
{"log":"Name, Avg. CPU, Avg. FPS, Avg. time, render time, flush time\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.855997881Z"}
{"log":"Empty screen, 11%, 25, 0, 0, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.857828373Z"}
{"log":"Moving wallpaper, 2%, 25, 0, 0, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.858455924Z"}
{"log":"Single rectangle, 0%, 25, 0, 0, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.859125276Z"}
{"log":"Multiple rectangles, 0%, 25, 0, 0, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.859847899Z"}
{"log":"Multiple RGB images, 0%, 25, 0, 0, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.860658884Z"}
{"log":"Multiple ARGB images, 23%, 25, 1, 1, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.86156999Z"}
{"log":"Rotated ARGB images, 48%, 24, 20, 20, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.862511197Z"}
{"log":"Multiple labels, 3%, 25, 0, 0, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.863549175Z"}
{"log":"Screen sized text, 30%, 24, 11, 11, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.864642275Z"}
{"log":"Multiple arcs, 18%, 24, 7, 7, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.865801475Z"}
{"log":"Containers, 3%, 25, 0, 0, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.867039657Z"}
{"log":"Containers with overlay, 88%, 21, 44, 44, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.868379231Z"}
{"log":"Containers with opa, 10%, 24, 3, 3, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.869736765Z"}
{"log":"Containers with opa_layer, 22%, 24, 8, 8, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.871269383Z"}
{"log":"Containers with scrolling, 25%, 25, 10, 10, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.872899952Z"}
{"log":"Widgets demo, 34%, 25, 13, 13, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.874447759Z"}
{"log":"All scenes avg.,19%, 24, 7, 7, 0\r\r\n","stream":"stdout","time":"2025-03-14T20:33:57.875031879Z"}
{"log":"LVGL Benchmark Over\r\n","stream":"stdout","time":"2025-03-14T20:33:57.933458479Z"}
````
Outpus a json file with the format:
```json
[
{
"scene_name": "",
"avg_cpu": 0,
"avg_fps": 0,
"avg_time": 0,
"render_time": 0,
"flush_time": 0,
}
...
]
```
"""
import sys
import json
def main():
if len(sys.argv) < 2:
print("Missing output path")
return
output_path = sys.argv[1]
found_start_of_results = False
found_header = False
results = []
for line in sys.stdin:
if "Benchmark Summary" in line:
found_start_of_results = True
continue
if not found_start_of_results:
continue
fmt_line: str = json.loads(line)["log"].strip()
cols = fmt_line.split(",")
if len(cols) < 6:
if fmt_line != "LVGL Benchmark Over":
print(f"Warning found invalid log line '{fmt_line}'. Skipping...")
continue
if not found_header:
found_header = True
continue
results.append(
{
"scene_name": cols[0],
"avg_cpu": int(cols[1].replace("%", "")),
"avg_fps": int(cols[2]),
"avg_time": int(cols[3]),
"render_time": int(cols[4]),
"flush_time": int(cols[5]),
}
)
with open(output_path, "w") as f:
json.dump(results, f)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,9 @@
#!/bin/sh
# This script installs extra dependencies for the lvperf Docker images used in the CI pipeline.
# The images are based on Alpine Linux and support runtime installation of dependencies,
# allowing you to extend functionality without rebuilding the image.
#
# For guidance on how dependencies are typically added, refer to the Dockerfiles:
# - https://github.com/smartobjectoriented/so3/blob/main/docker/Dockerfile.lvperf_32b
# - https://github.com/smartobjectoriented/so3/blob/main/docker/Dockerfile.lvperf_64b

View File

@@ -0,0 +1,151 @@
"""
This script takes json and mpk input files and combines them
An mpk (msgpack) file contains the benchmark result history for a specific config.
Unpacked, this file will have the following format:
```json
[
{
"commit_hash": "<commit_hash>"
"scenes": [
{
"scene_name": "",
"avg_cpu": 0,
"avg_fps": 0,
"avg_time": 0,
"render_time": 0,
"flush_time": 0,
}
...
]
},
...
]
```
A json file contains the benchmark result for a specific commit:
```json
[
{
"scene_name": "",
"avg_cpu": 0,
"avg_fps": 0,
"avg_time": 0,
"render_time": 0,
"flush_time": 0,
}
...
]
```
This script combines the json files with the mpk files and outputs new mpk files
The files are expected to follow the format 'results-<image_type>-<config_name>.[mpk|json]'
There are 3 possibilities when handling these files
1. The pair (`file1.mpk` `file1.json`) exists: Normal operation, we have a history file and some new results.
In this case we take the information inside the `file1.json` and append it to the `file1.mpk` before exporting it
2. `file1.json` exists but `file1.mpk` doesn't: Happens when a new config and/or image type are added and there's no history yet
In this case we create a new `file1.mpk` with a single history entry.
3. `file1.mpk` exists but `file1.json` doesn't: Happens when an old config isn't tested anymore for any reason.
In this case we simpyl copy the old `file1.mpk`
"""
import argparse
import os
import shutil
import msgpack
import json
def save_mpk(mpk_path, mpk_data):
with open(mpk_path, "wb") as f:
f.write(mpk_data)
def generate_new_mpk(json_path, output_path, commit_hash):
with open(json_path, "r") as f:
json_data: list = json.load(f)
mpk_data = msgpack.packb([{"commit_hash": commit_hash, "scenes": json_data}])
save_mpk(output_path, mpk_data)
def append_json_to_mpk(mpk_path, json_path, output_path, commit_hash):
with open(mpk_path, "rb") as f:
mpk_data: list = msgpack.unpackb(f.read())
with open(json_path, "r") as f:
json_data = json.load(f)
mpk_data.append({"commit_hash": commit_hash, "scenes": json_data})
save_mpk(output_path, msgpack.packb(mpk_data))
def path(folder, filename):
return os.path.join(folder, filename)
def main():
parser = argparse.ArgumentParser(
description="Merge new results into previous results"
)
parser.add_argument("--commit-hash", type=str, required=True, help="Commit Hash")
parser.add_argument(
"--input",
type=str,
required=True,
help="Input Folder",
)
parser.add_argument(
"--output",
type=str,
required=True,
help="Output Folder",
)
args = parser.parse_args()
input_folder: str = args.input
output_folder: str = args.output
commit_hash: str = args.commit_hash
input_files = os.listdir(input_folder)
input_mpk: set[str] = {file for file in input_files if ".mpk" in file}
input_json: list[str] = [file for file in input_files if ".json" in file]
for json_file in input_json:
mpk_file = json_file.replace(".json", ".mpk")
json_path = path(input_folder, json_file)
output_path = path(output_folder, mpk_file)
if mpk_file not in input_mpk:
# First time this config is being run
print(
f"Couldn't find pair for {json_file} - ({mpk_file} not found). Generating new mpk file"
)
generate_new_mpk(json_path, output_path, commit_hash)
else:
print(f"Found pair ({mpk_file} {json_file}). Combining")
mpk_path = path(input_folder, mpk_file)
append_json_to_mpk(mpk_path, json_path, output_path, commit_hash)
input_mpk.remove(mpk_file)
# Keep old mpk files
for mpk_file in input_mpk:
print(f"Couldn't find new reults to add to {mpk_file}. Copying it")
mpk_path = path(input_folder, mpk_file)
output_path = path(output_folder, mpk_file)
shutil.copy2(mpk_path, output_path)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
Hi :wave:, thank you for your PR!
We've run benchmarks in an emulated environment. Here are the results:
#### ARM Emulated 32b - lv_conf_perf32b
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| All scenes avg. | 20 | 24 | 7 | 7 | 0 |
<details>
<summary>
Detailed Results Per Scene
</summary>
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| Empty screen | 11 | 25 | 0 | 0 | 0 |
| Moving wallpaper | 1 | 25 | 0 | 0 | 0 |
| Single rectangle | 0 | 25 | 0 | 0 | 0 |
| Multiple rectangles | 0 | 25 | 0 | 0 | 0 |
| Multiple RGB images | 0 | 25 | 0 | 0 | 0 |
| Multiple ARGB images | 22 | 25 | 1 | 1 | 0 |
| Rotated ARGB images | 47 | 24 | 20 | 20 | 0 |
| Multiple labels | 2 | 25 | 0 | 0 | 0 |
| Screen sized text | 30 | 24 | 11 | 11 | 0 |
| Multiple arcs | 19 | 24 | 7 | 7 | 0 |
| Containers | 1 | 25 | 0 | 0 | 0 |
| Containers with overlay | 87 | 21 | 44 | 44 | 0 |
| Containers with opa | 23 | 25 | 4 | 4 | 0 |
| Containers with opa_layer | 22 | 25 | 8 | 8 | 0 |
| Containers with scrolling | 25 | 25 | 10 | 10 | 0 |
| Widgets demo | 34 | 24 | 13 | 13 | 0 |
| All scenes avg. | 20 | 24 | 7 | 7 | 0 |
</details>
#### ARM Emulated 64b - lv_conf_perf64b
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| All scenes avg. | 16 | 24 | 6 | 6 | 0 |
<details>
<summary>
Detailed Results Per Scene
</summary>
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| Empty screen | 11 | 25 | 0 | 0 | 0 |
| Moving wallpaper | 0 | 25 | 0 | 0 | 0 |
| Single rectangle | 0 | 25 | 0 | 0 | 0 |
| Multiple rectangles | 0 | 25 | 0 | 0 | 0 |
| Multiple RGB images | 0 | 25 | 0 | 0 | 0 |
| Multiple ARGB images | 1 | 25 | 0 | 0 | 0 |
| Rotated ARGB images | 23 | 25 | 10 | 10 | 0 |
| Multiple labels | 3 | 24 | 0 | 0 | 0 |
| Screen sized text | 23 | 24 | 10 | 10 | 0 |
| Multiple arcs | 15 | 24 | 6 | 6 | 0 |
| Containers | 3 | 25 | 0 | 0 | 0 |
| Containers with overlay | 90 | 23 | 41 | 41 | 0 |
| Containers with opa | 19 | 25 | 4 | 4 | 0 |
| Containers with opa_layer | 13 | 24 | 5 | 5 | 0 |
| Containers with scrolling | 25 | 25 | 10 | 10 | 0 |
| Widgets demo | 33 | 25 | 13 | 13 | 0 |
| All scenes avg. | 16 | 24 | 6 | 6 | 0 |
</details>
Disclaimer: These benchmarks were run in an emulated environment using QEMU with instruction counting mode.
The timing values represent relative performance metrics within this specific virtualized setup and should
not be interpreted as absolute real-world performance measurements. Values are deterministic and useful for
comparing different LVGL features and configurations, but may not correlate directly with performance on
physical hardware. The measurements are intended for comparative analysis only.
---
:robot: This comment was automatically generated by a bot.

View File

@@ -0,0 +1 @@
[{"scene_name": "Empty screen", "avg_cpu": 11, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Moving wallpaper", "avg_cpu": 1, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Single rectangle", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple rectangles", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple RGB images", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple ARGB images", "avg_cpu": 22, "avg_fps": 25, "avg_time": 1, "render_time": 1, "flush_time": 0}, {"scene_name": "Rotated ARGB images", "avg_cpu": 47, "avg_fps": 24, "avg_time": 20, "render_time": 20, "flush_time": 0}, {"scene_name": "Multiple labels", "avg_cpu": 2, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Screen sized text", "avg_cpu": 30, "avg_fps": 24, "avg_time": 11, "render_time": 11, "flush_time": 0}, {"scene_name": "Multiple arcs", "avg_cpu": 19, "avg_fps": 24, "avg_time": 7, "render_time": 7, "flush_time": 0}, {"scene_name": "Containers", "avg_cpu": 1, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Containers with overlay", "avg_cpu": 87, "avg_fps": 21, "avg_time": 44, "render_time": 44, "flush_time": 0}, {"scene_name": "Containers with opa", "avg_cpu": 23, "avg_fps": 25, "avg_time": 4, "render_time": 4, "flush_time": 0}, {"scene_name": "Containers with opa_layer", "avg_cpu": 22, "avg_fps": 25, "avg_time": 8, "render_time": 8, "flush_time": 0}, {"scene_name": "Containers with scrolling", "avg_cpu": 25, "avg_fps": 25, "avg_time": 10, "render_time": 10, "flush_time": 0}, {"scene_name": "Widgets demo", "avg_cpu": 34, "avg_fps": 24, "avg_time": 13, "render_time": 13, "flush_time": 0}, {"scene_name": "All scenes avg.", "avg_cpu": 20, "avg_fps": 24, "avg_time": 7, "render_time": 7, "flush_time": 0}]

View File

@@ -0,0 +1 @@
[{"scene_name": "Empty screen", "avg_cpu": 11, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Moving wallpaper", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Single rectangle", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple rectangles", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple RGB images", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple ARGB images", "avg_cpu": 1, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Rotated ARGB images", "avg_cpu": 23, "avg_fps": 25, "avg_time": 10, "render_time": 10, "flush_time": 0}, {"scene_name": "Multiple labels", "avg_cpu": 3, "avg_fps": 24, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Screen sized text", "avg_cpu": 23, "avg_fps": 24, "avg_time": 10, "render_time": 10, "flush_time": 0}, {"scene_name": "Multiple arcs", "avg_cpu": 15, "avg_fps": 24, "avg_time": 6, "render_time": 6, "flush_time": 0}, {"scene_name": "Containers", "avg_cpu": 3, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Containers with overlay", "avg_cpu": 90, "avg_fps": 23, "avg_time": 41, "render_time": 41, "flush_time": 0}, {"scene_name": "Containers with opa", "avg_cpu": 19, "avg_fps": 25, "avg_time": 4, "render_time": 4, "flush_time": 0}, {"scene_name": "Containers with opa_layer", "avg_cpu": 13, "avg_fps": 24, "avg_time": 5, "render_time": 5, "flush_time": 0}, {"scene_name": "Containers with scrolling", "avg_cpu": 25, "avg_fps": 25, "avg_time": 10, "render_time": 10, "flush_time": 0}, {"scene_name": "Widgets demo", "avg_cpu": 33, "avg_fps": 25, "avg_time": 13, "render_time": 13, "flush_time": 0}, {"scene_name": "All scenes avg.", "avg_cpu": 16, "avg_fps": 24, "avg_time": 6, "render_time": 6, "flush_time": 0}]

View File

@@ -0,0 +1,83 @@
Hi :wave:, thank you for your PR!
We've run benchmarks in an emulated environment. Here are the results:
#### ARM Emulated 32b - lv_conf_perf32b
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| All scenes avg. | 20 | 24 | 7 | 7 | 0 |
<details>
<summary>
Detailed Results Per Scene
</summary>
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| Empty screen | 11 | 25 | 0 | 0 | 0 |
| Moving wallpaper | 1 | 25 | 0 | 0 | 0 |
| Single rectangle | 0 | 25 | 0 | 0 | 0 |
| Multiple rectangles | 0 | 25 | 0 | 0 | 0 |
| Multiple RGB images | 0 | 25 | 0 | 0 | 0 |
| Multiple ARGB images | 22 (-1)| 25 | 1 | 1 | 0 |
| Rotated ARGB images | 47 (-1)| 24 | 20 | 20 | 0 |
| Multiple labels | 2 (-2)| 25 | 0 | 0 | 0 |
| Screen sized text | 30 (+1)| 24 (-1)| 11 (-1)| 11 (-1)| 0 |
| Multiple arcs | 19 (+4)| 24 | 7 | 7 | 0 |
| Containers | 1 (-1)| 25 | 0 | 0 | 0 |
| Containers with overlay | 87 (-2)| 21 | 44 | 44 | 0 |
| Containers with opa | 23 (+1)| 25 | 4 | 4 | 0 |
| Containers with opa_layer | 22 (+1)| 25 | 8 | 8 | 0 |
| Containers with scrolling | 25 | 25 | 10 | 10 | 0 |
| Widgets demo | 34 | 24 (-1)| 13 | 13 | 0 |
| All scenes avg. | 20 | 24 | 7 | 7 | 0 |
</details>
#### ARM Emulated 64b - lv_conf_perf64b
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| All scenes avg. | 16 | 24 | 6 | 6 | 0 |
<details>
<summary>
Detailed Results Per Scene
</summary>
| Scene Name | Avg CPU (%) | Avg FPS | Avg Time (ms) | Render Time (ms) | Flush Time (ms) |
|------------|------------|---------|--------------|----------------|--------------|
| Empty screen | 11 | 25 | 0 | 0 | 0 |
| Moving wallpaper | 0 | 25 | 0 | 0 | 0 |
| Single rectangle | 0 | 25 | 0 | 0 | 0 |
| Multiple rectangles | 0 | 25 | 0 | 0 | 0 |
| Multiple RGB images | 0 | 25 | 0 | 0 | 0 |
| Multiple ARGB images | 1 | 25 | 0 | 0 | 0 |
| Rotated ARGB images | 23 | 25 | 10 | 10 | 0 |
| Multiple labels | 3 | 24 | 0 | 0 | 0 |
| Screen sized text | 23 | 24 | 10 | 10 | 0 |
| Multiple arcs | 15 | 24 | 6 | 6 | 0 |
| Containers | 3 (+1)| 25 (+1)| 0 | 0 | 0 |
| Containers with overlay | 90 (+2)| 23 | 41 | 41 | 0 |
| Containers with opa | 19 | 25 | 4 | 4 | 0 |
| Containers with opa_layer | 13 (-2)| 24 (-1)| 5 (-1)| 5 (-1)| 0 |
| Containers with scrolling | 25 | 25 (-1)| 10 | 10 | 0 |
| Widgets demo | 33 | 25 | 13 | 13 | 0 |
| All scenes avg. | 16 | 24 | 6 | 6 | 0 |
</details>
Disclaimer: These benchmarks were run in an emulated environment using QEMU with instruction counting mode.
The timing values represent relative performance metrics within this specific virtualized setup and should
not be interpreted as absolute real-world performance measurements. Values are deterministic and useful for
comparing different LVGL features and configurations, but may not correlate directly with performance on
physical hardware. The measurements are intended for comparative analysis only.
---
:robot: This comment was automatically generated by a bot.

View File

@@ -0,0 +1 @@
[{"scene_name": "Empty screen", "avg_cpu": 11, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Moving wallpaper", "avg_cpu": 1, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Single rectangle", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple rectangles", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple RGB images", "avg_cpu": 0, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Multiple ARGB images", "avg_cpu": 22, "avg_fps": 25, "avg_time": 1, "render_time": 1, "flush_time": 0}, {"scene_name": "Rotated ARGB images", "avg_cpu": 47, "avg_fps": 24, "avg_time": 20, "render_time": 20, "flush_time": 0}, {"scene_name": "Multiple labels", "avg_cpu": 2, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Screen sized text", "avg_cpu": 30, "avg_fps": 24, "avg_time": 11, "render_time": 11, "flush_time": 0}, {"scene_name": "Multiple arcs", "avg_cpu": 19, "avg_fps": 24, "avg_time": 7, "render_time": 7, "flush_time": 0}, {"scene_name": "Containers", "avg_cpu": 1, "avg_fps": 25, "avg_time": 0, "render_time": 0, "flush_time": 0}, {"scene_name": "Containers with overlay", "avg_cpu": 87, "avg_fps": 21, "avg_time": 44, "render_time": 44, "flush_time": 0}, {"scene_name": "Containers with opa", "avg_cpu": 23, "avg_fps": 25, "avg_time": 4, "render_time": 4, "flush_time": 0}, {"scene_name": "Containers with opa_layer", "avg_cpu": 22, "avg_fps": 25, "avg_time": 8, "render_time": 8, "flush_time": 0}, {"scene_name": "Containers with scrolling", "avg_cpu": 25, "avg_fps": 25, "avg_time": 10, "render_time": 10, "flush_time": 0}, {"scene_name": "Widgets demo", "avg_cpu": 34, "avg_fps": 24, "avg_time": 13, "render_time": 13, "flush_time": 0}, {"scene_name": "All scenes avg.", "avg_cpu": 20, "avg_fps": 24, "avg_time": 7, "render_time": 7, "flush_time": 0}]

Some files were not shown because too many files have changed in this diff Show More