Compare commits

...

73 Commits

Author SHA1 Message Date
Lephenixnoir 82027e1057
bump version to 2.10.0 2023-04-01 23:51:12 +02:00
Lephenixnoir 7b77fb9c0b
libfxlink: add status functions to avoid looking into fdev fields 2023-04-01 21:35:28 +02:00
Lephenixnoir 394d05726d
fxlink: add missing <unistd.h> to tui/tui-interactive.c 2023-03-28 21:43:59 +02:00
Lephenixnoir 0c0eb7f4f5
libfxlink: use old libusb for compatibility with 1.0.23 2023-03-28 19:35:00 +02:00
Lephenixnoir 1251ca23ee
libfxlink: return status from fxlink_device_start_bulk_{IN,OUT} 2023-03-27 19:47:08 +02:00
Lephenixnoir 1573db3860
libfxlink: install the library 2023-03-27 19:46:29 +02:00
Lephenixnoir 065233387d
split fxlink into library (not installed yet) and executable 2023-03-26 12:20:50 +02:00
Lephenixnoir 3f4aa1e750
fxlink: usable TUI command setup + gintctl test commands 2023-03-26 11:41:55 +02:00
Lephenixnoir cef9d21076
fxlink: start implementing TUI commands (mainly gintctl tests) 2023-03-17 21:32:01 +01:00
Lephenixnoir f83ea7e3d3
fxsdk: restore almost-original CG icon, but with the GIMP template 2023-03-12 22:07:30 +01:00
Lephenixnoir 4b980d949b
fxsdk: another failed attempt at a better CG icon 2023-03-12 20:55:48 +01:00
Lephenixnoir 0a61ffc523
fxlink: basic TUI commands 2023-03-12 20:55:18 +01:00
Lephenixnoir c7c1ec35f7
fxlink: send zero-length packets after commands 2023-03-12 20:54:53 +01:00
Lephenixnoir 3dc9f06219
fxlink: basic writing logic and 'test' command 2023-03-04 18:07:30 +01:00
Lephenixnoir 9d30377d90
fxlink: full rewrite; deeper device management and TUI
This commit rewrites the entire device management layer of fxlink on the
libusb side, providing new abstractions that support live/async device
management, including communication.

This system is put to use in a new TUI interactive mode (fxlink -t)
which can run in the background, connects to calculators automatically
without interfering with file transfer tools, and is much more detailed
in its interface than the previous interactive mode (fxlink -i).

The TUI mode will also soon be extended to support sending data.
2023-03-03 00:29:00 +01:00
Lephenixnoir d6ed47b133
fxsdk: add build-cg-push to default .gitignore 2023-01-19 21:35:22 +01:00
Lephenixnoir ae452c3bc2 Merge pull request 'Add fast push mode' (#10) from Heath123/fxsdk:dev into dev
Reviewed-on: https://gitea.planet-casio.com/Lephenixnoir/fxsdk/pulls/10
2023-01-19 09:38:12 +01:00
Heath Mitchell 6849c1624e Add push mode 2023-01-17 23:05:38 +00:00
Lephenixnoir 7b2294d8c0
fxsdk: don't generate G3A in fastload, use bin 2023-01-17 10:13:11 +01:00
Lephenixnoir d91e6b7497
fxsdk: add build-cg-push command (no send yet) 2023-01-17 09:58:16 +01:00
Lephenixnoir dc522072e7
fxconv: honor section setting for ASM conversions 2023-01-15 16:32:51 +01:00
Lephenixnoir c9e9f347e4
fxsdk: GIMP icon template and new default icon 2023-01-02 17:56:25 +01:00
Lephenixnoir ebfde1f13d
fxlink: add reconnecting interactive mode (-ir)
This exposes how terrible the libusb code is. Future problem for now.
2022-12-03 13:34:06 +01:00
Lephenixnoir e31d053a08
fxsdk: handle parentheses in paths for fxgxa
Quotes are still impossible because CMake is unredeemable in that
regard.
2022-11-25 10:56:59 +01:00
Lephenixnoir 8c966821e8
fxconv: fix bad detection of fxconv-metadata.txt for relative paths 2022-11-05 18:44:33 +01:00
Lephenixnoir 1a443454c9
fxsdk: provide an FXSDK_TOOLCHAIN variable (= "sh-elf-") 2022-10-29 14:37:28 +02:00
Lephenixnoir db0a54dc25
fxgxa: add missing include 2022-10-04 22:09:30 +02:00
Lephenixnoir a1ee256ba5
bump version to 2.9.2 2022-08-29 21:21:32 +02:00
Lephenixnoir b8e2897560
fxsdk: add an OTHER_MACROS argument to find_simple_library() 2022-08-29 21:20:12 +02:00
Lephenixnoir 38ff1f765b
bump version to 2.9.1 2022-08-29 00:45:10 +02:00
Lephenixnoir 223cd31b6c
fxsdk: compatibility with CMake 3.24 2022-08-29 00:34:55 +02:00
Lephenixnoir 92316b9f5a
bump version to 2.9.0 2022-08-21 19:50:25 +02:00
Lephenixnoir aae8b951c0
fxsdk: add --no-warn-rwx-segments in linker options 2022-08-21 09:40:29 +02:00
Lephenixnoir daf1e865b3
update missing bits in README 2022-08-19 14:52:24 +02:00
Lephenixnoir 147ff08869
fxsdk: add sysroot, support for listdc++, README tutorial, LICENSE
This commit mainly introduces an almost-normal sysroot where the
compiler and libs will be installed. This is done in order to properly
build libstdc++-v3 from the GCC tree while also cleaning up the
slightly chaotic current install system.

* Add an `fxsdk --version` command
* Redo the README like a translation of the French topic
* Up default project to gint 2.9 to help get migrations right
* Add new `fxsdk path` command reporting important folders
* Add new CMake variables FXSDK_SYSROOT, FXSDK_INCLUDE, FXSDK_LIB so
  CMake can access the information of `fxsdk path`
* fxSDK is now in charge of removing the entire sysroot including the
  cross-compiler
* Add a LICENSE file
2022-08-19 14:38:19 +02:00
Lephenixnoir 4296ef0872
fxsdk: adjustment for #9 2022-08-03 21:52:58 +01:00
Lephenixnoir dd6cd3fcd4
fxlink: add a message if `cp` fails 2022-08-03 21:50:15 +01:00
Lephenixnoir 6032ce38cf Merge pull request 'Added a specific charset (extascii) to get access to 256 characters of the ascii table' (#9) from Slyvtt/fxsdk:slyvtt-patch-1 into dev
Reviewed-on: https://gitea.planet-casio.com/Lephenixnoir/fxsdk/pulls/9
2022-08-03 22:51:13 +02:00
Sylvain PILLOT 2c0815077c Added a specific charset (extascii) to get access to 256 characters of the ascii table
Added the support of extended ascii table (char 128 and aboce up to 255).
2022-08-01 14:40:13 +02:00
Lephenixnoir 1a7939bbb9
fxsdk: fix long-standing typo in default .gitignore 2022-07-16 18:18:57 +01:00
Lephenixnoir a7046966da
fxconv: fix double object file size for RGB565 images 2022-07-16 17:23:14 +01:00
Lephenixnoir 00b85297a3
bump version to 2.8.0 2022-05-16 22:03:29 +01:00
Lephenixnoir e4289f6c4c
fxconv: remove alpha field of images 2022-05-14 12:54:18 +01:00
Lephenixnoir b29c494715
fxconv: rewrite image converter, forcing alpha value 2022-05-14 12:38:52 +01:00
Lephenixnoir 58cb14157d
fxconv: new image format (and libimg stride update) 2022-05-12 15:26:42 +01:00
Lephenixnoir 6fd943ea67
fxconv: simplify alpha assignment in P8/P4
* alpha is now the last color of the palette rather than always being 0.
* alpha is not materialized in the P8 palette.
* Fixed a bug where images with more than 32/256 colors being converted
  in P4/P8 with transparency would use all colors for opaque pixels,
  causing alpha to randomly land on a color index that is in use.
2022-05-07 17:54:20 +01:00
Lephenixnoir 6d2dcea900
fxconv: "p4" and "p8" now select best new format
Instead of p8 being the old p8 (which now doesn't display anymore!) and
p4 being p4_rgb565a.
2022-05-07 14:00:26 +01:00
Lephenixnoir 6788a7b5fe
fxsdk: round RGB16 images to even widths
This allows 4-alignment on the input to be preserved from line to line,
which is very valuable for rendering optimizations.
2022-05-04 17:16:33 +01:00
Lephenixnoir 27e60884c3
fxlink: generate blob names based on application/type 2022-05-01 18:56:52 +01:00
Lephenixnoir 5e004f989e
Cleanup for PR#8 2022-05-01 16:20:47 +01:00
Lephenixnoir cd9d669275
Merge PR #8 into dev 2022-05-01 15:36:16 +01:00
Sylvain PILLOT 6103d852d5 added -u, --unmount option to force unmounting the disk after end of operations 2022-04-20 18:07:58 +02:00
Sylvain PILLOT 85314f8310 change option name to -q, --quiet and improved code layout 2022-04-20 13:07:30 +02:00
Sylvain PILLOT 89ca11678c added --silent-mode and --fxlink-log options 2022-04-19 18:36:09 +02:00
Lephenixnoir c517447a02
fxsdk: finally implement the send-fx and send-cg commands 2022-04-15 10:53:56 +01:00
Lephenixnoir 2cc5d7ac5b
implement fxgxa and use it in both build systems 2022-03-20 19:58:11 +00:00
Lephenixnoir ec39aa5cde
move fxg1a/ -> fxgxa/ to prepare implementing fxg3a 2022-03-20 12:34:08 +00:00
Lephenixnoir 0fd94fd894
bump version to 2.7.1 2022-03-19 19:33:00 +00:00
Lephenixnoir 5feca5d2e1
cmake: allow single quotes in paths for generate_g3a() 2022-03-19 19:32:57 +00:00
Lephenixnoir e8c7084942
fxsdk: avoid quote in heredoc for Mac OS
Not sure what's happening here.
2022-02-18 10:47:25 +01:00
Lephenixnoir aa362c15f6
fxlink: don't use <endian.h> 2022-02-16 20:58:41 +01:00
Lephenixnoir 90d5e723e0
fxconv: explicit error when no metadata is set for a file 2022-01-28 19:07:53 +01:00
Lephenixnoir 41c7ac0aec
fxconv: allow custom-type to work as type 2022-01-28 19:07:38 +01:00
Lephenixnoir f17d741d54
fxconv: fix CLI which was broken since 4d46661d3 2022-01-26 20:54:40 +01:00
Lephenixnoir 9e097109bc
cmake: allow assets at root of project in fxconv_declare_assets() 2022-01-05 19:15:35 +01:00
Lephenixnoir b777fe76f6
bump version to 2.7.0 2021-12-31 10:49:17 +01:00
Lephenixnoir bbf716b031
fxconv: resolve name_regex in the fxconv-metadata.txt API 2021-12-28 19:01:43 +01:00
Lephenixnoir 4d46661d3b
fxconv: expose fxconv-metadata.txt parsing functions in API 2021-12-28 18:37:40 +01:00
Lephenixnoir 6dae13007e
fxconv: "fix" palette remap with non-continous "P" image
Just take the holes and save less space
2021-12-23 21:14:41 +01:00
Lephenixnoir afc6d51217
fxsdk: add more fxg1a/mkg3a flags in generate_{g1a,g3a}() 2021-11-18 23:16:17 +01:00
Lephenixnoir cdeefa1de6
fxconv: fix conversion of char-spacing attribute 2021-11-04 14:46:40 +01:00
Lephenixnoir 317b82348f
fxconv: add support for {P4,P8}_{RGB565,RGB565A} for Azur 2021-10-23 16:45:58 +02:00
Lephenixnoir 68be7fe522
fxconv: allow section specification from fxconv-metadata.txt 2021-09-26 10:55:20 +02:00
92 changed files with 8159 additions and 3108 deletions

View File

@ -1,7 +1,7 @@
# Build system for the fxSDK
cmake_minimum_required(VERSION 3.15)
project(fxSDK VERSION 2.6.0 LANGUAGES C)
project(fxSDK VERSION 2.10.0 LANGUAGES C)
option(FXLINK_DISABLE_UDISKS2 "Do not build the UDisks2-based features of fxlink")
option(FXLINK_DISABLE_SDL2 "Do not build the SDL2-based features of fxlink")
@ -9,13 +9,13 @@ option(FXLINK_DISABLE_SDL2 "Do not build the SDL2-based features of fxlink")
find_package(PkgConfig REQUIRED)
pkg_check_modules(libpng REQUIRED libpng16 IMPORTED_TARGET)
pkg_check_modules(libusb REQUIRED libusb-1.0 IMPORTED_TARGET)
# pkg_check_modules(libudev libudev IMPORTED_TARGET)
if(NOT FXLINK_DISABLE_UDISKS2)
pkg_check_modules(udisks2 REQUIRED udisks2 IMPORTED_TARGET)
endif()
if(NOT FXLINK_DISABLE_SDL2)
pkg_check_modules(sdl2 REQUIRED sdl2 IMPORTED_TARGET)
endif()
pkg_check_modules(ncurses REQUIRED ncurses IMPORTED_TARGET)
set(CMAKE_INSTALL_MESSAGE LAZY)
set(SRC "${CMAKE_CURRENT_SOURCE_DIR}")
@ -23,26 +23,61 @@ set(BIN "${CMAKE_CURRENT_BINARY_DIR}")
add_compile_options(-Wall -Wextra -std=c11 -Og -g -D_GNU_SOURCE)
# fxg1a
add_executable(fxg1a fxg1a/dump.c fxg1a/edit.c fxg1a/file.c fxg1a/icon.c
fxg1a/main.c fxg1a/util.c)
target_include_directories(fxg1a PUBLIC fxg1a/)
target_link_libraries(fxg1a PkgConfig::libpng)
# fxgxa
add_executable(fxgxa fxgxa/dump.c fxgxa/edit.c fxgxa/file.c fxgxa/icon.c
fxgxa/main.c fxgxa/util.c)
target_include_directories(fxgxa PUBLIC fxgxa/)
target_link_libraries(fxgxa PkgConfig::libpng)
# fxg1a as a symlink (for compatibility)
add_custom_target(fxg1a ALL
COMMAND ${CMAKE_COMMAND} -E create_symlink "fxgxa" "fxg1a")
# fxsdk
add_custom_command(OUTPUT "${BIN}/fxsdk.sh"
COMMAND sed "'s#@FXSDK_PREFIX@#${CMAKE_INSTALL_PREFIX}#'"
COMMAND sed
-e "'s#@CMAKE_INSTALL_PREFIX@#${CMAKE_INSTALL_PREFIX}#'"
-e "'s#@CMAKE_PROJECT_VERSION@#${CMAKE_PROJECT_VERSION}#'"
"${SRC}/fxsdk/fxsdk.sh" > "${BIN}/fxsdk.sh"
DEPENDS "${SRC}/fxsdk/fxsdk.sh")
add_custom_target(fxsdk ALL DEPENDS "${BIN}/fxsdk.sh")
# libfxlink
configure_file(libfxlink/include/fxlink/config.h.in
"${BIN}/include/fxlink/config.h")
add_library(libfxlink STATIC
libfxlink/defs.c
libfxlink/devices.c
libfxlink/filter.c
libfxlink/logging.c
libfxlink/protocol.c)
target_link_libraries(libfxlink PUBLIC PkgConfig::libusb)
target_include_directories(libfxlink PUBLIC
"${BIN}/include"
"${SRC}/libfxlink/include")
set_target_properties(libfxlink PROPERTIES
OUTPUT_NAME "fxlink") # libfxlink.a
# fxlink
configure_file(fxlink/config.h.in "${BIN}/include/fxlink/config.h")
add_executable(fxlink fxlink/usb.c fxlink/filter.c fxlink/interactive.c
fxlink/main.c fxlink/png.c fxlink/properties.c fxlink/ud2.c fxlink/util.c
fxlink/protocol.c fxlink/sdl2.c)
target_link_libraries(fxlink PkgConfig::libpng PkgConfig::libusb) # PkgConfig::libudev
target_include_directories(fxlink PRIVATE "${BIN}/include/fxlink")
add_executable(fxlink
fxlink/main.c
fxlink/modes/interactive.c
fxlink/modes/list.c
fxlink/modes/push.c
fxlink/modes/udisks2.c
fxlink/tooling/libpng.c
fxlink/tooling/sdl2.c
fxlink/tooling/udisks2.c
fxlink/tui/commands.c
fxlink/tui/command-util.c
fxlink/tui/input.c
fxlink/tui/layout.c
fxlink/tui/render.c
fxlink/tui/tui-interactive.c)
target_link_libraries(fxlink
libfxlink PkgConfig::libpng PkgConfig::ncurses -lm)
target_include_directories(fxlink PRIVATE
"${SRC}/fxlink/include")
if(NOT FXLINK_DISABLE_UDISKS2)
target_link_libraries(fxlink PkgConfig::udisks2)
endif()
@ -56,10 +91,17 @@ endif()
install(PROGRAMS "${BIN}/fxsdk.sh" TYPE BIN RENAME fxsdk)
install(DIRECTORY fxsdk/assets DESTINATION share/fxsdk)
install(DIRECTORY fxsdk/cmake/ DESTINATION lib/cmake/fxsdk)
# fxg1a
install(TARGETS fxg1a)
# fxgxa, fxg1a
install(TARGETS fxgxa)
install(FILES "${BIN}/fxg1a" TYPE BIN)
# fxconv
install(PROGRAMS fxconv/fxconv-main.py TYPE BIN RENAME fxconv)
install(FILES fxconv/fxconv.py TYPE BIN)
#fxlink
# libfxlink
install(FILES "${BIN}/include/fxlink/config.h" DESTINATION include/fxlink/)
install(DIRECTORY libfxlink/include/ DESTINATION include
FILES_MATCHING PATTERN "*.h")
install(DIRECTORY libfxlink/cmake/ DESTINATION lib/cmake)
install(TARGETS libfxlink DESTINATION lib)
# fxlink
install(TARGETS fxlink)

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright (C) 2015-2022 gint/fxSDK contributors
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.

216
README.md
View File

@ -1,151 +1,169 @@
# fxSDK
The fxSDK is a development kit for CASIO graphing calculators in the fx-9860G
and fx-CG 50 families. It provides command-line helper tools and build systems
for add-ins and libraries, and is commonly used to develop add-ins running the
[gint kernel](/Lephenixnoir/gint).
*Topic on Planète Casio : [fxSDK, un SDK alternatif pour écrire des add-ins](https://www.planet-casio.com/Fr/forums/topic13164-last-fxsdk-un-sdk-alternatif-pour-ecrire-des-add-ins.html)*
The fxSDK is free software; you may use it for any purpose, share it, modify
modify it and share your changes. No credit required, but please let me know!
The fxSDK is a development kit for CASIO graphing calculators in the fx-9860G and fx-CG series. It provides command-line tools, build systems and a cross-compilation setup for add-ins and libraries. It's designed to be used with the [gint unikernel](/Lephenixnoir/gint) which provides a powerful base runtime to build add-ins from.
The fxSDK is compatible with Linux and has been successfully built on Mac OS.
If there are compatibility issues, I am willing to try and port stuff to your
favorite operating system. Windows users have good support with WSL.
This repository only contains the command-line tools of the SDK; in order to program add-ins more components like a [cross-compiler](https://gitea.planet-casio.com/Lephenixnoir/sh-elf-gcc) and usually the [gint kernel](https://gitea.planet-casio.com/Lephenixnoir/gint) are required. See the [Installing](#installing) section for details.
## Basic install and use
The fxSDK is distributed under the [MIT License](LICENSE).
The simplest way to install the fxSDK is to use
[GiteaPC](/Lephenixnoir/GiteaPC), an automation tool that builds and installs
repositories from Planète Casio's Gitea forge.
## Compatibility and support
```bash
% giteapc install Lephenixnoir/fxsdk
```
**Calculators**
The fxSDK depends on the [`sh-elf-gcc` compiler](/Lephenixnoir/sh-elf-gcc) so
GiteaPC might build it too as a dependency. You will also need the PIL library
for Python, as well as libusb and optionally UDisks2 for fxlink. Use the
GiteaPC configuration `:noudisks2` to disable UDisks2-based features.
The fxSDK (more specifically gint in this case) targets most calculators of the CASIO fx-9860G series, including:
```bash
# On Debian, Ubuntu, WSL and the like:
% sudo apt install python3-pil libusb-dev # (Optionally) udisks2
# On Arch Linux, Manjaro and the like:
% sudo pacman -S python-pillow libusb # (Optionally) udisks2
```
* ![](https://www.planet-casio.com/images/icones/calc/g85.png) (Partial support) SH3-based fx-9860G, fx-9860G SD, similar models with OS 1.xx, and the fx-9860G SDK emulator.
* ![](https://www.planet-casio.com/images/icones/calc/g95.png) SH3-based fx-9750G, fx-9860G II, fx-9860G II SD, and similar models with OS 2.xx.
* ![](https://www.planet-casio.com/images/icones/calc/g75+.png) All SH4-based models with OS 2.xx, including the fx-9750G II and SH4 fx-9860G II.
* ![](https://www.planet-casio.com/images/icones/calc/g35+e2.png) The fx-9750G III and fx-9860G III.
Use the `fxsdk` command to manage projects. You can create an empty add-in
project with `fxsdk new` and a name for a new folder:
It also supports the calculators of the CASIO fx-CG series:
```bash
% fxsdk new MyAddin
```
* ![](https://www.planet-casio.com/images/icones/calc/cg20.png) The original fx-CG 10/20.
* ![](https://www.planet-casio.com/images/icones/calc/g90+e.png) The fx-CG 50.
From that folder, you can build the add-in with the build commands:
**Operating systems**
```bash
# Build the add-in for fx-9860G (.g1a):
% fxsdk build-fx
# Build the add-in for fx-CG 50 (.g3a):
% fxsdk build-cg
```
* Linux: officially supported; any distribution should work as long as you can install the dependencies.
* Mac OS: builds consistently but may require slight tweaking due to lack of regular testing.
* Windows: WSL's Ubuntu is officially supported. Windows itself is not supported, but contributions towards it are welcome.
## Tools in the fxSDK
**Programming languages**
A tool called *fxos* used to live here and has now moved to [its own
repository](/Lephenixnoir/fxos).
* C: Latest GCC support for the language (currently C11/C2X). The standard library is the custom-built [fxlibc](https://gitea.planet-casio.com/Vhex-Kernel-Core/fxlibc/) which supports most of C99. Using another ported libc is possible.
* C++: Latest GCC support for the language (currently C++20/C++23). The standard library is the latest [libstdc++](https://gcc.gnu.org/onlinedocs/libstdc++/).
* Assembly: CASIO calculators run on SuperH processors (mostly the SH4AL-DSP). binutils provides the tools needed to write programs in assembler.
* Others? Stuff that compiles to C is fine (eg. [fxtran for Fortran](https://www.planet-casio.com/Fr/forums/topic17064-1-fxtran-codez-en-fortran-pour-votre-casio.html)). Other GCC targets can work ([the D compiler builds](https://www.planet-casio.com/Fr/forums/topic17037-last-omegafail-dlang-et-gint.html) -- libphobos slightly harder). Languages that target LLVM, like Rust, are definitely out of the question for now as LLVM does not have a SuperH backend.
**Build systems**
The fxSDK uses CMake as the main build system for add-ins and libraries. An older Makefile-based project template is still available and CLI interfaces are officially maintained, but you're expected to manage your build system through updates if you stray from CMake.
## Installing
The following options are all for installing the entire fxSDK including the cross-compiler, libraries, etc. not just this repository.
**Method 1: Using GiteaPC (recommended for beginners)**
[GiteaPC](https://gitea.planet-casio.com/Lephenixnoir/GiteaPC) is a package-manager-like set of scripts to clone, build and install repositories from Planète Casio's Gitea forge. It automates basically the entire process, and is the recommended way to get the fxSDK up and running. See the instructions on the repository's README.
**Method 2: Using the AUR (for Arch Linux-based distributions)**
[Dark Storm](https://www.planet-casio.com/Fr/compte/voir_profil.php?membre=Dark%20Storm) maintains a package repository for Arch called [MiddleArch](https://www.planet-casio.com/Fr/forums/topic16790-1-middlearch-un-depot-communautaire.html) which includes consistently up-to-date versions of the fxSDK.
**Method 3: Manual build (for experienced users)**
You can build the fxSDK and its tools manually by following the instructions in each README file. Please refer to the GiteaPC tutorial for a list of what to install in what order. As a warning: there is quite a lot of stuff (SDK tools, the cross-compiler, a libm, a libc, the libstdc++, the kernel, user libraries and then some) so expect to spend some time installing and updating everything.
## Using the fxSDK: Command-line tools
When developing add-ins with the fxSDK, you mainly interact with command-line tools and the fxSDK's build system. Let's first have a look at the command-line tools. You can get the command-line help for any tool by invoking it without arguments, eg `fxsdk` or `fxgxa`.
*Note: A tool called `fxos` used to live here and has now moved to [its own repository](/Lephenixnoir/fxos).*
**Project management** with `fxsdk`
`fxsdk` lets you set up projects almost instantly with a default folder
structure and a build system for both fx-9860G and fx-CG 50. The default build
system is CMake since version 2.3, and a bare-bones Makefile is also supported.
Use the `fxsdk` command to manage projects. You can create an empty add-in project with `fxsdk new` and a name for a new folder:
`fxsdk` only writes files at project creation time, so you keep control over
your build system and configuration - it just helps you get started faster.
```
% fxsdk new MyAddin
Created a new project MyAddin (build system: CMake).
Type 'fxsdk build-fx' or 'fxsdk build-cg' to compile the program.
```
Summary of commands (`fxsdk --help` for details):
From that folder, you can build the add-in with the `fxsdk build-*` commands:
* `fxsdk new`: Create a new project
* `fxsdk build/build-fx/build-cg`: Configure and compile add-ins and libraries
* `fxsdk send/send-fx/send-cg`: Install files to the calculator (WIP)
```bash
# Build the add-in for fx-98600G-series calculators (.g1a):
% fxsdk build-fx
# Build the add-in for fx-CG-series calculators (.g3a):
% fxsdk build-cg
```
**G1A file generation** with `fxg1a`
You can then send the add-in through your preferred method. Some shortcuts are provided:
`fxg1a` is a versatile g1a file editor that creates, edits and dumps the header
of fx-9860G add-ins files. It is used to build a g1a file out of a binary
program.
* `fxsdk send-fx` will send the g1a file with [p7](https://gitea.planet-casio.com/cake/p7utils) (which is like FA-124/xfer9860) if it's installed. This works for every fx-9860G-series models except the fx-975G0G III and fx-9860G III.
* `fxsdk send-cg` will send the g3a file with fxlink using UDisks2, replicating the process of copying with the file manager. See below for details about fxlink.
It supports PNG icons, checking the validity and checksums of the header,
repairing broken headers and dumping both the application data and icon.
The command `fxsdk path` reports the folders in which the important files of the SDK (mainly the cross-compiler) are located.
`fxg1a` is called automatically by the build system in your add-in, so you
don't need to worry about it, but here are the main commands:
**G1A/G3A file generation** with `fxgxa`
* `fxg1a -g`: Generate g1a files
* `fxg1a -e`: Edit g1a files
* `fxg1a -d`: Dump metadata, checksum, and icon
* `fxg1a -r`: Repair control bytes and checksums for broken files
* `fxg1a -x`: Extract icon into a PNG file
`fxgxa` is a versatile g1a/g3a file editor that creates, edits and dumps the header of add-ins files. The build system calls it as part of `fxsdk build-*` so you only need to use it directly when you want to inspect existing add-ins.
It supports using and dumping PNG icons of any formats, validating header checksums, repairing broken headers and dumping add-in details. Here are the main commands:
* `fxgxa --g1a|--g3a`: Generate g1a/g3a files
* `fxgxa -e`: Edit g1a/g3a files
* `fxgxa -d`: Dump metadata, checksum, and icon
* `fxgxa -r`: Repair control bytes and checksums for broken files
* `fxgxa -x`: Extract icons into PNG files
`fxgxa` has an alias, `fxg1a`, for compatibility with fxSDK up to 2.7 for which there was no g3a file editor in the fxSDK.
**Asset conversion** with `fxconv`
`fxconv` is a programmable asset converter that converts images, fonts and
other common asset types into data structures usable directly in add-ins. The
built-in formats include gint images and fonts, [libimg](/Lephenixnoir/libimg)
images, and binary blobs.
`fxconv` is a programmable asset converter that converts images, fonts and other common asset types into data structures usable directly in add-ins. The built-in formats include gint images and fonts, [libimg](/Lephenixnoir/libimg) images, and binary blobs.
Projects can extend the support to custom types for maps, dialogs, GUI
descriptions, or other application-specific assets. Extensions to `fxconv` are
implemented in Python within the project.
Projects can extend the support to custom types for maps, dialogs, GUI descriptions, or other application-specific assets. Extensions to `fxconv` are implemented in Python within the project.
`fxconv` can be used directly on the command-line but normally you specify
parameters in `fxconv-metadata.txt` and let the build system do the magic.
`fxconv` is tightly integrated into the build system. Normally you declare assets in a `CMakeLists.txt` file, set their parameters in an `fxconv-metadata.txt` file, and then let the build system do the magic.
TODO: Link to gint tutorials or better explain how to use fxconv.
**USB communication** with `fxlink`
`fxlink` is a USB communication tool that can be used to send files to
calculators as well as to communicate with gint's USB driver from an add-in.
Currently, the tool is a work-in-progress, and most of the work has been spent
in properly detecting, characterizing and filtering connected calculators.
`fxlink` is a USB communication tool that can be used to send files to calculators as well as to communicate with gint's USB driver from an add-in. The tool is still it its early stages but already offers two useful features.
The main backend is libusb. With libusb, `fxlink` can detect CASIO calculators
connected through USB and has a test mode used to prototype interactions with
gint.
Note: `fxlink` does not work within WSL machines on Windows, see [this bug](https://github.com/Microsoft/WSL/issues/2195).
`fxlink` also supports a UDisks2 backend for systems that have UDisks2; with
this backend, `fxlink` can detect Mass Storage calculators (essentially the
fx-CG series and the G-III series) connected through USB, mount them without
root access, and transfer some files all from the command-line.
The first feature is interactive communication with add-ins using libusb. This allows add-ins to send text, screenshots, and video captures of their output to a computer in real-time.
The second feature is sending files to fx-CG and G-III calculators (the ones that behave like USB drives) using UDisks2. `fxlink` can mount the calculators, copy files and unmount them from the command-line without root access.
## Using the fxSDK: Build systems
**Building add-ins with CMake**
The official build system is CMake since fxSDK 2.3. When creating a new project, a default `CMakeLists.txt` is generated. There are few deviations from standard CMake practices; refer to a CMake tutorial for general explanations (there is [an fxSDK-specific one in French on Planète Casio](https://www.planet-casio.com/Fr/forums/topic16647-1-tutoriel-compiler-des-add-ins-avec-cmake-fxsdk.html)). The differences are explained below.
Because we are using a cross-compiler, we can't just call `cmake` to configure the project; extra parameters are needed. The `fxsdk build-*` commands call CMake for you with the correct parameters.
The fxSDK provides [a couple of modules](fxsdk/cmake), including:
* [`FX9860G.cmake`](fxsdk/cmake/FX9860G.cmake) and [`FXCG50.cmake`](fxsdk/cmake/FXCG50.cmake) that are loaded automatically by `fxsdk build-fx` and `fxsdk build-cg` respectively. These are one of the reasons why we don't call `cmake` directly. Anything defined here is available to your `CMakeLists.txt`, which includes a number of variables called `FXSDK_*` to give you information on the target and install.
* [`Fxconv.cmake`](fxsdk/cmake/Fxconv.cmake) which provides functions to use fxconv. `fxconv_declare_assets(... WITH_METADATA)` will mark source files as assets to be converted with fxconv, and `fxconv_declare_converters(...)` declares Python modules containing custom conversion functions.
* [`GenerateG1A.cmake`](fxsdk/cmake/GenerateG1A.cmake) and [`GenerateG3A.cmake`](fxsdk/cmake/GenerateG3A.cmake) wrap `fxgxa` and allow you to generate g1a/g3a files for the add-in. The default `CMakeLists.txt` shows how to use them.
* [`FindSimpleLibrary.cmake`](fxsdk/cmake/FindSimpleLibrary.cmake) and [`GitVersionNumber.cmake`](fxsdk/cmake/GitVersionNumber.cmake) are general utilities for libraries. See the [Lephenixnoir/Template-gint-library](https://gitea.planet-casio.com/Lephenixnoir/Template-gint-library) repository for an example of building a library with the fxSDK.
**Building add-ins with make alone**
The original Makefile used to build add-ins is still available. A Makefile-based project can be created with the `--makefile` option of `fxsdk new`. However that Makefile is rarely tested thus occasionally out-of-date, and in general requires you to maintain it through fxSDK updates. It is only advised to use it if you're experienced with make.
## Manual build instructions
The fxSDK is platform-agnostic; a single install will cover any target
platforms. Here are the dependencies:
The following instructions are part of the manual install process for the fxSDK. You should install this repository first. If you previously had an fxSDK setup, cross-compiler, etc. then you should probably remove them before installing the new one to avoid interference.
The dependencies for this repository are:
* CMake
* libpng ≥ 1.6
* Python ≥ 3.7 (might work in 3.6)
* The Pillow library for Python 3
* libusb 1.0
* The UDisks2 library, unless disabled
* The UDisks2 library (unless disabled)
First configure; usual options are:
When configuring, you should set an install prefix that you have write access to. I suggest `$HOME/.local`. Note that the cross-compiler *must* be later installed in the path printed by `fxsdk path sysroot`, which is within said prefix.
* `-DCMAKE_INSTALL_PREFIX` to change the install folder;
* `-DFXLINK_DISABLE_UDISKS2=1` to disable UDisks2 support in `fxlink`.
* Use `-DCMAKE_INSTALL_PREFIX=...` to change the install folder;
* Use `-DFXLINK_DISABLE_UDISKS2=1` to disable UDisks2 support in `fxlink`, if you don't have UDisks2 or you're using WSL.
```sh
% cmake -B build
```
Then make and install as usual.
```sh
```bash
% cmake -B build [OPTIONS...]
% make -C build
% make -C build install
```
If you selected an install folder for which you don't have write access (which
apparently includes the default folder on Mac OS), you will need `sudo` to
install.
You can then proceed to install the cross-compiler. If in doubt about the order in which you need to install repositories, refer to the [GiteaPC README](https://gitea.planet-casio.com/Lephenixnoir/GiteaPC) or check the `giteapc.make` files of each repository, where dependencies are listed.

View File

@ -3,8 +3,6 @@
import getopt
import sys
import os
import re
import fnmatch
import fxconv
import importlib.util
@ -54,52 +52,6 @@ try:
except ImportError:
converters = []
def parse_parameters(params):
"""Parse parameters of the form "NAME:VALUE" into a dictionary."""
d = dict()
def insert(d, path, value):
if len(path) == 1:
d[path[0]] = value
else:
if not path[0] in d:
d[path[0]] = dict()
insert(d[path[0]], path[1:], value)
for decl in params:
if ":" not in decl:
raise FxconvError(f"invalid parameter {decl}, ignoring")
else:
name, value = decl.split(":", 1)
value = value.strip()
if name == "name_regex":
value = value.split(" ", 1)
insert(d, name.split("."), value)
return d
def parse_parameters_metadata(contents):
"""Parse parameters from a metadata file contents."""
RE_COMMENT = re.compile(r'#.*$', re.MULTILINE)
contents = re.sub(RE_COMMENT, "", contents)
RE_WILDCARD = re.compile(r'^(\S(?:[^:\s]|\\:|\\ )*)\s*:\s*$', re.MULTILINE)
lead, *elements = [ s.strip() for s in re.split(RE_WILDCARD, contents) ]
if lead:
raise FxconvError(f"invalid metadata: {lead} appears before wildcard")
# Group elements by pairs (left: wildcard, right: list of properties)
elements = list(zip(elements[::2], elements[1::2]))
metadata = []
for (wildcard, params) in elements:
params = [ s.strip() for s in params.split("\n") if s.strip() ]
metadata.append((wildcard, parse_parameters(params)))
return metadata
def main():
types = "binary image font bopti-image libimg-image custom"
mode = ""
@ -154,25 +106,26 @@ def main():
# In automatic mode, look for information in fxconv-metadata.txt
if mode == "":
metadata_file = os.path.dirname(input) + "/fxconv-metadata.txt"
basename = os.path.basename(input)
metadata_file = os.path.join(os.path.dirname(input),
"fxconv-metadata.txt")
if not os.path.exists(metadata_file):
return err(f"using auto mode but {metadata_file} does not exist")
with open(metadata_file, "r") as fp:
metadata = parse_parameters_metadata(fp.read())
metadata = fxconv.Metadata(path=metadata_file)
params = metadata.rules_for(input)
params = dict()
for (wildcard, p) in metadata:
if fnmatch.fnmatchcase(basename, wildcard):
params.update(**p)
if params is None:
return err(f"no metadata specified for {input}")
if "section" in params:
target["section"] = params["section"]
# In manual conversion modes, read parameters from the command-line
else:
params = parse_parameters(args)
params = fxconv.parse_parameters(args)
if "type" in params:
if "type" in params or "custom-type" in params:
pass
elif len(mode) == 1:
params["type"] = { "b": "binary", "i": "image", "f": "font" }[mode]
@ -180,7 +133,7 @@ def main():
params["type"] = mode
# Will be deprecated in the future
if params["type"] == "image":
if params.get("type") == "image":
warn("type 'image' is deprecated, use 'bopti-image' instead")
params["type"] = "bopti-image"

View File

@ -6,6 +6,7 @@ import os
import tempfile
import subprocess
import collections
import fnmatch
import re
from PIL import Image
@ -23,6 +24,8 @@ __all__ = [
"convert_bopti_fx", "convert_bopti_cg",
"convert_topti",
"convert_libimg_fx", "convert_libimg_cg",
# Meta API to use fxconv-metadata.txt files
"Metadata", "parse_parameters",
]
#
@ -87,35 +90,50 @@ FX_PROFILES = [
# fx-CG 50 profiles
class CgProfile:
def __init__(self, id, name, alpha):
"""
Construct a CgProfile object.
* [id] is the profile ID in bopti
* [name] is the profile name as found in the specification key
* [alpha] is True if this profile supports alpha, False otherwise
"""
def __init__(self, id, depth, names, alpha=None, palette=None):
# Numerical ID
self.id = id
self.name = name
self.supports_alpha = alpha
# Name for printing
self.names = names
# Bit depth
self.depth = depth
# Whether the profile has alpha
self.has_alpha = (alpha is not None)
# Alpha value (None has_alpha == False)
self.alpha = alpha
# Whether the profile is indexed
self.is_indexed = (palette is not None)
if palette is not None:
# Palette base value (skipping the alpha value)
self.palette_base = palette[0]
# Color count (indices are always 0..color_count-1, wraps around)
self.color_count = palette[1]
# Whether to trim the palette to a minimal length
self.trim_palette = palette[2]
@staticmethod
def find(name):
"""Find a profile by name."""
for profile in CG_PROFILES:
if profile.name == name:
if name in profile.names:
return profile
return None
IMAGE_RGB16 = 0
IMAGE_P8 = 1
IMAGE_P4 = 2
CG_PROFILES = [
# 16-bit R5G6B5
CgProfile(0x0, "r5g6b5", False),
# 16-bit R5G6B5 with alpha
CgProfile(0x1, "r5g6b5a", True),
# 8-bit palette
CgProfile(0x2, "p8", True),
# 4-bit palette
CgProfile(0x3, "p4", True),
# 16-bit RGB565 and RGB565 with alpha
CgProfile(0x0, IMAGE_RGB16, ["rgb565", "r5g6b5"]),
CgProfile(0x1, IMAGE_RGB16, ["rgb565a", "r5g6b5a"], alpha=0x0001),
# 8-bit palette for RGB565 and RGB565A
CgProfile(0x4, IMAGE_P8, "p8_rgb565", palette=(0x80,256,True)),
CgProfile(0x5, IMAGE_P8, "p8_rgb565a", alpha=0x80, palette=(0x81,256,True)),
# 4-bit palette for RGB565 and RGB565A
CgProfile(0x6, IMAGE_P4, "p4_rgb565", palette=(0,16,False)),
CgProfile(0x3, IMAGE_P4, "p4_rgb565a", alpha=0, palette=(1,16,False)),
]
# Libimg flags
@ -128,19 +146,21 @@ LIBIMG_FLAG_RO = 2
FX_CHARSETS = {
# Digits 0...9
"numeric": [ (ord('0'), 10) ],
"numeric": [ (ord('0'), 10) ],
# Uppercase letters A...Z
"upper": [ (ord('A'), 26) ],
"upper": [ (ord('A'), 26) ],
# Upper and lowercase letters A..Z, a..z
"alpha": [ (ord('A'), 26), (ord('a'), 26) ],
"alpha": [ (ord('A'), 26), (ord('a'), 26) ],
# Letters and digits A..Z, a..z, 0..9
"alnum": [ (ord('A'), 26), (ord('a'), 26), (ord('0'), 10) ],
"alnum": [ (ord('A'), 26), (ord('a'), 26), (ord('0'), 10) ],
# All printable characters from 0x20 to 0x7e
"print": [ (0x20, 95) ],
# All 128 ASII characters
"ascii": [ (0x00, 128) ],
"print": [ (0x20, 95) ],
# All 128 ASCII characters
"ascii": [ (0x00, 128) ],
# Custom Unicode block intervals
"unicode": [],
"unicode": [],
# Single block 0x00-0xff (does not imply single-byte encoding)
"256chars": [ (0x00, 256) ],
}
#
@ -503,7 +523,19 @@ def _image_project(img, f):
# Image conversion for fx-CG 50
#
def image_has_alpha(img):
# Convert the alpha channel to 1-bit; check if there are transparent pixels
try:
alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE)
alpha_levels = { t[1]: t[0] for t in alpha_channel.getcolors() }
return 0 in alpha_levels
except ValueError:
return False
def convert_bopti_cg(input, params):
return convert_image_cg(input, params)
def convert_image_cg(input, params):
if isinstance(input, Image.Image):
img = input.copy()
else:
@ -515,41 +547,53 @@ def convert_bopti_cg(input, params):
area = Area(params.get("area", {}), img)
img = img.crop(area.tuple())
# If no profile is specified, fall back to r5g6b5 or r5g6b5a later on
name = params.get("profile", None)
if name is not None:
profile = CgProfile.find(name)
img = img.convert("RGBA")
if name in [ "r5g6b5", "r5g6b5a", None ]:
# Encode the image into the 16-bit format
encoded, alpha = r5g6b5(img)
#---
# Format selection
#---
name = "r5g6b5" if alpha is None else "r5g6b5a"
profile = CgProfile.find(name)
format_name = params.get("profile", "")
has_alpha = image_has_alpha(img)
elif name in [ "p4", "p8" ]:
# Encoded the image into 16-bit with a palette of 16 or 256 entries
color_count = 1 << int(name[1])
encoded, palette, alpha = r5g6b5(img, color_count=color_count)
# If no format is specified, select rgb565 or rgb565a
if format_name == "":
format_name = "rgb565a" if has_alpha else "rgb565"
# Similarly, if "p8" or "p4" is specified, select the cheapest variation
elif format_name == "p8":
format_name = "p8_rgb565a" if has_alpha else "p8_rgb565"
elif format_name == "p4":
format_name = "p4_rgb565a" if has_alpha else "p4_rgb565"
encoded = palette + encoded
# Otherwise, just use the format as specified
format = CgProfile.find(format_name)
if format is None:
raise FxconvError(f"unknown image format '{format_name}")
# Check that we have transparency support if we need it
if has_alpha and not format.has_alpha:
raise FxconvError(f"'{input}' has transparency, which {format_name} "+
"doesn't support")
#---
# Structure generation
#---
data, stride, palette, color_count = image_encode(img, format)
o = ObjectData()
o += u8(format.id)
o += u8(3) # DATA_RO, PALETTE_RO
o += u16(color_count)
o += u16(img.width)
o += u16(img.height)
o += u32(stride)
o += ref(data, padding=4)
if palette is None:
o += u32(0)
else:
raise FxconvError(f"unknown color profile '{name}'")
if alpha is not None and not profile.supports_alpha:
raise FxconvError(f"'{input}' has transparency; use r5g6b5a, p8 or p4")
w, h, a = img.width, img.height, alpha or 0x0000
header = bytearray([
0x00, profile.id, # Profile identification
a >> 8, a & 0xff, # Alpha color
w >> 8, w & 0xff, # Width
h >> 8, h & 0xff, # Height
])
return header + encoded
o += ref(palette)
return o
#
# Font conversion
@ -677,7 +721,7 @@ def convert_topti(input, params):
line_height = int(params.get("height", grid.h))
# Default character spacing to 1
char_spacing = params.get("char-spacing", 1)
char_spacing = int(params.get("char-spacing", "1"))
#--
# Encoding blocks
@ -815,11 +859,12 @@ def convert_libimg_cg(input, params):
img = img.crop(area.tuple())
# Encode the image into 16-bit format and force the alpha to 0x0001
encoded, alpha = r5g6b5(img, alpha=(0x0001,0x0000))
format = CgProfile.find("rgb565a")
encoded, stride, palette, color_count = image_encode(img, format)
o = ObjectData()
o += u16(img.width) + u16(img.height)
o += u16(img.width) + u8(LIBIMG_FLAG_RO) + bytes(1)
o += u16(stride // 2) + u8(LIBIMG_FLAG_RO) + bytes(1)
o += ref(encoded)
return o
@ -901,155 +946,141 @@ def quantize(img, dither=False):
return img
def r5g6b5(img, color_count=0, alpha=None):
def image_encode(img, format):
"""
Convert a PIL.Image.Image into an R5G6B5 byte stream. If there are
transparent pixels, chooses a color to implement alpha and replaces them
with this color.
Encodes a PIL.Image.Image into one of the fx-CG image formats. The color
depth is either RGB16, P8 or P4, with various transparency settings and
palette encodings.
Returns the converted image as a bytearray and the alpha value, or None if
no alpha value was used.
If color_count is provided, it should be either 16 or 256. The image is
encoded with a palette of this size. Returns the converted image as a
bytearray, the palette as a bytearray, and the alpha value (None if there
were no transparent pixels).
If alpha is provided, it should be a pair (alpha value, replacement).
Trandarpent pixels will be encoded with the specified alpha value and
pixels with the value will be encoded with the replacement.
Returns 4 values:
* data: A bytearray containing the encoded image data
* stride: The byte stride of the data array
* palette: A bytearray containing the encoded palette (None if not indexed)
* color_count: Number of colors in the palette (-1 if not indexed)
"""
def rgb24to16(r, g, b):
r = (r & 0xff) >> 3
g = (g & 0xff) >> 2
b = (b & 0xff) >> 3
return (r << 11) | (g << 5) | b
#---
# Separate the alpha channel
#---
# Save the alpha channel and make it 1-bit
try:
# Save the alpha channel and make it 1-bit. If there are transparent
# pixels, set has_alpha=True and record the alpha channel in alpha_pixels.
if format.has_alpha:
alpha_channel = img.getchannel("A").convert("1", dither=Image.NONE)
alpha_levels = { t[1]: t[0] for t in alpha_channel.getcolors() }
has_alpha = 0 in alpha_levels
replacement = None
else:
alpha_channel = Image.new("1", img.size, 1)
if has_alpha:
alpha_pixels = alpha_channel.load()
except ValueError:
has_alpha = False
# Convert the input image to RGB
alpha_pixels = alpha_channel.load()
img = img.convert("RGB")
# Optionally convert to palette
if color_count:
palette_size = color_count - int(has_alpha)
img = img.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE,
colors=palette_size)
palette = img.getpalette()
# Transparent pixels have random values on the RGB channels, causing them
# to use up palette entries during quantization. To avoid that, set their
# RGB data to a color used somewhere else in the image.
pixels = img.load()
# Choose an alpha color
if alpha is not None:
alpha, replacement = alpha
elif color_count > 0:
# Transparency is mapped to the last palette element, if there are no
# transparent pixels then select an index out of bounds.
alpha = color_count - 1 if has_alpha else 0xffff
elif has_alpha:
# Compute the set of all used R5G6B5 colors
colormap = set()
for y in range(img.height):
for x in range(img.width):
if alpha_pixels[x, y] > 0:
colormap.add(rgb24to16(*pixels[x, y]))
# Choose an alpha color among the unused ones
available = set(range(65536)) - colormap
if not available:
raise FxconvError("image uses all 65536 colors and alpha")
alpha = available.pop()
else:
alpha = None
def alpha_encoding(color, a):
if a > 0:
if color == alpha:
return replacement
else:
return color
else:
return alpha
# Create a byte array with all encoded pixels
pixel_count = img.width * img.height
if not color_count:
size = pixel_count * 2
elif color_count == 256:
size = pixel_count
elif color_count == 16:
size = (pixel_count + 1) // 2
# Result of encoding
encoded = bytearray(size)
# Number of pixels encoded so far
entries = 0
# Offset into the array
offset = 0
bg_color = next((pixels[x,y]
for x in range(img.width) for y in range(img.height)
if alpha_pixels[x,y] > 0),
(0,0,0))
for y in range(img.height):
for x in range(img.width):
a = alpha_pixels[x, y] if has_alpha else 0xff
if alpha_pixels[x,y] == 0:
pixels[x,y] = bg_color
if not color_count:
c = alpha_encoding(rgb24to16(*pixels[x, y]), a)
encoded[offset] = c >> 8
encoded[offset+1] = c & 0xff
offset += 2
#---
# Quantize to generate a palette
#---
elif color_count == 16:
c = alpha_encoding(pixels[x, y], a)
if format.is_indexed:
palette_max_size = format.color_count - int(format.has_alpha)
img = img.convert("P",
dither=Image.NONE,
palette=Image.ADAPTIVE,
colors=palette_max_size)
# Aligned pixels: left 4 bits = high 4 bits of current byte
if (entries % 2) == 0:
encoded[offset] |= (c << 4)
# Unaligned pixels: right 4 bits of current byte
# The palette format is a list of N triplets where N includes both the
# opaque colors we just generated and an alpha color. Sometimes colors
# after img.convert() are not numbered 0..N-1, so take the max.
pixels = img.load()
N = 1 + max(pixels[x,y]
for y in range(img.height) for x in range(img.width))
palette = img.getpalette()[:3*N]
palette = list(zip(palette[::3], palette[1::3], palette[2::3]))
# For formats with transparency, make the transparent color consistent
if format.has_alpha:
N += 1
palette = [(255, 0, 255)] + palette
# Also keep track of how to remap indices from the values generated by
# img.convert() into the palette, which is shifted by 1 due to alpha
# and also starts at format.palette_base. Note: format.palette_base
# already starts 1 value later for formats with alpha.
palette_map = [(format.palette_base + i) % format.color_count
for i in range(N)]
else:
px = img.load()
#---
# Encode data into a bytearray
#---
def rgb24to16(rgb):
r = (rgb[0] & 0xff) >> 3
g = (rgb[1] & 0xff) >> 2
b = (rgb[2] & 0xff) >> 3
return (r << 11) | (g << 5) | b
if format.depth == IMAGE_RGB16:
# Preserve alignment between rows by padding to 4 bytes
stride = (img.width + 1) // 2 * 4
size = stride * img.height
elif format.depth == IMAGE_P8:
size = img.width * img.height
stride = img.width
elif format.depth == IMAGE_P4:
# Pad whole bytes
stride = (img.width + 1) // 2
size = stride * img.height
# Encode the pixel data
data = bytearray(size)
for y in range(img.height):
for x in range(img.width):
a = alpha_pixels[x,y]
if format.depth == IMAGE_RGB16:
# If c lands on the alpha value, flip its lowest bit
c = rgb24to16(pixels[x, y])
c = format.alpha if (a == 0) else c ^ (c == format.alpha)
offset = (stride * y) + x * 2
data[offset:offset+2] = u16(c)
elif format.depth == IMAGE_P8:
c = palette_map[pixels[x, y]] if a > 0 else format.alpha
offset = (stride * y) + x
data[offset] = c
elif format.depth == IMAGE_P4:
c = palette_map[pixels[x, y]] if a > 0 else format.alpha
offset = (stride * y) + (x // 2)
if x % 2 == 0:
data[offset] |= (c << 4)
else:
encoded[offset] |= c
offset += 1
data[offset] |= c
elif color_count == 256:
c = alpha_encoding(pixels[x, y], a)
encoded[offset] = c
offset += 1
# Encode the palette
entries += 1
if not color_count:
return encoded, alpha
# Encode the palette as R5G6B5
encoded_palette = bytearray(2 * color_count)
for c in range(color_count - int(has_alpha)):
r, g, b = palette[3*c], palette[3*c+1], palette[3*c+2]
rgb16 = rgb24to16(r, g, b)
encoded_palette[2*c] = rgb16 >> 8
encoded_palette[2*c+1] = rgb16 & 0xff
return encoded, encoded_palette, alpha
if format.is_indexed:
N = N if format.trim_palette else format.color_count
encoded_palette = bytearray(2 * N)
for i, rgb24 in enumerate(palette):
encoded_palette[2*i:2*i+2] = u16(rgb24to16(rgb24))
return data, stride, encoded_palette, N
else:
return data, stride, None, -1
def convert(input, params, target, output=None, model=None, custom=None):
"""
@ -1073,11 +1104,7 @@ def convert(input, params, target, output=None, model=None, custom=None):
if output is None:
output = os.path.splitext(input)[0] + ".o"
if "name" in params:
pass
elif "name_regex" in params:
params["name"] = re.sub(*params["name_regex"], os.path.basename(input))
else:
if "name" not in params:
raise FxconvError(f"no name specified for conversion '{input}'")
if target["arch"] is None:
@ -1159,20 +1186,6 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None,
Produces an output file and returns nothing.
"""
# Unfold ObjectData into data and assembly
if isinstance(data, ObjectData):
asm = ".section .rodata\n"
asm += f".global {symbol}\n"
asm += f"{symbol}:\n"
asm += data.link(symbol)[0]
asm += (assembly or "")
data = None
assembly = asm
if data is None and assembly is None:
raise FxconvError("elf() but no data and no assembly")
# Toolchain parameters
if toolchain is None:
@ -1193,6 +1206,20 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None,
raise FxconvError(f"non-trivial architecture for {toolchain} must be "+
"specified")
# Unfold ObjectData into data and assembly
if isinstance(data, ObjectData):
asm = ".section " + section.split(",",1)[0] + "\n"
asm += f".global {symbol}\n"
asm += f"{symbol}:\n"
asm += data.link(symbol)[0]
asm += (assembly or "")
data = None
assembly = asm
if data is None and assembly is None:
raise FxconvError("elf() but no data and no assembly")
# Generate data - in <output> directly if there is no assembly
if data:
@ -1247,3 +1274,118 @@ def elf(data, output, symbol, toolchain=None, arch=None, section=None,
fp_obj.close()
if assembly:
fp_asm.close()
#
# Meta API
#
def parse_parameters(params):
"""Parse parameters of the form "NAME:VALUE" into a dictionary."""
d = dict()
def insert(d, path, value):
if len(path) == 1:
d[path[0]] = value
else:
if not path[0] in d:
d[path[0]] = dict()
insert(d[path[0]], path[1:], value)
for decl in params:
if ":" not in decl:
raise FxconvError(f"invalid parameter {decl}, ignoring")
else:
name, value = decl.split(":", 1)
value = value.strip()
if name == "name_regex":
value = value.split(" ", 1)
insert(d, name.split("."), value)
return d
def _parse_metadata(contents):
"""
Parse the contents of an fxconv-metadata.txt file. Comments start with '#'
anywhere on a line and extend to the end of the line.
The file is divided in blocks that start with a "<wildcard>:" pattern at
the first column of a line (no leading spaces) followed by zero or more
properties declared as "key: value" (with at least one leading space).
The key can contain dots (eg. "category.field"), in which case the value
for the main component ("category") is itself a dictionary.
"""
RE_COMMENT = re.compile(r'#.*$', re.MULTILINE)
contents = re.sub(RE_COMMENT, "", contents)
RE_WILDCARD = re.compile(r'^(\S(?:[^:\s]|\\:|\\ )*)\s*:\s*$', re.MULTILINE)
lead, *elements = [ s.strip() for s in re.split(RE_WILDCARD, contents) ]
if lead:
raise FxconvError(f"invalid metadata: {lead} appears before wildcard")
# Group elements by pairs (left: wildcard, right: list of properties)
elements = list(zip(elements[::2], elements[1::2]))
metadata = []
for (wildcard, params) in elements:
params = [ s.strip() for s in params.split("\n") if s.strip() ]
metadata.append((wildcard, parse_parameters(params)))
return metadata
class Metadata:
def __init__(self, path=None, text=None):
"""
Load either an fxconv-metadata.txt file (if path is not None) or the
contents of such a file (if text is not None).
"""
if (path is not None) == (text is not None):
raise ValueError("Metadata must have exactly one of path and text")
if path is not None:
self._path = path
with open(path, "r") as fp:
self._rules = _parse_metadata(fp.read())
elif text is not None:
self._path = None
self._rules = _parse_metadata(text)
def path(self):
"""
Returns the path of the file from which the metadata was parsed, or
None if the metadata was parsed from string.
"""
return self._path
def rules(self):
"""
Returns a list of pairs (wildcard, rules) where the wildcard is a
string and the rules are a nested dictionary.
"""
return self._rules
def rules_for(self, path):
"""
Returns the parameters that apply to the specified path, or None if no
wildcard matches it. The special key "name_regex" is also resolved into
the regular key "name".
"""
basename = os.path.basename(path)
params = dict()
matched = False
for (wildcard, p) in self._rules:
if fnmatch.fnmatchcase(basename, wildcard):
params.update(**p)
matched = True
if not matched:
return None
if "name_regex" in params and not "name" in params:
params["name"] = re.sub(*params["name_regex"], basename)
return params

View File

@ -1,146 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <endianness.h>
#include <fxg1a.h>
#include <g1a.h>
/* check(): Check validity of a g1a control or fixed field
This function checks a single field of a g1a header (depending on the value
of @test, from 0 up) and returns:
* 0 if the field is valid
* 1 if there is a minor error (wrong fixed-byte entry)
* 2 if there is a major error (like not a g1a, bad checksum, etc)
* -1 if the value of @test is out of bounds
It produces a description of the check in @status (even if the test is
passed); the string should have room for at least 81 bytes.
@test Test number
@g1a G1A file being manipulated
@size File size
@status Array row, at least 81 bytes free */
static int check(int test, struct g1a const *g1a, size_t size, char *status)
{
#define m(msg, ...) sprintf(status, msg, ##__VA_ARGS__)
struct header const *h = &g1a->header;
uint8_t const *raw = (void *)h;
uint16_t sum;
uint8_t ctrl;
switch(test)
{
case 0:
m("Signature \"USBPower\" \"########\"");
strncpy(status + 28, h->magic, 8);
return strncmp(h->magic, "USBPower", 8) ? 2:0;
case 1:
m("MCS Type 0xf3 0x%02x", h->mcs_type);
return (h->mcs_type != 0xf3) ? 2:0;
case 2:
m("Sequence 1 0x0010001000 0x%02x%02x%02x%02x%02x",
h->seq1[0], h->seq1[1], h->seq1[2], h->seq1[3], h->seq1[4]);
return strncmp((const char *)h->seq1, "\x00\x01\x00\x01\x00",
5) ? 1:0;
case 3:
ctrl = raw[0x13] + 0x41;
m("Control 1 0x%02x 0x%02x", ctrl, h->control1);
return (h->control1 != ctrl) ? 2:0;
case 4:
m("Sequence 2 0x01 0x%02x", h->seq2);
return (h->seq2 != 0x01) ? 1:0;
case 5:
m("File size 1 %-8zu %u", size,
be32toh(h->filesize_be1));
return (be32toh(h->filesize_be1) != size) ? 2:0;
case 6:
ctrl = raw[0x13] + 0xb8;
m("Control 2 0x%02x 0x%02x", ctrl, h->control2);
return (h->control2 != ctrl) ? 2:0;
case 7:
sum = checksum(g1a, size);
m("Checksum 0x%02x 0x%02x", sum,
be16toh(h->checksum));
return (be16toh(h->checksum) != sum) ? 2:0;
case 8:
m("File size 2 %-8zu %u", size,
be32toh(h->filesize_be2));
return (be32toh(h->filesize_be2) != size) ? 2:0;
default:
return -1;
}
}
/* unknown(): Print an unknown field
@data Address of field
@offset Offset of field in header
@size Number of consecutive unknown bytes */
static void unknown(uint8_t const *data, size_t offset, size_t size)
{
printf(" 0x%03zx %-4zd 0x", offset, size);
for(size_t i = 0; i < size; i++) printf("%02x", data[offset + i]);
printf("\n");
}
/* field(): Print a text field with limited size
@field Address of text field
@size Maximum number of bytes to print */
static void field(const char *field, size_t size)
{
for(size_t i = 0; i < size && field[i]; i++) putchar(field[i]);
printf("\n");
}
/* dump(): Print the detailed header fields of a g1a file */
void dump(struct g1a const *g1a, size_t size)
{
struct header const *header = &g1a->header;
uint8_t const *raw = (void *)header;
/* Checks for g1a files */
char status[81];
int ret = 0;
int passed = 0;
printf("G1A signature checks:\n\n");
printf(" Sta. Field Expected Value\n");
for(int test = 0; ret >= 0; test++)
{
ret = check(test, g1a, size, status);
passed += !ret;
if(ret < 0) break;
printf(" %s %s\n", ret ? "FAIL" : "OK ", status);
}
printf("\nFields with unknown meanings:\n\n");
printf(" Offset Size Value\n");
unknown(raw, 0x015, 1);
unknown(raw, 0x018, 6);
unknown(raw, 0x028, 3);
unknown(raw, 0x02c, 4);
unknown(raw, 0x03a, 2);
unknown(raw, 0x04a, 2);
unknown(raw, 0x1d0, 4);
unknown(raw, 0x1dc, 20);
unknown(raw, 0x1f4, 12);
printf("\nApplication metadata:\n\n");
printf(" Program name: ");
field(header->name, 8);
printf(" Internal name: ");
field(header->internal, 8);
printf(" Version: ");
field(header->version, 10);
printf(" Build date: ");
field(header->date, 14);
printf("\nProgram icon:\n\n");
icon_print(header->icon);
}

View File

@ -1,72 +0,0 @@
#include <fxg1a.h>
#include <stdio.h>
#include <string.h>
#include <endianness.h>
/* sign(): Sign header by filling fixed fields and checksums */
void sign(struct g1a *g1a, size_t size)
{
struct header *header = &g1a->header;
/* Fixed elements */
memcpy(header->magic, "USBPower", 8);
header->mcs_type = 0xf3;
memcpy(header->seq1, "\x00\x10\x00\x10\x00", 5);
header->seq2 = 0x01;
header->filesize_be1 = htobe32(size);
header->filesize_be2 = htobe32(size);
/* Control bytes and checksums */
header->control1 = size + 0x41;
header->control2 = size + 0xb8;
header->checksum = htobe16(checksum(g1a, size));
}
/* edit_name(): Set application name */
void edit_name(struct g1a *g1a, const char *name)
{
memset(g1a->header.name, 0, 8);
if(!name) return;
for(int i = 0; name[i] && i < 8; i++)
g1a->header.name[i] = name[i];
}
/* edit_internal(): Set internal name */
void edit_internal(struct g1a *g1a, const char *internal)
{
memset(g1a->header.internal, 0, 8);
if(!internal) return;
for(int i = 0; internal[i] && i < 8; i++)
g1a->header.internal[i] = internal[i];
}
/* edit_version(): Set version string */
void edit_version(struct g1a *g1a, const char *version)
{
memset(g1a->header.version, 0, 10);
if(!version) return;
for(int i = 0; version[i] && i < 10; i++)
g1a->header.version[i] = version[i];
}
/* edit_date(): Set build date */
void edit_date(struct g1a *g1a, const char *date)
{
memset(g1a->header.date, 0, 14);
if(!date) return;
for(int i = 0; date[i] && i < 14; i++)
g1a->header.date[i] = date[i];
}
/* edit_icon(): Set icon from monochrome bitmap */
void edit_icon(struct g1a *g1a, uint8_t const *mono)
{
memcpy(g1a->header.icon, mono, 68);
}

View File

@ -1,176 +0,0 @@
//---
// fxg1a:fxg1a - Main interfaces
//---
#ifndef FX_FXG1A
#define FX_FXG1A
#include <stdlib.h>
#include <stdint.h>
#include <stddef.h>
#include <g1a.h>
/*
** Header dumping (dump.c)
*/
/* dump(): Print the detailed header fields of a g1a file
This function takes as argument the full file loaded into memory and the
size of the file. It does various printing to stdout as main job.
@g1a Full file data
@size Size of g1a file */
void dump(struct g1a const *g1a, size_t size);
/*
** Header manipulation (edit.c)
*/
/* sign(): Sign header by filling fixed fields and checksums
This function fills the fixed fields and various checksums of a g1a file. To
do this it accesses some of the binary data. To set the user-customizable
field, use the edit_*() functions. (The value of the customizable fields
does not influence the checksums so it's okay to not call this function
afterwards.)
@g1a Header to sign
@size Size of raw file data */
void sign(struct g1a *g1a, size_t size);
/* edit_*(): Set various fields of a g1a header */
void edit_name (struct g1a *g1a, const char *name);
void edit_internal (struct g1a *g1a, const char *internal);
void edit_version (struct g1a *g1a, const char *version);
void edit_date (struct g1a *g1a, const char *date);
/* edit_icon(): Set monochrome icon of a g1a header
The icon parameter must be loaded in 1-bit bitmap format. */
void edit_icon(struct g1a *header, uint8_t const *mono);
/*
** Utility functions (util.c)
*/
/* checksum(): Sum of 8 big-endian shorts at 0x300
Computes the third checksum by summing bytes from the data part of the file.
@g1a Add-in file whose checksum is requested
@size Size of file */
uint16_t checksum(struct g1a const *g1a, size_t size);
/* default_output(): Calculate default output file name
This function computes a default file name by replacing the extension of
@name (if it exists) or adding one. The extension is specified as a suffix,
usually in the form ".ext".
The resulting string might be as long as the length of @name plus that of
@suffix (plus one NUL byte); the provided buffer must point to a suitably-
large allocated space.
@name Input file name
@suffix Suffix to add or replace @name's extension with
@output Output file name */
void default_output(const char *name, const char *suffix, char *output);
/* default_internal(): Calculate default internal name
This function determines a default internal name, which is '@' followed by
at most 7 uppercase letters taken from the application name.
@name Application name
@output Internal name string (9 bytes) */
void default_internal(const char *name, char *output);
/*
** File manipulation (file.c)
*/
/* load_g1a(): Load a g1a file into memory
This function loads @filename into a dynamically-allocated buffer and
returns the address of that buffer; it must be free()'d after use. When
loading the file, if @size is not NULL, it receives the size of the file.
On error, load() prints a message an stderr and returns NULL. The header
is inverted before this function returns.
@filename File to load
@size If non-NULL, receives the file size
Returns a pointer to a buffer with loaded data, or NULL on error. */
struct g1a *load_g1a(const char *filename, size_t *size);
/* load_binary(): Load a binary file into memory
This function operates like load_g1a() but reserves space for an empty
header. The header is initialized with all zeros.
@filename File to load
@size If non-NULL, receives the file size
Returns a pointer to a buffer with loaded data, or NULL on error. */
struct g1a *load_binary(const char *filename, size_t *size);
/* save_g1a(): Save a g1a file to disk
This functions creates @filename, then writes a g1a header and a chunk of
raw data to it. Since it temporarily inverts the header to comply with
Casio's obfuscated format, it needs write access to @g1a. Returns non-zero
on error.
@filename File to write (it will be overridden if it exists)
@g1a G1A data to write
@size Size of data
Returns zero on success and a nonzero error code otherwise. */
int save_g1a(const char *filename, struct g1a *g1a, size_t size);
/*
** Icon management (icon.c)
*/
/* icon_print(): Show a monochrome 30*17 icon on stdout
The buffer should point to a 68-byte array. */
void icon_print(uint8_t const *icon);
/* icon_load(): Load a monochrome PNG image into an array
This function loads a PNG image into a 1-bit buffer; each row is represented
by a fixed number of bytes, each byte being 8 pixels. Rows are loaded from
top to bottom, and from left to right.
If the image is not a PNG image or a reading error occurs, this functions
prints an error message on stderr and returns NULL.
@filename PNG file to load
@width If non-NULL, receives image width
@height If non-NULL, receives image height
Returns a pointer to a free()able buffer with loaded data, NULL on error. */
uint8_t *icon_load(const char *filename, size_t *width, size_t *height);
/* icon_save(): Save an 8-bit array to a PNG image
Assumes 8-bit GRAY format.
@filename Target filename
@input An 8-bit GRAY image
@width Width of input, should be equal to stride
@height Height of input
Returns non-zero on error. */
int icon_save(const char *filename, uint8_t *input, size_t width,
size_t height);
/* icon_conv_8to1(): Convert an 8-bit icon to 1-bit
The returned 1-bit icon is always of size 30*17, if the input size does not
match it is adjusted.
@input 8-bi data
@width Width of input image
@height Height of input image
Returns a free()able buffer with a 1-bit icon on success, NULL on error. */
uint8_t *icon_conv_8to1(uint8_t const *input, size_t width, size_t height);
/* icon_conv_1to8(): Convert an 1-bit icon to 8-bit
Input 1-bit is assumed to be 30*17 in size, this function returns an 8-bit
buffer with the same dimensions.
@mono Input monochrome icon (from a g1a header, for instance)
Returns a free()able buffer, or NULL on error. */
uint8_t *icon_conv_1to8(uint8_t const *mono);
#endif /* FX_FXG1A */

View File

@ -1,59 +0,0 @@
//---
// fxg1a:g1a - Add-in header for Casio's G1A format
//---
#ifndef FX_G1A
#define FX_G1A
#include <stdint.h>
/* TODO: eStrips are not supported yet */
struct estrip
{
uint8_t data[80];
};
/* G1A file header with 0x200 bytes. When output to a file the standard part
(first 0x20 bytes) of this header is bit-inverted, but the non-inverted
version makes a lot more sens so we'll be using it. */
struct header
{ /* Offset Size Value */
char magic[8]; /* 0x000 8 "USBPower" */
uint8_t mcs_type; /* 0x008 1 0xf3 (AddIn) */
uint8_t seq1[5]; /* 0x009 5 0x0010001000 */
uint8_t control1; /* 0x00e 1 *0x13 + 0x41 */
uint8_t seq2; /* 0x00f 1 0x01 */
uint32_t filesize_be1; /* 0x010 4 File size, big endian */
uint8_t control2; /* 0x014 1 *0x13 + 0xb8 */
uint8_t _1; /* 0x015 1 ??? */
uint16_t checksum; /* 0x016 2 BE sum of 8 shorts at 0x300 */
uint8_t _2[6]; /* 0x018 6 ??? */
uint16_t mcs_objects; /* 0x01e 2 MCS-only, unused */
char internal[8]; /* 0x020 8 Internal app name with '@' */
uint8_t _3[3]; /* 0x028 3 ??? */
uint8_t estrips; /* 0x02b 1 Number of estrips (0..4) */
uint8_t _4[4]; /* 0x02c 4 ??? */
char version[10]; /* 0x030 10 Version "MM.mm.pppp" */
uint8_t _5[2]; /* 0x03a 2 ??? */
char date[14]; /* 0x03c 14 Build date "yyyy.MMdd.hhmm" */
uint8_t _6[2]; /* 0x04a 2 ??? */
uint8_t icon[68]; /* 0x04c 68 30*17 monochrome icon */
struct estrip estrip1; /* 0x090 80 eStrip 1 */
struct estrip estrip2; /* 0x0e0 80 eStrip 2 */
struct estrip estrip3; /* 0x130 80 eStrip 3 */
struct estrip estrip4; /* 0x180 80 eStrip 4 */
uint8_t _7[4]; /* 0x1d0 4 ??? */
char name[8]; /* 0x1d4 8 Add-in name */
uint8_t _8[20]; /* 0x1dc 20 ??? */
uint32_t filesize_be2; /* 0x1f0 4 File size, big endian */
uint8_t _9[12]; /* 0x1f4 12 ??? */
};
/* A full g1a file, suitable for use with pointers */
struct g1a
{
struct header header;
uint8_t code[];
};
#endif /* FX_G1A */

View File

@ -1,132 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <fxg1a.h>
#include <png.h>
/* icon_print(): Show a monochrome 30*17 icon on stdout */
void icon_print(uint8_t const *icon)
{
for(int y = 0; y < 17; y++)
{
for(int x = 0; x < 30; x++)
{
int v = icon[(y << 2) + (x >> 3)] & (0x80 >> (x & 7));
putchar(v ? '#' : ' ');
putchar(v ? '#' : ' ');
}
putchar('\n');
}
}
/* icon_load(): Load a monochrome PNG image into an array */
uint8_t *icon_load(const char *filename, size_t *width, size_t *height)
{
png_image img;
memset(&img, 0, sizeof img);
img.opaque = NULL;
img.version = PNG_IMAGE_VERSION;
png_image_begin_read_from_file(&img, filename);
if(img.warning_or_error)
{
fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1
? "warning": "error", img.message);
if(img.warning_or_error > 1)
{
png_image_free(&img);
return NULL;
}
}
img.format = PNG_FORMAT_GRAY;
void *buffer = calloc(img.width * img.height, 1);
if(!buffer)
{
fprintf(stderr, "error: cannot read %s: %m\n", filename);
png_image_free(&img);
return NULL;
}
png_image_finish_read(&img, NULL, buffer, img.width, NULL);
if(width) *width = img.width;
if(height) *height = img.height;
png_image_free(&img);
return buffer;
}
/* icon_save(): Save an 8-bit array to a PNG image */
int icon_save(const char *filename, uint8_t *input, size_t width,
size_t height)
{
png_image img;
memset(&img, 0, sizeof img);
img.version = PNG_IMAGE_VERSION;
img.width = width;
img.height = height;
img.format = PNG_FORMAT_GRAY;
png_image_write_to_file(&img, filename, 0, input, 0, NULL);
png_image_free(&img);
if(img.warning_or_error)
{
fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1
? "warning": "error", img.message);
if(img.warning_or_error > 1) return 1;
}
return 0;
}
/* icon_conv_8to1(): Convert an 8-bit icon to 1-bit */
uint8_t *icon_conv_8to1(uint8_t const *input, size_t width, size_t height)
{
if(!input) return NULL;
uint8_t *mono = calloc(68, 1);
if(!mono) return NULL;
size_t stride = width;
/* If the image is wider than 30 pixels, ignore columns at the right */
if(width > 30) width = 30;
/* Skip the first line if there is enough rows, because in standard
30*19 icons, the first and last lines are skipped */
if(height > 17) input += stride, height--;
/* If height is still larger than 17, ignore rows at the bottom */
if(height > 17) height = 17;
/* Then copy individual pixels on the currently-blank image */
for(size_t y = 0; y < height; y++)
for(size_t x = 0; x < width; x++)
{
int offset = (y << 2) + (x >> 3);
int color = input[y * stride + x] < 128;
uint8_t mask = color << (~x & 7);
mono[offset] |= mask;
}
return mono;
}
/* icon_conv_1to8(): Convert an 1-bit icon to 8-bit */
uint8_t *icon_conv_1to8(uint8_t const *mono)
{
uint8_t *data = calloc(30 * 17, 1);
if(!data) return NULL;
for(int y = 0; y < 17; y++)
for(int x = 0; x < 30; x++)
{
int offset = (y << 2) + (x >> 3);
int bit = mono[offset] & (0x80 >> (x & 7));
data[y * 30 + x] = (bit ? 0x00 : 0xff);
}
return data;
}

View File

@ -1,272 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <getopt.h>
#include <fxg1a.h>
#include <g1a.h>
static const char *help_string =
"usage: %1$s [-g] <binary file> [options...]\n"
" %1$s -e <g1a file> [options...]\n"
" %1$s -d <g1a file>\n"
" %1$s -r <g1a file> [-o <g1a file>]\n"
" %1$s -x <g1a file> [-o <png file>]\n"
"\n"
"fxg1a creates or edits g1a files (add-in applications for Casio fx9860g\n"
"calculator series) that consist of a g1a header followed by binary code.\n"
"\n"
"Operating modes:\n"
" -g, --g1a Generate a g1a file (default)\n"
" -e, --edit Edit header of an existing g1a file\n"
" -d, --dump Dump header of an existing g1a file\n"
" -r, --repair Recalculate control bytes and checksums\n"
" -x, --extract Extract icon into a PNG file\n"
"\n"
"General options:\n"
" -o, --output=<file> Output file (default: input file with .g1a suffix\n"
" [-g]; with .png suffix [-x]; input file [-e, -r])\n"
"\n"
"Generation and edition options:\n"
" -i, --icon=<png> Program icon, in PNG format (default: blank icon)\n"
" -n, --name=<name> Add-in name, 8 bytes (default: output file name)\n"
" --version=<text> Program version, MM.mm.pppp format (default: empty)\n"
" --internal=<name> Internal name, eg. '@NAME' (default: empty)\n"
" --date=<date> Date of build, yyyy.MMdd.hhmm (default: now)\n";
/*
** Field customization
*/
/* A set of user-defined fields, often taken on the command-line
Default values are NULL and indicate "no value" (-g) or "no change" (-e). */
struct fields
{
/* New values for basic fields */
const char *name;
const char *version;
const char *internal;
const char *date;
/* Icon file name */
const char *icon;
};
/* fields_edit(): Set the value of some fields altogether
@header Header to edit, is assumed checksumed and filled
@fields New values for fields, any members can be NULL */
void fields_edit(struct g1a *header, struct fields const *fields)
{
/* For easy fields, just call the appropriate edition function */
if(fields->name) edit_name(header, fields->name);
if(fields->version) edit_version(header, fields->version);
if(fields->internal) edit_internal(header, fields->internal);
if(fields->date) edit_date(header, fields->date);
/* Load icon from PNG file */
if(fields->icon)
{
size_t width, height;
uint8_t *data = icon_load(fields->icon, &width, &height);
if(!data) return;
uint8_t *mono = icon_conv_8to1(data, width, height);
free(data);
if(!mono) return;
edit_icon(header, mono);
}
}
/*
** Tool implementation
*/
int main(int argc, char **argv)
{
/* Result of option parsing */
int mode = 'g', error = 0;
struct fields fields = { 0 };
const char *output = NULL;
const struct option longs[] = {
{ "help", no_argument, NULL, 'h' },
{ "g1a", no_argument, NULL, 'g' },
{ "edit", no_argument, NULL, 'e' },
{ "dump", no_argument, NULL, 'd' },
{ "repair", no_argument, NULL, 'r' },
{ "extract", no_argument, NULL, 'x' },
{ "output", required_argument, NULL, 'o' },
{ "icon", required_argument, NULL, 'i' },
{ "name", required_argument, NULL, 'n' },
{ "version", required_argument, NULL, 'v' },
{ "internal", required_argument, NULL, 't' },
{ "date", required_argument, NULL, 'a' },
{ NULL, 0, NULL, 0 },
};
int option = 0;
while(option >= 0 && option != '?')
switch((option = getopt_long(argc, argv, "hgedrxo:i:n:", longs, NULL)))
{
case 'h':
fprintf(stderr, help_string, argv[0]);
return 0;
case 'g':
case 'e':
case 'd':
case 'r':
case 'x':
mode = option;
break;
case 'o':
output = optarg;
break;
case 'i':
fields.icon = optarg;
break;
case 'n':
fields.name = optarg;
break;
case 'v':
fields.version = optarg;
break;
case 't':
fields.internal = optarg;
break;
case 'a':
fields.date = optarg;
break;
case '?':
error = 1;
break;
}
if(error) return 1;
if(argv[optind] == NULL)
{
fprintf(stderr, help_string, argv[0]);
return 1;
}
if(mode == 'g')
{
/* Load binary file into memory */
size_t size;
struct g1a *g1a = load_binary(argv[optind], &size);
if(!g1a) return 1;
/* If [output] is set, use it, otherwise compute a default */
char *alloc = NULL;
if(!output)
{
alloc = malloc(strlen(argv[optind]) + 5);
if(!alloc) {fprintf(stderr, "error: %m\n"); return 1;}
default_output(argv[optind], ".g1a", alloc);
}
/* Start with output file name as application name */
edit_name(g1a, output ? output : alloc);
/* Start with "now" as build date */
char date[15];
time_t t = time(NULL);
struct tm *now = localtime(&t);
strftime(date, 15, "%Y.%m%d.%H%M", now);
edit_date(g1a, date);
/* Start with an uppercase name as internal name */
char internal[9];
default_internal(fields.name ? fields.name : g1a->header.name,
internal);
edit_internal(g1a, internal);
/* Edit the fields with user-customized values */
fields_edit(g1a, &fields);
/* Set fixed fields and calculate checksums */
sign(g1a, size);
save_g1a(output ? output : alloc, g1a, size);
free(alloc);
/* Write output file */
free(g1a);
}
if(mode == 'e')
{
/* Load g1a file into memory */
size_t size;
struct g1a *g1a = load_g1a(argv[optind], &size);
if(!g1a) return 1;
/* Edit the fields with user-customized values */
fields_edit(g1a, &fields);
/* We don't reset fixed fields or recalculate checksums because
we only want to edit what was requested by the user.
Besides, the control bytes and checksums do *not* depend on
the value of user-customizable fields. */
/* Regenerate input file, or output somewhere else */
if(!output) output = argv[optind];
save_g1a(output, g1a, size);
free(g1a);
}
if(mode == 'd')
{
/* Load and dump the g1a */
size_t size;
struct g1a *g1a = load_g1a(argv[optind], &size);
if(!g1a) return 1;
dump(g1a, size);
free(g1a);
}
if(mode == 'r')
{
/* Load g1a file into memory */
size_t size;
struct g1a *g1a = load_g1a(argv[optind], &size);
if(!g1a) return 1;
/* Repair file by recalculating fixed fields and checksums */
sign(g1a, size);
/* Regenerate input file, or output somewhere else */
if(!output) output = argv[optind];
save_g1a(output, g1a, size);
free(g1a);
}
if(mode == 'x')
{
/* Load g1a file into memory */
size_t size;
struct g1a *g1a = load_g1a(argv[optind], &size);
if(!g1a) return 1;
/* Generate 8-bit icon from g1a 1-bit */
uint8_t *data = icon_conv_1to8(g1a->header.icon);
if(!data)
{
fprintf(stderr, "error: %m\n");
return 1;
}
/* Calculate a default output name if none is provided */
if(output)
{
icon_save(output, data, 30, 17);
}
else
{
char *alloc = malloc(strlen(argv[optind]) + 5);
if(!alloc) {fprintf(stderr, "error: %m\n"); return 1;}
default_output(argv[optind], ".png", alloc);
icon_save(alloc, data, 30, 17);
free(alloc);
}
}
return 0;
}

View File

@ -1,63 +0,0 @@
#include <string.h>
#include <ctype.h>
#include <endianness.h>
#include <fxg1a.h>
/*
** Public API
*/
/* checksum(): Sum of 8 big-endian shorts at 0x300 */
uint16_t checksum(struct g1a const *g1a, size_t size)
{
uint16_t shorts[16] = { 0 };
/* Extract 16 bytes from the file (maybe less are available) */
int available = size - 0x300;
if(available < 0) available = 0;
if(available > 16) available = 16;
memcpy(shorts, g1a->code + 0x100, available);
/* Do the big-endian sum */
uint16_t sum = 0;
for(int i = 0; i < 8; i++) sum += htobe16(shorts[i]);
return sum;
}
/* default_output(): Calculate default output file name */
void default_output(const char *name, const char *suffix, char *output)
{
/* Check if there is a dot at the end of @name, before the last '/'.
The dot must also not be in first position (hidden files) */
size_t end = strlen(name) - 1;
while(end >= 1 && name[end] != '/' && name[end] != '.') end--;
/* If we don't have a dot in the file name, append the extension */
if(end < 1 || name[end] != '.')
{
strcpy(output, name);
strcat(output, suffix);
}
/* If we found a dot before the last slash, replace the extension */
else
{
memcpy(output, name, end);
strcpy(output + end, suffix);
}
}
/* default_internal(): Calculate default internal name */
void default_internal(const char *name, char *output)
{
output[0] = '@';
int i=1;
for(int j=0; name[j] && i < 8; j++)
{
if(isalpha(name[j])) output[i++] = toupper(name[j]);
}
output[i] = 0;
}

275
fxgxa/dump.c Normal file
View File

@ -0,0 +1,275 @@
#include <stdio.h>
#include <string.h>
#include <endianness.h>
#include <fxgxa.h>
#include <g1a.h>
/* check_g1a(): Check validity of a g1a control or fixed field
This function checks a single field of a g1a header (depending on the value
of @test, from 0 up) and returns:
* 0 if the field is valid
* 1 if there is a minor error (wrong fixed-byte entry)
* 2 if there is a major error (like not a g1a, bad checksum, etc)
* -1 if the value of @test is out of bounds
It produces a description of the check in @status (even if the test is
passed); the string should have room for at least 81 bytes.
@test Test number
@g1a G1A file being manipulated
@size File size
@status Array row, at least 81 bytes free */
#define m(msg, ...) sprintf(status, msg, ##__VA_ARGS__)
static int check_g1a(int test, struct g1a const *g1a, size_t size,char *status)
{
struct g1a_header const *h = &g1a->header;
uint8_t const *raw = (void *)h;
uint16_t sum;
uint8_t ctrl;
switch(test)
{
case 0:
m("Signature \"USBPower\" \"########\"");
strncpy(status + 28, h->magic, 8);
return strncmp(h->magic, "USBPower", 8) ? 2:0;
case 1:
m("MCS Type 0xf3 0x%02x", h->mcs_type);
return (h->mcs_type != 0xf3) ? 2:0;
case 2:
m("Sequence 1 0x0010001000 0x%02x%02x%02x%02x%02x",
h->seq1[0], h->seq1[1], h->seq1[2], h->seq1[3], h->seq1[4]);
return memcmp((const char *)h->seq1, "\x00\x10\x00\x10\x00",
5) ? 1:0;
case 3:
ctrl = raw[0x13] + 0x41;
m("Control 1 0x%02x 0x%02x", ctrl, h->control1);
return (h->control1 != ctrl) ? 2:0;
case 4:
m("Sequence 2 0x01 0x%02x", h->seq2);
return (h->seq2 != 0x01) ? 1:0;
case 5:
m("File size 1 %-8zu %u", size,
be32toh(h->filesize_be1));
return (be32toh(h->filesize_be1) != size) ? 2:0;
case 6:
ctrl = raw[0x13] + 0xb8;
m("Control 2 0x%02x 0x%02x", ctrl, h->control2);
return (h->control2 != ctrl) ? 2:0;
case 7:
sum = checksum_g1a(g1a, size);
m("Checksum 0x%02x 0x%02x", sum,
be16toh(h->checksum));
return (be16toh(h->checksum) != sum) ? 2:0;
case 8:
m("File size 2 %-8zu %u", size,
be32toh(h->filesize_be2));
return (be32toh(h->filesize_be2) != size) ? 2:0;
default:
return -1;
}
}
/* unknown(): Dump the contents of an unknown field */
static void unknown(uint8_t const *data, size_t offset, size_t size)
{
printf(" 0x%03zx %-4zd 0x", offset, size);
for(size_t i = 0; i < size; i++) printf("%02x", data[offset + i]);
printf("\n");
}
/* field(): Print a potentially not NUL-terminated text field */
static void field(const char *field, size_t size)
{
for(size_t i = 0; i < size && field[i]; i++) putchar(field[i]);
printf("\n");
}
void dump_g1a(struct g1a const *g1a, size_t size)
{
struct g1a_header const *header = &g1a->header;
uint8_t const *raw = (void *)header;
/* Checks for g1a files */
char status[81];
int ret=0, passed=0;
printf("G1A signature checks:\n\n");
printf(" Sta. Field Expected Value\n");
for(int test = 0; ret >= 0; test++)
{
ret = check_g1a(test, g1a, size, status);
passed += !ret;
if(ret < 0) break;
printf(" %s %s\n", ret ? "FAIL" : "OK ", status);
}
printf("\nFields with unknown meanings:\n\n");
printf(" Offset Size Value\n");
unknown(raw, 0x015, 1);
unknown(raw, 0x018, 6);
unknown(raw, 0x028, 3);
unknown(raw, 0x02c, 4);
unknown(raw, 0x03a, 2);
unknown(raw, 0x04a, 2);
unknown(raw, 0x1d0, 4);
unknown(raw, 0x1dc, 20);
unknown(raw, 0x1f4, 12);
printf("\nApplication metadata:\n\n");
printf(" Program name: ");
field(header->name, 8);
printf(" Internal name: ");
field(header->internal, 8);
printf(" Version: ");
field(header->version, 10);
printf(" Build date: ");
field(header->date, 14);
printf("\nProgram icon:\n\n");
icon_print_1(header->icon, 30, 17);
}
/* See check_g3a() for a description */
static int check_g3a(int test, struct g3a const *g3a, size_t size,char *status)
{
struct g3a_header const *h = &g3a->header;
uint8_t const *raw = (void *)h;
uint16_t sum;
uint32_t sum2;
uint8_t ctrl;
switch(test)
{
case 0:
m("Signature \"USBPower\" \"########\"");
strncpy(status + 28, h->magic, 8);
return strncmp(h->magic, "USBPower", 8) ? 2:0;
case 1:
m("MCS Type 0x2c 0x%02x", h->mcs_type);
return (h->mcs_type != 0x2c) ? 2:0;
case 2:
m("Sequence 1 0x0010001000 0x%02x%02x%02x%02x%02x",
h->seq1[0], h->seq1[1], h->seq1[2], h->seq1[3], h->seq1[4]);
return memcmp((const char *)h->seq1, "\x00\x01\x00\x01\x00",
5) ? 1:0;
case 3:
ctrl = raw[0x13] + 0x41;
m("Control 1 0x%02x 0x%02x", ctrl, h->control1);
return (h->control1 != ctrl) ? 2:0;
case 4:
m("Sequence 2 0x01 0x%02x", h->seq2);
return (h->seq2 != 0x01) ? 1:0;
case 5:
m("File size 1 %-8zu %u", size,
be32toh(h->filesize_be1));
return (be32toh(h->filesize_be1) != size) ? 2:0;
case 6:
ctrl = raw[0x13] + 0xb8;
m("Control 2 0x%02x 0x%02x", ctrl, h->control2);
return (h->control2 != ctrl) ? 2:0;
case 7:
sum = checksum_g3a(g3a, size);
m("Checksum 0x%02x 0x%02x", sum,
be16toh(h->checksum));
return (be16toh(h->checksum) != sum) ? 2:0;
case 8:
m("File size 2 %-8zu %u", size - 0x7004,
be32toh(h->filesize_be2));
return (be32toh(h->filesize_be2) != size - 0x7004) ? 2:0;
case 9:
sum2 = checksum_g3a_2(g3a, size);
m("Checksum 2 0x%08x 0x%08x", sum2,
be32toh(h->checksum_2));
return (be32toh(h->checksum_2) != sum2) ? 2:0;
case 10:
m("File size 1 %-8zu %u", size,
be32toh(h->filesize_be3));
return (be32toh(h->filesize_be3) != size) ? 2:0;
case 11:
sum2 = checksum_g3a_2(g3a, size);
uint32_t footer = be32toh(*(uint32_t *)((void *)g3a+size-4));
m("Footer 0x%08x 0x%08x", sum2, footer);
return (footer != sum2) ? 2:0;
default:
return -1;
}
}
void dump_g3a(struct g3a const *g3a, size_t size)
{
struct g3a_header const *header = &g3a->header;
uint8_t const *raw = (void *)header;
/* Checks for g3a files */
char status[81];
int ret=0, passed=0;
printf("G3A signature checks:\n\n");
printf(" Sta. Field Expected Value\n");
for(int test = 0; ret >= 0; test++)
{
ret = check_g3a(test, g3a, size, status);
passed += !ret;
if(ret < 0) break;
printf(" %s %s\n", ret ? "FAIL" : "OK ", status);
}
printf("\nFields with unknown meanings:\n\n");
printf(" Offset Size Value\n");
unknown(raw, 0x015, 1);
unknown(raw, 0x018, 6);
unknown(raw, 0x026, 8);
unknown(raw, 0x032, 14);
unknown(raw, 0x050, 12);
unknown(raw, 0x12c, 4);
unknown(raw, 0x13a, 2);
unknown(raw, 0x14a, 38);
printf(" 0x590 2348 ");
bool is_zeros = true;
for(int i = 0; i < 2348; i++)
if(raw[0x590+i] != 0) is_zeros =1;
printf(is_zeros ? "<All zeros>\n" : "<Not shown (non-zero)>\n");
printf("\nApplication metadata:\n\n");
printf(" Program name: ");
field(header->name, 16);
printf(" Internal name: ");
field(header->internal, 11);
printf(" Version: ");
field(header->version, 10);
printf(" Build date: ");
field(header->date, 14);
printf(" Filename: ");
field(header->filename, 324);
printf("\nUnselected program icon:\n\n");
icon_print_16(header->icon_uns, 92, 64);
printf("\nSelected program icon:\n\n");
icon_print_16(header->icon_sel, 92, 64);
printf("\n");
}
void dump(void *gxa, size_t size)
{
if(is_g1a(gxa))
return dump_g1a(gxa, size);
if(is_g3a(gxa))
return dump_g3a(gxa, size);
}

139
fxgxa/edit.c Normal file
View File

@ -0,0 +1,139 @@
#include <fxgxa.h>
#include <stdio.h>
#include <string.h>
#include <endianness.h>
void sign(void *gxa, size_t size)
{
if(is_g1a(gxa)) {
struct g1a_header *header = gxa;
/* Fixed elements */
memcpy(header->magic, "USBPower", 8);
header->mcs_type = 0xf3;
memcpy(header->seq1, "\x00\x10\x00\x10\x00", 5);
header->seq2 = 0x01;
header->filesize_be1 = htobe32(size);
header->filesize_be2 = htobe32(size);
/* Control bytes and checksums */
header->control1 = size + 0x41;
header->control2 = size + 0xb8;
header->checksum = htobe16(checksum_g1a(gxa, size));
}
else if(is_g3a(gxa)) {
struct g3a_header *header = gxa;
/* Fixed elements */
memcpy(header->magic, "USBPower", 8);
header->mcs_type = 0x2c;
memcpy(header->seq1, "\x00\x01\x00\x01\x00", 5);
header->seq2 = 0x01;
memcpy(header->seq3, "\x01\x01", 2);
header->filesize_be1 = htobe32(size);
header->filesize_be2 = htobe32(size - 0x7000 - 4);
header->filesize_be3 = htobe32(size);
/* Control bytes and checksums */
header->control1 = size + 0x41;
header->control2 = size + 0xb8;
header->checksum = htobe16(checksum_g3a(gxa, size));
header->checksum_2 = htobe32(checksum_g3a_2(gxa, size));
/* Last 4 bytes */
uint32_t *footer = gxa + size - 4;
*footer = header->checksum_2;
}
}
void edit_name(void *gxa, const char *name)
{
if(is_g1a(gxa)) {
memset(G1A(gxa)->header.name, 0, 8);
if(!name) return;
for(int i = 0; name[i] && i < 8; i++)
G1A(gxa)->header.name[i] = name[i];
}
else if(is_g3a(gxa)) {
memset(G3A(gxa)->header.name, 0, 16);
if(!name) return;
for(int i = 0; name[i] && i < 16; i++)
G3A(gxa)->header.name[i] = name[i];
for(int j = 0; j < 8; j++) {
memset(G3A(gxa)->header.label[j], 0, 24);
for(int i = 0; name[i] && i < 24; i++)
G3A(gxa)->header.label[j][i] = name[i];
}
}
}
void edit_internal(void *gxa, const char *internal)
{
char *dst = NULL;
int size = 0;
if(is_g1a(gxa)) {
dst = G1A(gxa)->header.internal;
size = 8;
}
else if(is_g3a(gxa)) {
dst = G3A(gxa)->header.internal;
size = 11;
}
memset(dst, 0, size);
if(!internal) return;
for(int i = 0; internal[i] && i < size; i++)
dst[i] = internal[i];
}
void edit_version(void *gxa, const char *version)
{
char *dst = NULL;
int size = 10;
if(is_g1a(gxa))
dst = G1A(gxa)->header.version;
else if(is_g3a(gxa))
dst = G3A(gxa)->header.version;
memset(dst, 0, size);
if(!version) return;
for(int i = 0; version[i] && i < size; i++)
dst[i] = version[i];
}
void edit_date(void *gxa, const char *date)
{
char *dst = NULL;
int size = 14;
if(is_g1a(gxa))
dst = G1A(gxa)->header.date;
else if(is_g3a(gxa))
dst = G3A(gxa)->header.date;
memset(dst, 0, size);
if(!date) return;
for(int i = 0; date[i] && i < size; i++)
dst[i] = date[i];
}
void edit_g1a_icon(struct g1a *g1a, uint8_t const *mono)
{
memcpy(g1a->header.icon, mono, 68);
}
void edit_g3a_icon(struct g3a *g3a, uint16_t const *icon, bool selected)
{
if(selected)
memcpy(g3a->header.icon_sel, icon, 92*64*2);
else
memcpy(g3a->header.icon_uns, icon, 92*64*2);
}

View File

@ -7,13 +7,13 @@
#include <stdlib.h>
#include <string.h>
#include <fxg1a.h>
#include <fxgxa.h>
/* invert_header(): Bit-invert a standard header
Part of the header is stored inverted in files for obfuscation purposes. */
static void invert_header(struct g1a *g1a)
static void invert_header(void *gxa)
{
uint8_t *data = (void *)&g1a->header;
uint8_t *data = gxa;
for(size_t i = 0; i < 0x20; i++) data[i] = ~data[i];
}
@ -26,7 +26,7 @@ static void invert_header(struct g1a *g1a)
/* load(): Fully load a file into memory
Allocates a buffer with @prepend leading bytes initialized to zero. */
static void *load(const char *filename, size_t *size, size_t prepend)
static void *load(const char *filename, size_t *size, int header, int footer)
{
int fd;
struct stat statbuf;
@ -40,13 +40,13 @@ static void *load(const char *filename, size_t *size, size_t prepend)
if(x > 0) fail("cannot stat %s", filename);
filesize = statbuf.st_size;
data = malloc(prepend + filesize);
data = malloc(header + filesize + footer);
if(!data) fail("cannot load %s", filename);
size_t remaining = filesize;
while(remaining > 0)
{
size_t offset = prepend + filesize - remaining;
size_t offset = header + filesize - remaining;
ssize_t y = read(fd, data + offset, remaining);
if(y < 0) fail("cannot read from %s", filename);
@ -54,58 +54,55 @@ static void *load(const char *filename, size_t *size, size_t prepend)
}
close(fd);
memset(data, 0, prepend);
memset(data, 0, header);
memset(data + header + filesize, 0, footer);
if(size) *size = prepend + filesize;
if(size) *size = header + filesize + footer;
return data;
}
/* load_g1a(): Load a g1a file into memory */
struct g1a *load_g1a(const char *filename, size_t *size)
void *load_gxa(const char *filename, size_t *size)
{
struct g1a *ret = load(filename, size, 0);
void *ret = load(filename, size, 0, 0);
if(ret) invert_header(ret);
return ret;
}
/* load_binary(): Load a binary file into memory */
struct g1a *load_binary(const char *filename, size_t *size)
void *load_binary(const char *filename, size_t *size, int header, int footer)
{
struct g1a *ret = load(filename, size, 0x200);
if(ret) memset(ret, 0xff, 0x20);
void *ret = load(filename, size, header, footer);
if(ret) invert_header(ret);
return ret;
}
#undef fail
#define fail(msg, ...) { \
fprintf(stderr, "error: " msg ": %m\n", ##__VA_ARGS__); \
close(fd); \
invert_header(g1a); \
return 1; \
rc = 1; \
goto end; \
}
/* save_g1a(): Save a g1a file to disk */
int save_g1a(const char *filename, struct g1a *g1a, size_t size)
int save_gxa(const char *filename, void *gxa, size_t size)
{
/* Invert header before saving */
invert_header(g1a);
invert_header(gxa);
int rc = 0;
int fd = creat(filename, 0644);
if(fd < 0) fail("cannot open %s", filename);
void const *raw = g1a;
ssize_t status;
size_t written = 0;
while(written < size)
{
status = write(fd, raw + written, size - written);
status = write(fd, gxa + written, size - written);
if(status < 0) fail("cannot write to %s", filename);
written += status;
}
close(fd);
end:
/* Before returning, re-invert header for further use */
invert_header(g1a);
return 0;
if(fd >= 0) close(fd);
invert_header(gxa);
return rc;
}

198
fxgxa/fxgxa.h Normal file
View File

@ -0,0 +1,198 @@
//---
// fxgxa:fxgxa - Main interfaces
//---
#ifndef FX_FXGXA
#define FX_FXGXA
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <g1a.h>
#include <g3a.h>
/* In this file, many functions accept either a [struct g1a] or a [struct g3a]
as their argument. In this case, the argument is noted [void *gxa]. */
/*
** Header dumping (dump.c)
*/
/* dump(): Print the detailed header fields of a file
This function takes as argument the full file loaded into memory and the
size of the file. It does various printing to stdout as main job.
@gxa Full file data
@size Size of the file */
void dump(void *gxa, size_t size);
/*
** Header manipulation (edit.c)
*/
/* sign(): Sign header by filling fixed fields and checksums
This function fills the fixed fields and various checksums of a g1a file. To
do this it accesses some of the binary data. To set the user-customizable
field, use the edit_*() functions. (The value of the customizable fields
does not influence the checksums so it's okay to not call this function
afterwards.)
@g1a Header to sign
@size Size of raw file data */
void sign(void *gxa, size_t size);
/* edit_*(): Set various fields */
void edit_name (void *gxa, const char *name);
void edit_internal (void *gxa, const char *internal);
void edit_version (void *gxa, const char *version);
void edit_date (void *gxa, const char *date);
/* edit_g1a_icon(): Set monochrome icon of a g1a header
The icon parameter must be loaded in 1-bit bitmap format. */
void edit_g1a_icon(struct g1a *header, uint8_t const *mono);
/* edit_g3a_icon(): Set one of the color icons of a g3a header
The icon must be loaded in RGB565 16-bit format. */
void edit_g3a_icon(struct g3a *header, uint16_t const *icon, bool selected);
/*
** Utility functions (util.c)
*/
/* is_g1a(), is_g3a(): Check file type */
bool is_g1a(void *gxa);
bool is_g3a(void *gxa);
#define G1A(gxa) ((struct g1a *)(gxa))
#define G3A(gxa) ((struct g3a *)(gxa))
/* word_sum(): Sum of big-endian words at some offset in a file
This is used for the third checksum.
@gxa File whose checksum is requested
@offset File offset to start counting
@words Number of words to read
@size Size of file (in case the region overflows) */
uint16_t word_sum(void const *gxa, size_t offset, int words, size_t size);
/* checksum_g1a(): Word sum of 8 big-endian shorts at 0x300 */
uint16_t checksum_g1a(struct g1a const *g1a, size_t size);
/* checksum_g3a(): Word sum of 8 big-endian shorts at 0x7100 */
uint16_t checksum_g3a(struct g3a const *g3a, size_t size);
/* checksum_g3a_2(): Sum of ranges 0...0x20 + 0x24...EOF-4 (in-out) */
uint32_t checksum_g3a_2(struct g3a const *g3a, size_t size);
/* default_output(): Calculate default output file name
This function computes a default file name by replacing the extension of
@name (if it exists) or adding one. The extension is specified as a suffix,
usually in the form ".ext".
The resulting string might be as long as the length of @name plus that of
@suffix (plus one NUL byte); the provided buffer must point to a suitably-
large allocated space.
@name Input file name
@suffix Suffix to add or replace @name's extension with
@output Output file name */
void default_output(const char *name, const char *suffix, char *output);
/* default_internal(): Calculate default internal name
This function determines a default internal name, which is '@' followed by
at most size-1 uppercase letters taken from the application name. The buffer
must have [size+1] bytes reserved.
@name Application name
@output Internal name string (9 bytes) */
void default_internal(const char *name, char *output, size_t size);
/*
** File manipulation (file.c)
*/
/* load_gxa(): Load a g1a/g3a file into memory
This function loads @filename into a dynamically-allocated buffer and
returns the address of that buffer; it must be free()'d after use. When
loading the file, if @size is not NULL, it receives the size of the file.
On error, load() prints a message an stderr and returns NULL. The header
is inverted before this function returns.
@filename File to load
@size If non-NULL, receives the file size
Returns a pointer to a buffer with loaded data, or NULL on error. */
void *load_gxa(const char *filename, size_t *size);
/* load_binary(): Load a binary file into memory
This function operates like load_gxa() but reserves space for an empty
header. The header is initialized with all zeros.
@filename File to load
@size If non-NULL, receives the file size (with header/footer)
@header_size Extra room to add as header
@footer_size Extra room to add as footer
Returns a pointer to a buffer with loaded data, or NULL on error. */
void *load_binary(const char *filename, size_t *size, int header_size,
int footer_size);
/* save_gxa(): Save a g1a/g3a file to disk
This functions creates @filename, then writes a header and a chunk of raw
data to it. Since it temporarily inverts the header to comply with Casio's
obfuscated format, it needs write access to @g1a. Returns non-zero on error.
@filename File to write (it will be overridden if it exists)
@gxa G1A/G3A data to write
@size Size of data
Returns zero on success and a nonzero error code otherwise. */
int save_gxa(const char *filename, void *gxa, size_t size);
/*
** Icon management (icon.c)
*/
/* icon_load(): Load a PNG image into a RGB888 array
This function loads a PNG image into an RGB888 buffer. If the image is not a
PNG image or a reading error occurs, this functions prints an error message
on stderr and returns NULL.
@filename PNG file to load
@width If non-NULL, receives image width
@height If non-NULL, receives image height
Returns a pointer to a free()able buffer with loaded data, NULL on error. */
uint8_t *icon_load(const char *filename, int *width, int *height);
/* icon_save(): Save an RGB888 array to a PNG image.
@filename Target filename
@input A 24-bit RGB888 array
@width Width of input (should have no gaps)
@height Height of input
Returns non-zero on error. */
int icon_save(const char *filename, uint8_t *input, int width, int height);
/* icon_conv_24to1(): Convert RGB888 to 1-bit monochrome array
Rows are byte-padded (ie. width is rounded up to a multiple of 8). Returns
newly-allocated memory, NULL on error. */
uint8_t *icon_conv_24to1(uint8_t const *rgb24, int width, int height);
/* icon_conv_1to24(): Convert a 1-bit monochrome array to RGB888 */
uint8_t *icon_conv_1to24(uint8_t const *mono, int width, int height);
/* icon_conv_24to16(): Convert RGB888 to big-endian RGB565 */
uint16_t *icon_conv_24to16(uint8_t const *rgb24, int width, int height);
/* icon_conv_16to24(): Convert big-endian RGB565 to RGB888 */
uint8_t *icon_conv_16to24(uint16_t const *rgb16be, int width, int height);
/* icon_print_1(): Show a 1-bit image on stdout (ASCII art) */
void icon_print_1(uint8_t const *mono, int width, int height);
/* icon_print_16(): Show a 16-bit image on stdout (RGB ANSI escape codes) */
void icon_print_16(uint16_t const *rgb16be, int width, int height);
#endif /* FX_FXGXA */

60
fxgxa/g1a.h Normal file
View File

@ -0,0 +1,60 @@
//---
// fxgxa:g1a - Add-in header for Casio's G1A format
//---
#ifndef FX_G1A
#define FX_G1A
#include <stdint.h>
/* TODO: eStrips are not supported yet */
struct g1a_estrip
{
uint8_t data[80];
};
/* G1A file header with 0x200 bytes. When output to a file the standard part
(first 0x20 bytes) of this header is bit-inverted, but the non-inverted
version makes a lot more sens so we'll be using it. */
struct g1a_header
{ /* Offset Size Value */
char magic[8]; /* 0x000 8 "USBPower" */
uint8_t mcs_type; /* 0x008 1 0xf3 (AddIn) */
uint8_t seq1[5]; /* 0x009 5 0x0010001000 */
uint8_t control1; /* 0x00e 1 *0x13 + 0x41 */
uint8_t seq2; /* 0x00f 1 0x01 */
uint32_t filesize_be1; /* 0x010 4 File size, big endian */
uint8_t control2; /* 0x014 1 *0x13 + 0xb8 */
uint8_t _1; /* 0x015 1 ??? */
uint16_t checksum; /* 0x016 2 BE sum of 8 shorts at 0x300 */
uint8_t _2[6]; /* 0x018 6 ??? */
uint16_t mcs_objects; /* 0x01e 2 MCS-only, unused */
char internal[8]; /* 0x020 8 Internal app name with '@' */
uint8_t _3[3]; /* 0x028 3 ??? */
uint8_t estrips; /* 0x02b 1 Number of estrips (0..4) */
uint8_t _4[4]; /* 0x02c 4 ??? */
char version[10]; /* 0x030 10 Version "MM.mm.pppp" */
uint8_t _5[2]; /* 0x03a 2 ??? */
char date[14]; /* 0x03c 14 Build date "yyyy.MMdd.hhmm" */
uint8_t _6[2]; /* 0x04a 2 ??? */
uint8_t icon[68]; /* 0x04c 68 30*17 monochrome icon */
struct g1a_estrip estrip1; /* 0x090 80 eStrip 1 */
struct g1a_estrip estrip2; /* 0x0e0 80 eStrip 2 */
struct g1a_estrip estrip3; /* 0x130 80 eStrip 3 */
struct g1a_estrip estrip4; /* 0x180 80 eStrip 4 */
uint8_t _7[4]; /* 0x1d0 4 ??? */
char name[8]; /* 0x1d4 8 Add-in name */
uint8_t _8[20]; /* 0x1dc 20 ??? */
uint32_t filesize_be2; /* 0x1f0 4 File size, big endian */
uint8_t _9[12]; /* 0x1f4 12 ??? */
};
/* A full g1a file, suitable for use with pointers */
struct g1a
{
struct g1a_header header;
uint8_t code[];
};
#endif /* FX_G1A */

58
fxgxa/g3a.h Normal file
View File

@ -0,0 +1,58 @@
//---
// fxgxa:g3a - Add-in header for Casio's G3A format
//---
#ifndef FX_G3A
#define FX_G3A
#include <stdint.h>
/* G3A file header with 0x7000 bytes. When output to a file the standard part
(first 0x20 bytes) of this header is bit-inverted. */
struct g3a_header
{ /* Offset|Size|Value */
char magic[8]; /* 0x000 8 "USBPower" */
uint8_t mcs_type; /* 0x008 1 0x2c (AddIn) */
uint8_t seq1[5]; /* 0x009 5 0x0001000100 */
uint8_t control1; /* 0x00e 1 *0x13 + 0x41 */
uint8_t seq2; /* 0x00f 1 0x01 */
uint32_t filesize_be1; /* 0x010 4 File size, big endian */
uint8_t control2; /* 0x014 1 *0x13 + 0xb8 */
uint8_t _1; /* 0x015 1 ??? */
uint16_t checksum; /* 0x016 2 BE sum of 8 shorts at 7100 */
uint8_t _2[6]; /* 0x018 6 ??? */
uint16_t mcs_objects; /* 0x01e 2 MCS-only, unused */
uint32_t checksum_2; /* 0x020 4 Checksum: 0..1f+24..EOF-4 */
uint8_t seq3[2]; /* 0x024 2 0x0101 */
uint8_t _3[8]; /* 0x026 8 ??? */
uint32_t filesize_be2; /* 0x02e 4 Filesize - 0x7000 - 4 */
uint8_t _4[14]; /* 0x032 14 ??? */
char name[16]; /* 0x040 16 Add-in name + NUL */
uint8_t _5[12]; /* 0x050 12 ??? */
uint32_t filesize_be3; /* 0x05c 4 Filesize */
char internal[11]; /* 0x060 11 Internal name with '@' */
char label[8][24]; /* 0x06b 192 Language labels */
uint8_t allow_estrip; /* 0x12b 1 Allow use as eAct strip? */
uint8_t _6[4]; /* 0x12c 4 ??? */
char version[10]; /* 0x130 10 Version "MM.mm.pppp" */
uint8_t _7[2]; /* 0x13a 2 ??? */
char date[14]; /* 0x13c 14 Date "yyyy.MMdd.hhmm" */
uint8_t _8[38]; /* 0x14a 38 ??? */
char estrip_label[8][36]; /* 0x170 288 eStrip language labels */
uint8_t eact_icon[0x300]; /* 0x290 768 eAct icon (64x24) */
uint8_t _9[0x92c]; /* 0x590 2348 ??? */
char filename[324]; /* 0xebc 324 Filename "X.g3a" */
uint16_t icon_uns[0x1800]; /* 0x1000 5376 Unselected icon */
uint16_t icon_sel[0x1800]; /* 0x4000 5376 Selected icon */
} __attribute__((packed, aligned(4)));
/* A full g3a file, suitable for use with pointers */
struct g3a
{
struct g3a_header header;
uint8_t code[];
};
#endif /* FX_G3A */

174
fxgxa/icon.c Normal file
View File

@ -0,0 +1,174 @@
#include <stdio.h>
#include <string.h>
#include <fxgxa.h>
#include <png.h>
#include <endianness.h>
uint8_t *icon_load(const char *filename, int *width, int *height)
{
png_image img;
memset(&img, 0, sizeof img);
img.opaque = NULL;
img.version = PNG_IMAGE_VERSION;
void *buffer = NULL;
png_image_begin_read_from_file(&img, filename);
if(img.warning_or_error) {
fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1
? "warning": "error", img.message);
if(img.warning_or_error > 1) goto err;
}
img.format = PNG_FORMAT_RGB;
buffer = calloc(img.width * img.height, 3);
if(!buffer) {
fprintf(stderr, "error: cannot read %s: %m\n", filename);
goto err;
}
png_image_finish_read(&img, NULL, buffer, 0, NULL);
if(img.warning_or_error) {
fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1
? "warning": "error", img.message);
if(img.warning_or_error > 1) goto err;
}
if(width) *width = img.width;
if(height) *height = img.height;
png_image_free(&img);
return buffer;
err:
png_image_free(&img);
free(buffer);
return NULL;
}
int icon_save(const char *filename, uint8_t *input, int width, int height)
{
png_image img;
memset(&img, 0, sizeof img);
img.version = PNG_IMAGE_VERSION;
img.width = width;
img.height = height;
img.format = PNG_FORMAT_RGB;
png_image_write_to_file(&img, filename, 0, input, 0, NULL);
png_image_free(&img);
if(img.warning_or_error) {
fprintf(stderr, "libpng %s: %s\n", img.warning_or_error == 1
? "warning": "error", img.message);
if(img.warning_or_error > 1) return 1;
}
return 0;
}
uint8_t *icon_conv_24to1(uint8_t const *rgb24, int width, int height)
{
int bytes_per_row = (width + 7) >> 3;
uint8_t *mono = calloc(bytes_per_row * height, 1);
if(!mono) return NULL;
for(int y = 0; y < height; y++)
for(int x = 0; x < width; x++) {
int in = 3 * (y * width + x);
int out = (y * bytes_per_row) + (x >> 3);
int color = (rgb24[in] + rgb24[in+1] + rgb24[in+2]) < 384;
mono[out] |= color << (~x & 7);
}
return mono;
}
uint8_t *icon_conv_1to24(uint8_t const *mono, int width, int height)
{
int bytes_per_row = (width + 7) >> 3;
uint8_t *rgb24 = calloc(width * height, 3);
if(!rgb24) return NULL;
for(int y = 0; y < height; y++)
for(int x = 0; x < width; x++) {
int in = (y * bytes_per_row) + (x >> 3);
int out = 3 * (y * width + x);
int color = (mono[in] & (0x80 >> (x & 7))) != 0 ? 0x00 : 0xff;
rgb24[out] = color;
rgb24[out+1] = color;
rgb24[out+2] = color;
}
return rgb24;
}
uint16_t *icon_conv_24to16(uint8_t const *rgb24, int width, int height)
{
uint16_t *rgb16be = calloc(width * height, 2);
if(!rgb16be) return NULL;
for(int y = 0; y < height; y++)
for(int x = 0; x < width; x++) {
int in = 3 * (y * width + x);
int color = ((rgb24[in] & 0xf8) << 8)
| ((rgb24[in+1] & 0xfc) << 3)
| ((rgb24[in+2] & 0xf8) >> 3);
rgb16be[y * width + x] = htobe16(color);
}
return rgb16be;
}
uint8_t *icon_conv_16to24(uint16_t const *rgb16be, int width, int height)
{
uint8_t *rgb24 = calloc(width * height, 3);
if(!rgb24) return NULL;
for(int y = 0; y < height; y++)
for(int x = 0; x < width; x++) {
int out = 3 * (y * width + x);
int color = be16toh(rgb16be[y * width + x]);
rgb24[out] = (color & 0xf800) >> 8;
rgb24[out+1] = (color & 0x07e0) >> 3;
rgb24[out+2] = (color & 0x001f) << 3;
}
return rgb24;
}
void icon_print_1(uint8_t const *mono, int width, int height)
{
int bytes_per_row = (width + 7) >> 3;
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
int in = (y * bytes_per_row) + (x >> 3);
int color = (mono[in] & (0x80 >> (x & 7))) != 0;
putchar(color ? '#' : ' ');
putchar(color ? '#' : ' ');
}
putchar('\n');
}
}
void icon_print_16(uint16_t const *rgb16be, int width, int height)
{
uint8_t *rgb24 = icon_conv_16to24(rgb16be, width, height);
if(!rgb24) {
fprintf(stderr, "error: %m\n");
return;
};
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
int in = 3 * (y * width + x);
int r=rgb24[in], g=rgb24[in+1], b=rgb24[in+2];
printf("\e[48;2;%d;%d;%dm ", r, g, b);
}
printf("\e[0m\n");
}
free(rgb24);
}

355
fxgxa/main.c Normal file
View File

@ -0,0 +1,355 @@
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <getopt.h>
#include <fxgxa.h>
#include <g1a.h>
#include <g3a.h>
static const char *help_string =
"usage: fxgxa [-g] <binary file> [options...]\n"
" fxgxa -e <g1a/g3a file> [options...]\n"
" fxgxa -d <g1a/g3a file>\n"
" fxgxa -r <g1a/g3a file> [-o <g1a/g3a file>]\n"
" fxgxa -x <g1a/g3a file> [-o <png file>]\n"
"\n"
"fxgxa creates or edits g1a and g3a files (add-in applications for CASIO\n"
"fx-9860G and fx-CG series) that consist of a header followed by code.\n"
"\n"
"Operating modes:\n"
" -g, --g1a, --g3a Generate a g1a/g3a file (default)\n"
" -e, --edit Edit header of an existing g1a/g3a file\n"
" -d, --dump Dump header of an existing g1a/g3a file\n"
" -r, --repair Recalculate control bytes and checksums\n"
" -x, --extract Extract icon into a PNG file\n"
"\n"
"General options:\n"
" -o, --output=<file> Output file (default: input with .g1a/.g3a suffix\n"
" [-g]; with .png suffix [-x]; input file [-e, -r])\n"
" --output-uns=<file> Output for unselected icon with [-x] and g3a file\n"
" --output-sel=<file> Output for selected icon with [-x] and g3a file\n"
"\n"
"Generation and edition options:\n"
" -i, --icon=<png> Program icon, in PNG format (default: blank) [g1a]\n"
" --icon-uns=<png> Unselected program icon, in PNG format [g3a]\n"
" --icon-sel=<png> Selected program icon, in PNG format [g3a]\n"
" -n, --name=<name> Add-in name (default: output file name)\n"
" --version=<text> Program version, MM.mm.pppp format (default: empty)\n"
" --internal=<name> Internal name, eg. '@NAME' (default: empty)\n"
" --date=<date> Date of build, yyyy.MMdd.hhmm (default: now)\n";
/*
** Field customization
*/
/* A set of user-defined fields, often taken on the command-line
Default values are NULL and indicate "no value" (-g) or "no change" (-e). */
struct fields
{
/* New values for basic fields */
const char *name;
const char *version;
const char *internal;
const char *date;
/* Icon file name */
const char *icon;
const char *icon_uns, *icon_sel;
// TODO: G3A: Fill the filename field
};
/* fields_edit(): Set the value of some fields altogether
@gxa Header to edit, is assumed checksumed and filled
@fields New values for fields, any members can be NULL */
void fields_edit(void *gxa, struct fields const *fields)
{
/* For easy fields, just call the appropriate edition function */
if(fields->name) edit_name(gxa, fields->name);
if(fields->version) edit_version(gxa, fields->version);
if(fields->internal) edit_internal(gxa, fields->internal);
if(fields->date) edit_date(gxa, fields->date);
/* Load icon from PNG file */
if(fields->icon && is_g1a(gxa)) {
int w, h;
uint8_t *rgb24 = icon_load(fields->icon, &w, &h);
if(rgb24) {
/* Skip the first row if h > 17, since the usual
representation at 30x19 skips the first and last */
uint8_t *mono = icon_conv_24to1(
h > 17 ? rgb24 + 3*w : rgb24, w, h - (h > 17));
if(mono) edit_g1a_icon(gxa, mono);
free(mono);
}
free(rgb24);
}
if(fields->icon_uns && is_g3a(gxa)) {
int w, h;
uint8_t *rgb24 = icon_load(fields->icon_uns, &w, &h);
if(rgb24) {
uint16_t *rgb16be = icon_conv_24to16(rgb24, w, h);
if(rgb16be) edit_g3a_icon(gxa, rgb16be, false);
free(rgb16be);
}
free(rgb24);
}
if(fields->icon_sel && is_g3a(gxa)) {
int w, h;
uint8_t *rgb24 = icon_load(fields->icon_sel, &w, &h);
if(rgb24) {
uint16_t *rgb16be = icon_conv_24to16(rgb24, w, h);
if(rgb16be) edit_g3a_icon(gxa, rgb16be, true);
free(rgb16be);
}
free(rgb24);
}
}
/*
** Tool implementation
*/
int main(int argc, char **argv)
{
/* Result of option parsing */
int mode = 'g', error = 0;
struct fields fields = { 0 };
const char *output = NULL;
const char *output_uns=NULL, *output_sel=NULL;
const struct option longs[] = {
{ "help", no_argument, NULL, 'h' },
{ "g1a", no_argument, NULL, '1' },
{ "g3a", no_argument, NULL, '3' },
{ "edit", no_argument, NULL, 'e' },
{ "dump", no_argument, NULL, 'd' },
{ "repair", no_argument, NULL, 'r' },
{ "extract", no_argument, NULL, 'x' },
{ "output", required_argument, NULL, 'o' },
{ "output-uns", required_argument, NULL, 'O' },
{ "output-sel", required_argument, NULL, 'P' },
{ "icon", required_argument, NULL, 'i' },
{ "icon-uns", required_argument, NULL, 'I' },
{ "icon-sel", required_argument, NULL, 'J' },
{ "name", required_argument, NULL, 'n' },
{ "version", required_argument, NULL, 'v' },
{ "internal", required_argument, NULL, 't' },
{ "date", required_argument, NULL, 'a' },
{ NULL, 0, NULL, 0 },
};
int option = 0;
while(option >= 0 && option != '?')
switch((option = getopt_long(argc, argv, "hgedrxo:i:n:", longs, NULL)))
{
case 'h':
fprintf(stderr, help_string, argv[0]);
return 0;
case 'g':
case 'e':
case 'd':
case 'r':
case 'x':
case '1':
case '3':
mode = option;
break;
case 'o':
output = optarg;
break;
case 'O':
output_uns = optarg;
break;
case 'P':
output_sel = optarg;
break;
case 'i':
fields.icon = optarg;
break;
case 'I':
fields.icon_uns = optarg;
break;
case 'J':
fields.icon_sel = optarg;
break;
case 'n':
fields.name = optarg;
break;
case 'v':
fields.version = optarg;
break;
case 't':
fields.internal = optarg;
break;
case 'a':
fields.date = optarg;
break;
case '?':
error = 1;
break;
}
if(mode == 'g' && !strcmp(argv[0], "fxg1a"))
mode = '1';
if(mode == 'g' && !strcmp(argv[0], "fxg3a"))
mode = '3';
if(error) return 1;
if(argv[optind] == NULL)
{
fprintf(stderr, help_string, argv[0]);
return 1;
}
if(mode == 'g') {
fprintf(stderr, "cannot guess -g; use --g1a or --g3a\n");
return 1;
}
if(mode == '1' || mode == '3')
{
/* Load binary file into memory */
size_t size;
int header = (mode == '1' ? 0x200 : 0x7000);
int footer = (mode == '1' ? 0 : 4);
void *gxa = load_binary(argv[optind], &size, header, footer);
if(!gxa) return 1;
/* If [output] is set, use it, otherwise compute a default */
char *alloc = NULL;
if(!output)
{
alloc = malloc(strlen(argv[optind]) + 5);
if(!alloc) {fprintf(stderr, "error: %m\n"); return 1;}
default_output(argv[optind],
(mode == '1' ? ".g1a" : ".g3a"), alloc);
}
/* First set the type so that is_g1a() and is_g3a() work */
((uint8_t *)gxa)[8] = (mode == '1' ? 0xf3 : 0x2c);
/* Start with output file name as application name */
edit_name(gxa, output ? output : alloc);
/* Start with "now" as build date */
char date[15];
time_t t = time(NULL);
struct tm *now = localtime(&t);
strftime(date, 15, "%Y.%m%d.%H%M", now);
edit_date(gxa, date);
/* Start with an uppercase name as internal name */
char internal[12];
if(fields.name)
default_internal(fields.name, internal, 11);
else if(is_g1a(gxa))
default_internal(G1A(gxa)->header.name, internal, 11);
else if(is_g3a(gxa))
default_internal(G3A(gxa)->header.name, internal, 11);
edit_internal(gxa, internal);
/* Edit the fields with user-customized values */
fields_edit(gxa, &fields);
/* Set fixed fields and calculate checksums */
sign(gxa, size);
save_gxa(output ? output : alloc, gxa, size);
free(alloc);
free(gxa);
}
if(mode == 'e')
{
/* Load file into memory */
size_t size;
void *gxa = load_gxa(argv[optind], &size);
if(!gxa) return 1;
/* Edit the fields with user-customized values */
fields_edit(gxa, &fields);
/* We don't reset fixed fields or recalculate checksums because
we only want to edit what was requested by the user.
Besides, the control bytes and checksums do *not* depend on
the value of user-customizable fields. */
/* Regenerate input file, or output somewhere else */
if(!output) output = argv[optind];
save_gxa(output, gxa, size);
free(gxa);
}
if(mode == 'd')
{
size_t size;
void *gxa = load_gxa(argv[optind], &size);
if(!gxa) return 1;
dump(gxa, size);
free(gxa);
}
if(mode == 'r')
{
size_t size;
void *gxa = load_gxa(argv[optind], &size);
if(!gxa) return 1;
/* Repair file by recalculating fixed fields and checksums */
sign(gxa, size);
/* Regenerate input file, or output somewhere else */
if(!output) output = argv[optind];
save_gxa(output, gxa, size);
free(gxa);
}
if(mode == 'x')
{
size_t size;
void *gxa = load_gxa(argv[optind], &size);
if(!gxa) return 1;
if(is_g1a(gxa)) {
/* Add clean top/bottom rows */
uint8_t mono[76];
memcpy(mono, "\x00\x00\x00\x00", 4);
memcpy(mono+4, G1A(gxa)->header.icon, 68);
memcpy(mono+72, "\x7f\xff\xff\xfc", 4);
uint8_t *rgb24 = icon_conv_1to24(mono, 30, 19);
/* Calculate a default output name if none is given */
char *alloc = NULL;
if(!output) {
alloc = malloc(strlen(argv[optind]) + 5);
if(!alloc) {
fprintf(stderr, "error: %m\n");
return 1;
}
default_output(argv[optind], ".png", alloc);
}
icon_save(output ? output : alloc, rgb24, 30, 19);
free(alloc);
free(rgb24);
}
else if(is_g3a(gxa)) {
uint8_t *rgb24_uns = icon_conv_16to24(
G3A(gxa)->header.icon_uns, 92, 64);
uint8_t *rgb24_sel = icon_conv_16to24(
G3A(gxa)->header.icon_sel, 92, 64);
if(output_uns)
icon_save(output_uns, rgb24_uns, 92, 64);
if(output_sel)
icon_save(output_sel, rgb24_sel, 92, 64);
if(!output_uns && !output_sel) fprintf(stderr, "Please"
" specify --output-uns or --output-sel.\n");
free(rgb24_uns);
free(rgb24_sel);
}
}
return 0;
}

95
fxgxa/util.c Normal file
View File

@ -0,0 +1,95 @@
#include <string.h>
#include <ctype.h>
#include <endianness.h>
#include <fxgxa.h>
bool is_g1a(void *gxa)
{
/* Check the byte at offset 8 for file type 0xf3 */
uint8_t mcs_type = ((uint8_t *)gxa)[8];
return mcs_type == 0xf3;
}
bool is_g3a(void *gxa)
{
/* Check the byte at offset 8 for file type 0x2c */
uint8_t mcs_type = ((uint8_t *)gxa)[8];
return mcs_type == 0x2c;
}
uint16_t word_sum(void const *gxa, size_t offset, int words, size_t size)
{
uint16_t shorts[words];
memset(shorts, 0, 2*words);
/* Extract up to [2*words] bytes from the file */
int available = (int)size - offset;
if(available < 0) available = 0;
if(available > 2*words) available = 2*words;
memcpy(shorts, gxa+offset, available);
/* Do the big-endian sum */
uint16_t sum = 0;
for(int i = 0; i < words; i++)
sum += htobe16(shorts[i]);
return sum;
}
uint16_t checksum_g1a(struct g1a const *g1a, size_t size)
{
return word_sum(g1a, 0x300, 8, size);
}
uint16_t checksum_g3a(struct g3a const *g3a, size_t size)
{
return word_sum(g3a, 0x7100, 8, size);
}
uint32_t checksum_g3a_2(struct g3a const *g3a, size_t size)
{
uint32_t sum = 0;
uint8_t *data = (void *)g3a;
for(size_t i = 0; i < 0x20; i++)
sum += (data[i] ^ 0xff);
for(size_t i = 0x24; i < size - 4; i++)
sum += data[i];
return sum;
}
void default_output(const char *name, const char *suffix, char *output)
{
/* Check if there is a dot at the end of @name, before the last '/'.
The dot must also not be in first position (hidden files) */
size_t end = strlen(name) - 1;
while(end >= 1 && name[end] != '/' && name[end] != '.') end--;
/* If we don't have a dot in the file name, append the extension */
if(end < 1 || name[end] != '.')
{
strcpy(output, name);
strcat(output, suffix);
}
/* If we found a dot before the last slash, replace the extension */
else
{
memcpy(output, name, end);
strcpy(output + end, suffix);
}
}
void default_internal(const char *name, char *output, size_t size)
{
output[0] = '@';
size_t i = 1;
for(int j = 0; name[j] && i < size; j++)
{
if(isalpha(name[j])) output[i++] = toupper(name[j]);
}
output[i] = 0;
}

View File

@ -1,14 +0,0 @@
//---
// fxlink:config - Compile-time configuration
//---
#ifndef FXLINK_CONFIG_H
#define FXLINK_CONFIG_H
/* Disables UDisks2 interfaces for systems that don't use it. */
#cmakedefine FXLINK_DISABLE_UDISKS2
/* Disable SDL2 interfaces. */
#cmakedefine FXLINK_DISABLE_SDL2
#endif /* FXLINK_CONFIG_H */

View File

@ -1,194 +0,0 @@
#include "filter.h"
#include "util.h"
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
//---
// Property parser
//---
/* skip_spaces(): Skip spaces, returns true if end of string is reached */
bool skip_spaces(char const **input)
{
while(isspace(**input)) (*input)++;
return (**input == 0);
}
/* isword(): Identify valid word characters for the filter */
bool isword(int c)
{
return c && !strchr(" \t\n,;=", c);
}
/* read_word(): Copy the next word in the string, assumes word is non-empty */
char *read_word(char const **input)
{
char const *str = *input;
while(**input && isword(**input)) (*input)++;
return strndup(str, *input - str);
}
enum {
T_END, /* End of string */
T_PROP, /* Property; (*name) and (*value) are set */
T_COMMA = ',', /* Comma character (property separator) */
T_SEMI = ';', /* Semicolon character (option separator) */
};
/* lex(): Read a token from the input source
Returns the token type, updates (*input) and sets (*name) and (*value) to
either NULL or some freshly-allocated copies of the name and (optional)
value of the property (always NULL unless T_PROP is returned). The caller
should free() both. */
static int lex(char const **input, char **name, char **value)
{
*name = *value = NULL;
if(skip_spaces(input)) return T_END;
if(**input == ',' || **input == ';') {
(*input)++;
return (*input)[-1];
}
if(!isword(**input)) {
wrn("expected property name in filter, skipping '%c'", **input);
(*input)++;
return lex(input, name, value);
}
*name = read_word(input);
if(skip_spaces(input) || **input != '=')
return T_PROP;
(*input)++;
if(skip_spaces(input))
wrn("no value after '=' in filter property '%s'", *name);
else if(!isword(**input))
wrn("ignoring invalid value for filter property '%s'", *name);
else
*value = read_word(input);
return T_PROP;
}
filter_t *filter_parse(char const *input)
{
char *name=NULL, *value=NULL;
int t;
/* Create an initial filter with a single otion */
filter_t *filter = malloc(sizeof *filter);
if(!filter) return NULL;
filter->options = calloc(1, sizeof(properties_t));
if(!filter->options) {
free(filter);
return NULL;
}
filter->length = 1;
/* Current option */
properties_t *option = &filter->options[0];
while((t = lex(&input, &name, &value)) != T_END) {
/* Ignore property separators (tokens are already separated) */
if(t == ',') continue;
/* Add a new option in the filter */
if(t == ';') {
size_t new_size = (filter->length + 1) * sizeof(properties_t);
properties_t *new_options = realloc(filter->options, new_size);
if(!new_options) continue;
filter->options = new_options;
option = &filter->options[filter->length++];
*option = (properties_t){ 0 };
continue;
}
/* Add a new property to the current option */
if(!strcmp(name, "p7") && !value)
option->p7 = true;
else if(!strcmp(name, "mass_storage") && !value)
option->mass_storage = true;
else if(!strcmp(name, "series_cg") && !value)
option->series_cg = true;
else if(!strcmp(name, "series_g3") && !value)
option->series_g3 = true;
else if(!strcmp(name, "serial_number") && value) {
option->serial_number = strdup(value);
}
else wrn("ignoring invalid filter property: '%s' %s value", name,
value ? "with" : "without");
free(name);
free(value);
}
return filter;
}
void filter_free(filter_t *filter)
{
if(!filter) return;
for(size_t i = 0; i < filter->length; i++)
free(filter->options[i].serial_number);
free(filter->options);
free(filter);
}
//---
// Filtering API
//---
void filter_clean_libusb(filter_t *filter)
{
if(!filter) return;
for(size_t i = 0; i < filter->length; i++) {
properties_t *prop = &filter->options[i];
/* Suppress series_cg and series_g3, which are based off the USB Mass
Storage metadata provided only by UDisks2 */
if(prop->series_cg) {
wrn("ignoring series_cg in libusb filter (cannot be detected)");
prop->series_cg = false;
}
if(prop->series_g3) {
wrn("ignoring series_g3 in libusb filter (cannot be detected)");
prop->series_g3 = false;
}
}
}
void filter_clean_udisks2(filter_t *filter)
{
/* Every property can be used */
(void)filter;
}
bool filter_match(properties_t const *props, filter_t const *filter)
{
/* No filter is a pass-through */
if(!filter || !filter->length)
return true;
for(size_t i = 0; i < filter->length; i++) {
if(properties_match(props, &filter->options[i]))
return true;
}
return false;
}
void filter_print(FILE *fp, filter_t const *filter)
{
#define output(...) { \
if(sep) fprintf(fp, ", "); \
fprintf(fp, __VA_ARGS__); \
sep = true; \
}
for(size_t i = 0; i < filter->length; i++) {
if(i > 0) printf("; ");
properties_t *prop = &filter->options[i];
properties_print(fp, prop);
}
}

View File

@ -1,49 +0,0 @@
//---
// fxlink:filter - Property-based device filtering
//---
#ifndef FXLINK_FILTER_H
#define FXLINK_FILTER_H
#include "properties.h"
#include <stddef.h>
#include <stdio.h>
/* filter_t: An OR-combination of property filters
Attributes of properties_t objects have AND-semantics when used as filters;
all of them must match. For more flexibility, the command-line allows the
user to specify an OR-combination of such filters, called "options". */
typedef struct {
/* Array of options to be matched against; not terminated. */
properties_t *options;
/* Length of (options). */
size_t length;
} filter_t;
/* Return values for backend-specific matching functions */
enum {
FILTER_UNIQUE = 0,
FILTER_NONE = 1,
FILTER_MULTIPLE = 2,
FILTER_ERROR = 3,
};
/* filter_parse(): Parse a filter string */
filter_t *filter_parse(char const *specification);
/* filter_free(): Free a created by filter_parse() */
void filter_free(filter_t *filter);
/* filter_clean_libusb(): Disable filter properties unsupported for libusb */
void filter_clean_libusb(filter_t *filter);
/* filter_clean_udisks2(): Disable filter properties unsupported for udisks2 */
void filter_clean_udisks2(filter_t *filter);
/* filter_match(): Check whether some properties match the supplied filter */
bool filter_match(properties_t const *props, filter_t const *filter);
/* filter_print(): Print a parser filter (one-line; for debugging) */
void filter_print(FILE *fp, filter_t const *filter);
#endif /* FXLINK_FILTER_H */

View File

@ -1,24 +1,42 @@
//---
// fxlink:fxlink - Application logic
//---
#ifndef FXLINK_FXLINK_H
#define FXLINK_FXLINK_H
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.fxlink: Options and mode functions
#pragma once
#include <fxlink/filter.h>
#include <libusb.h>
#include "filter.h"
#include "util.h"
/* Global and command-line options. */
struct fxlink_options {
/* If not NULL, gets a copy of all text messages received in either
interactive mode */
FILE *log_file;
/* Extra details (mainly interactive messages) */
bool verbose;
};
extern struct fxlink_options options;
/* Main function for -l */
int main_list(filter_t *filter, delay_t *delay, libusb_context *context);
int main_list(struct fxlink_filter *filter, delay_t *delay,
libusb_context *context);
/* Main function for -b */
int main_blocks(filter_t *filter, delay_t *delay);
int main_blocks(struct fxlink_filter *filter, delay_t *delay);
/* Main function for -s */
int main_send(filter_t *filter, delay_t *delay, char **files);
int main_send(struct fxlink_filter *filter, delay_t *delay, char **files);
/* Main function for -i */
int main_interactive(filter_t *filter,delay_t *delay,libusb_context *context);
int main_interactive(struct fxlink_filter *filter, delay_t *delay,
libusb_context *context);
#endif /* FXLINK_FXLINK_H */
/* Main function for -t */
int main_tui_interactive(libusb_context *context);
/* Main function for -p */
int main_push(struct fxlink_filter *filter, delay_t *delay,
libusb_context *context, char **files);

View File

@ -0,0 +1,15 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tooling.libpng: Utilities based on libpng
#pragma once
#include <fxlink/protocol.h>
#include <png.h>
/* Save a raw image decoded from an fxlink message to a PNG file. Returns
zero on success. */
int fxlink_libpng_save_raw(struct fxlink_message_image_raw *raw,
char const *path);

View File

@ -0,0 +1,24 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tooling.sdl2: Utilities based on the SDL2 library
//
// Note: all the functions in this file are "neutralized" if the compile-time
// option FXLINK_DISABLE_SDL2 is set. See <fxlink/config.h.in>.
//---
#pragma once
#include <fxlink/protocol.h>
#ifndef FXLINK_DISABLE_SDL2
#include <SDL2/SDL.h>
#endif
/* Display a raw image on the window. If now window has been opened yet, one is
created automatically. */
void fxlink_sdl2_display_raw(struct fxlink_message_image_raw const *raw);
/* Handle SDL events. This should be called regularly from the main thread. */
void fxlink_sdl2_handle_events(void);

View File

@ -1,33 +1,39 @@
//---
// fxlink:ud2 - UDisks2 functions
//---
#ifndef FXLINK_UD2_H
#define FXLINK_UD2_H
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tooling.udisks2: Utilities based on the UDisks2 library
#pragma once
#include <fxlink/config.h>
#ifndef FXLINK_DISABLE_UDISKS2
#include <udisks/udisks.h>
#include "config.h"
#include "properties.h"
#include "filter.h"
#include "util.h"
#include <fxlink/filter.h>
/* ud2_properties(): Determine properties of a UDisks2 USB drive */
properties_t ud2_properties(UDisksDrive *drive);
struct fxlink_filter ud2_properties(UDisksDrive *drive);
/* Initialize a connection to the UDisks2 service via D-Bus. */
int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr);
/* Close the connection to the UDisks2 service. */
void ud2_end(UDisksClient *udc, UDisksManager *udm);
/* ud2_unique_matching(): Device matching the provided filter, if unique
Similar to usb_unique_matching(), please refer to "usb.h" for details.
There are just many more inputs and outputs. */
int ud2_unique_matching(filter_t const *filter, UDisksClient *udc,
int ud2_unique_matching(struct fxlink_filter const *filter, UDisksClient *udc,
UDisksManager *udm, UDisksBlock **block, UDisksDrive **drive,
UDisksFilesystem **fs);
/* ud2_unique_wait(): Wait for a device matching the provided filter to connect
Like usb_unique_wait(), please see "usb.h" for details. */
int ud2_unique_wait(filter_t const *filter, delay_t *delay, UDisksClient *udc,
UDisksManager *udm, UDisksBlock **block, UDisksDrive **drive,
UDisksFilesystem **fs);
Returns an FXLINK_FILTER_* code. If a unique device is found, sets *udc,
*udm, *block, *drive and *fs accordingly. */
int ud2_unique_wait(struct fxlink_filter const *filter, delay_t *delay,
UDisksClient *udc, UDisksManager *udm, UDisksBlock **block,
UDisksDrive **drive, UDisksFilesystem **fs);
//---
// Iteration on UDisks2 devices
@ -39,7 +45,7 @@ typedef struct {
UDisksDrive *drive;
UDisksFilesystem *fs;
/* Device properties */
properties_t props;
struct fxlink_filter props;
/* Whether the iteration has finished */
bool done;
@ -66,5 +72,3 @@ void ud2_iter_next(ud2_iterator_t *it);
!NAME.done; ud2_iter_next(&NAME)) if(!NAME.done)
#endif /* FXLINK_DISABLE_UDISKS2 */
#endif /* FXLINK_UD2_H */

View File

@ -0,0 +1,71 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tui.input: Asynchronous readline-style console input
//
// This header provides a basic asynchronous line edition mechanic attached to
// an ncurses window. It might be possible to use readline directly, but this
// is also a good exercise.
//---
#pragma once
#include <fxlink/defs.h>
#include <ncurses.h>
/* Input field attached to a window. This object only needs to be instantiated
once for multiple inputs. */
struct fxlink_TUI_input {
/* Line contents, NUL-terminated. The buffer might be larger. */
char *data;
/* Size of contents (not counting the NUL) */
int size;
/* Allocated size (always ≥ size+1) */
int alloc_size;
/* Cursor position within string */
int cursor;
/* Attached ncurses window */
WINDOW *win;
/* Original cursor position within window at start of input */
uint16_t wx, wy;
};
//---
// Text manipulation functions
//---
/* Initialize the input at least init_chars characters of content available.
Returns false on error. Previous contents are not freed! */
bool fxlink_TUI_input_init(struct fxlink_TUI_input *in, WINDOW *win,
int init_chars);
/* Clean up a line and free its contents. */
void fxlink_TUI_input_free(struct fxlink_TUI_input *in);
/* Realloc the line to ensure n characters plus a NUL can be written. */
bool fxlink_TUI_input_alloc(struct fxlink_TUI_input *in, int n);
/* Insert n characters at position p. */
bool fxlink_TUI_input_insert(struct fxlink_TUI_input *in, int p,
char const *str, int n);
/* Remove n characters at position p. Returns the number of characters
actually removed after bounds checking. */
int fxlink_TUI_input_delete(struct fxlink_TUI_input *in, int p, int n);
//--
// Rendering functions
//---
/* Clear the input up to the original cursor position */
void fxlink_TUI_input_clear(struct fxlink_TUI_input *in);
/* Redraw the input (needed after non-appending edits) */
void fxlink_TUI_input_redraw(struct fxlink_TUI_input *in);
/* Clear the screen as with C-l */
void fxlink_TUI_input_clearscreen(struct fxlink_TUI_input *in);
/* getch() for an input (usually called when there *is* input */
bool fxlink_TUI_input_getch(struct fxlink_TUI_input *in, WINDOW *logWindow);

View File

@ -0,0 +1,113 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tui.layout: Utility for laying out TUI windows in a flexbox treee
//
// This module can be used to set up a flexbox tree for window layout in the
// TUI. A `struct fxlink_TUI_box` is either an ncurses window, or a flexbox
// which arranges its children horizontally or vertically.
//
// Each box has some geometry settings:
// - w and h are initially set to the natural content size (can be 0)
// - min_w, min_h, max_w, max_h constrain the range of acceptable sizes
// - stretch_x and stretch_y indicate the box's tendency to grow
// - strech_force allows stretching beyond max_w/max_h (rarely needed)
//
// Most of the module is free-standing. Boxes should be created bottom to top,
// ie. windows first and then progressively larger groups. Specifying a WINDOW
// pointer in windows is optional; it is only used by fxlink_TUI_apply_layout()
// to actually configure ncurses windows. The root box of the tree should be
// kept in memory as it is used for rendering windows borders on the background
// window.
//
// Space distribution is initiated by a call to box_layout_root() after all the
// boxes have been created to form the tree. The root box receives the provided
// screen space, and splits it recursively between children. Boxes get their
// requested content size (clamped to minimum/maximum size) and any space left
// is distributed in proportion with stretch factors. Overflows are possible if
// the screen is too small to accommodate for everyone's content size, so the
// application of the layout should account for that and clamp to the screen
// size again (fxlink_TUI_apply_layout() does that).
//---
#pragma once
#include <fxlink/defs.h>
#include <ncurses.h>
#include <stdio.h>
#define FXLINK_TUI_BOX_MAXSIZE 8
/* Box either holding an ncurses WINDOW or arranging children in a flexbox. */
struct fxlink_TUI_box {
/* Position and size, excluding the border (which is shared). After layout,
x/y is the absolute position and w/h the allocated size. Other geometric
settings are only relevant during layout. */
uint16_t x, y, w, h;
/* Size constraints */
uint16_t min_w, min_h;
uint16_t max_w, max_h;
/* Stretch factor */
uint8_t stretch_x, stretch_y;
/* Stretch beyond limits */
bool stretch_force;
/* Box subdivision type: BOX_WINDOW, BOX_HORIZ and BOX_VERT */
short type;
union {
/* Valid for type == FXLINK_TUI_BOX_WINDOW */
struct {
char const *title;
WINDOW **win;
} window;
/* Valid for type == FXLINK_TUI_BOX_{HORIZONTAL,VERTICAL} */
struct fxlink_TUI_box *children[FXLINK_TUI_BOX_MAXSIZE];
};
};
enum {
/* Box is an ncurses windows. Before layout, the natural size of the
content is set in w and h (default 0), size constraints are set in
{min,max}_{w,h} (default 0/65535) and stretch rates in stretch_x,
stretch_y (default 1) and strech_force (default false). */
FXLINK_TUI_BOX_WINDOW,
/* Box is a horizontal of vertical flexbox. Before layout, children are
specified and they induce a natural content size. Size constraints are
stretch rates are specified as for windows. */
FXLINK_TUI_BOX_HORIZONTAL,
FXLINK_TUI_BOX_VERTICAL,
};
/* Make a window box. The title is used for the border rendering function in
the TUI rendering utils. The window pointer is optional and only needed for
fxlink_TUI_apply_layout(). */
struct fxlink_TUI_box *fxlink_TUI_box_mk_window(char const *title, WINDOW **w);
/* Make a vertical box with a fixed list of children */
struct fxlink_TUI_box *fxlink_TUI_box_mk_vertical(
struct fxlink_TUI_box *child1, ... /*, NULL */);
/* Make a horizontal box with a fixed list of children */
struct fxlink_TUI_box *fxlink_TUI_box_mk_horizontal(
struct fxlink_TUI_box *child1, ... /*, NULL */);
/* Specify the minimum size, maximum size and stretch rate of a box */
void fxlink_TUI_box_minsize(struct fxlink_TUI_box *box, int min_w, int min_h);
void fxlink_TUI_box_maxsize(struct fxlink_TUI_box *box, int max_w, int max_h);
void fxlink_TUI_box_stretch(struct fxlink_TUI_box *box,
int stretch_x, int stretch_y, bool force);
/* Recursively print box and children starting at specified indent level */
void fxlink_TUI_box_print(FILE *fp, struct fxlink_TUI_box const *b, int level);
/* Layout a root box for the specified available screen space. This accounts
for 1-unit borders around and between windows. For a full-screen window tree
x/y would be set to 0 and w/h to the screen size, but the tree can also be
laid out to occupy only a subset of screen space. */
void fxlink_TUI_box_layout(struct fxlink_TUI_box *root_box,
int x, int y, int w, int h);
/* Recursively apply the layout. This function resizes and moves ncurses
windows to fit the space allocated in the boxes. Returns false if an ncurses
error causes one of the windows to become NULL. */
bool fxlink_TUI_apply_layout(struct fxlink_TUI_box *root_box);

View File

@ -0,0 +1,32 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tui.render: TUI rendering utilities
#pragma once
#include <fxlink/tui/layout.h>
#include <ncurses.h>
/* printf to a window. */
#define print wprintw
/* printf with an ncurses attribute for the whole string. */
void aprint(WINDOW *win, int attr, char const *format, ...);
/* printf with an <fxlink/defs.h> format for the whole string. */
void fprint(WINDOW *win, int display_fmt, char const *format, ...);
/* Hard-coded color scheme */
#define FMT_FILENAME FMT_CYAN
#define FMT_SIZE FMT_MAGENTA
#define FMT_HEADER (FMT_CYAN | FMT_ITALIC)
#define FMT_BGSELECTED (FMT_BGWHITE | FMT_BLACK)
/* Translate <fxlink/defs.h> text format into ncurses attributes. */
int fmt_to_ncurses_attr(int display_fmt);
/* Recursively render borders around a box and its children. */
void fxlink_TUI_render_borders(struct fxlink_TUI_box const *box);
/* Render the window titles of all windows in the tree. */
void fxlink_TUI_render_titles(struct fxlink_TUI_box const *box);

View File

@ -1,272 +0,0 @@
#include "config.h"
#include "fxlink.h"
#include "util.h"
#include "properties.h"
#include "filter.h"
#include "protocol.h"
#include "usb.h"
#include "png.h"
#include "sdl2.h"
#include <libusb.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <png.h>
/* Video capture trackers, to avoid spamming terminal with messages */
static int last_message_was_video = 0;
static int video_frame_count = 0;
static char *output_file(char const *path,char const *type,char const *suffix)
{
char *filename = NULL;
int counter = 1;
time_t time_raw;
struct tm time_bd;
time(&time_raw);
localtime_r(&time_raw, &time_bd);
while(1) {
asprintf(&filename, "%s/fxlink-%.16s-%04d.%02d.%02d-%02dh%02d-%d.%s",
path, type, time_bd.tm_year + 1900, time_bd.tm_mon,
time_bd.tm_mday, time_bd.tm_hour, time_bd.tm_min, counter, suffix);
if(!filename) continue;
/* Try to find a name for a file that doesn't exist */
if(access(filename, F_OK) == -1) break;
free(filename);
counter++;
}
return filename;
}
static bool message_new(message_t *msg, usb_fxlink_header_t const *h)
{
int version_major = (h->version >> 8) & 0xff;
int version_minor = (h->version) & 0xff;
if(!strncmp(h->application,"fxlink",16) && !strncmp(h->type,"video",16)) {
if(last_message_was_video)
fprintf(stderr, "\r");
last_message_was_video = 1;
}
else {
if(last_message_was_video)
fprintf(stderr, "\n");
last_message_was_video = 0;
}
fprintf(stderr, "New message (v%d.%d): application '%.16s', type '%.16s', "
"size %d bytes", version_major, version_minor, h->application,
h->type, h->size);
if(last_message_was_video)
fprintf(stderr, " [video frame #%d]", ++video_frame_count);
else
fprintf(stderr, "\n");
msg->output = malloc(h->size);
if(!msg->output) {
err("cannot allocate memory for message of %d bytes", h->size);
return false;
}
msg->header = *h;
msg->size_read = 0;
msg->valid = true;
return true;
}
static void message_finish(message_t *msg)
{
char const *path = ".";
if(!strncmp(msg->header.application, "fxlink", 16)) {
if(!strncmp(msg->header.type, "image", 16)) {
usb_fxlink_image_t *img = (void *)msg->output;
char *filename = output_file(path, msg->header.type, "png");
uint8_t **row_pointers = fxlink_protocol_decode_image(msg);
fxlink_png_save(row_pointers, img->width, img->height, filename);
printf("Saved image (%dx%d, format=%d) to '%s'\n",
img->width, img->height, img->pixel_format, filename);
free(row_pointers);
free(filename);
return;
}
if(!strncmp(msg->header.type, "text", 16)) {
printf("------------------\n");
fwrite(msg->output, 1, msg->header.size, stdout);
if(msg->output[msg->header.size - 1] != '\n') printf("\n");
printf("------------------\n");
return;
}
if(!strncmp(msg->header.type, "video", 16)) {
usb_fxlink_image_t *img = (void *)msg->output;
uint8_t **row_pointers = fxlink_protocol_decode_image(msg);
#ifndef FXLINK_DISABLE_SDL2
sdl2_stream(row_pointers, img->width, img->height);
#else
warn("SDL2 support disabled, skipping video frame!");
#endif
return;
}
}
/* Default to saving to a blob */
char *filename = output_file(path, "blob", "bin");
FILE *fp = fopen(filename, "wb");
if(!fp) {
err("could not save to '%s': %m", filename);
return;
}
fwrite(msg->output, 1, msg->header.size, fp);
fclose(fp);
fprintf(stderr, "Saved as blob to '%s'\n", filename);
free(filename);
}
static void message_output(message_t *msg, void *buffer, int size)
{
int data_left = msg->header.size - msg->size_read;
if(size > data_left) {
err("Too much data in message, dropping %d bytes", size - data_left);
size = data_left;
}
memcpy(msg->output + msg->size_read, buffer, size);
msg->size_read += size;
if(msg->size_read >= msg->header.size) {
bool is_video = !strncmp(msg->header.application, "fxlink", 16) &&
!strncmp(msg->header.type, "video", 16);
if(!is_video)
fprintf(stderr, "Successfully read %d bytes\n", msg->size_read);
message_finish(msg);
msg->valid = false;
}
}
int main_interactive(filter_t *filter, delay_t *delay, libusb_context *context)
{
libusb_device *dev = NULL;
libusb_device_handle *dh = NULL;
/* Wait for a device to be connected */
filter_clean_libusb(filter);
int rc = usb_unique_wait(filter, delay, context, &dev);
if(rc == FILTER_NONE) {
printf("No device found.\n");
return 1;
}
else if(rc == FILTER_MULTIPLE) {
printf("Multiple devices found, ambiguous!\n");
return 1;
}
if((rc = libusb_open(dev, &dh))) {
rc = libusb_err(rc, "cannot open device %s", usb_id(dev));
goto end;
}
/* Don't detach kernel drivers to avoid breaking the Mass Storage
communications if fxlink is ever started while the native LINK
application is running! */
libusb_set_auto_detach_kernel_driver(dh, false);
if((rc = libusb_claim_interface(dh, 0))) {
rc = libusb_err(rc, "cannot claim interface on %s", usb_id(dev));
goto end;
}
printf("Connected to %s, starting test.\n", usb_id(dev));
/* This buffer is used to receive messages; if the header is not complete
it is left in the buffer, hence the extra room */
__attribute__((aligned(4)))
static uint8_t buffer[2048 + sizeof(usb_fxlink_header_t)] = { 0 };
/* Amount of data in the buffer */
int buffer_size = 0;
/* Current message */
message_t msg = { 0 };
while(1)
{
#ifndef FXLINK_DISABLE_SDL2
sdl2_tick();
#endif
int transferred = -1;
rc = libusb_bulk_transfer(dh, 0x81, buffer + buffer_size, 2048,
&transferred, 500);
if(rc == LIBUSB_ERROR_NO_DEVICE) {
if(last_message_was_video)
fprintf(stderr, "\n");
printf("Disconnected, leaving.\n");
break;
}
else if(rc && rc != LIBUSB_ERROR_TIMEOUT) {
rc = libusb_err(rc, "bulk transfer failed on %s", usb_id(dev));
continue;
}
if(transferred <= 0) continue;
buffer_size += transferred;
/* If there is an unfinished message, continue working on it */
if(msg.valid) {
message_output(&msg, buffer, buffer_size);
buffer_size = 0;
}
/* If the header is not yet fully transmitted, wait */
usb_fxlink_header_t *h = (void *)buffer;
if(buffer_size < (int)sizeof *h) continue;
/* Handle a new message */
if(h->version == 0x00000100) {
int data_size = buffer_size - sizeof *h;
if(!message_new(&msg, h))
printf("dropping %d bytes\n", data_size);
else
message_output(&msg, buffer + sizeof *h, data_size);
buffer_size = 0;
continue;
}
else {
err("invalid header, dropping %d bytes", transferred);
buffer_size = 0;
}
}
/* Save last unfinished message */
if(buffer_size > 0) {
printf("%d bytes not collected dropped\n", buffer_size);
}
rc = 0;
end:
if(dh) {
libusb_release_interface(dh, 0);
libusb_close(dh);
}
if(dev) libusb_unref_device(dev);
return rc;
}

View File

@ -1,9 +1,12 @@
#include "config.h"
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "fxlink.h"
#include "util.h"
#include "properties.h"
#include "filter.h"
#include "usb.h"
#include <fxlink/filter.h>
#include <fxlink/logging.h>
#include <libusb.h>
#include <getopt.h>
@ -11,72 +14,90 @@
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
int main_test(libusb_device *device, libusb_context *context);
#include <locale.h>
static const char *help_string =
"usage: %1$s -l [options...]\n"
" %1$s -b [options...]\n"
" %1$s -s [options...] <FILES...>\n"
" %1$s --test\n"
"usage: %1$s (-l|-b|-t) [General options]\n"
" %1$s -i [-r] [--fxlink-log[=<FILE>]] [General options]\n"
" %1$s -p <FILE> [General options]\n"
" %1$s -s <FILES>... [General options]\n"
"\n"
"fxlink interacts with CASIO calculators of the fx-9860G and fx-CG 50 series\n"
"over the USB port, through mass storage and custom USB protocols. Depending\n"
"on the mode, fxlink uses libusb (for discovery and USB communication)or\n"
"the UDisks2 library (to mount and use Mass Storage devices).\n"
"fxlink interacts with CASIO calculators of the fx and fx-CG families over\n"
"the USB port, using libusb. It can also transfer files for Mass Storage\n"
"calculators using UDisks2.\n"
"\n"
"Operating modes:\n"
" -l, --list List detected calculators on the USB ports (libusb)\n"
" -b, --blocks List detected Mass Storage filesystems (udisks2)\n"
" -s, --send Send a file to a Mass Storage calculator (udisks2)\n"
" -i, --interactive Interactive messaging with a gint add-in (libusb)\n"
"Standard (libusb) modes:\n"
" -l, --list List detected calculators on the USB ports\n"
" -i, --interactive Messaging with a gint add-in (calc -> PC only)\n"
" -t, --tui TUI interactive mode\n"
" -p, --push Push a .bin file to the Add-In Push app\n"
"\n"
"Mass Storage (UDisks2) modes:\n"
" -b, --blocks List detected Mass Storage calculators\n"
" -s, --send Send a file to a Mass Storage calc and unmount it\n"
"\n"
"General options:\n"
" -w DELAY Wait up to this many seconds for a calculator to\n"
" connect. If DELAY is unspecified, wait indefinitely.\n"
" -f FILTER Filter which calculators can be detected and used\n"
" --libusb-log=LEVEL libusb log level: NONE, ERROR, WARNING, INFO, DEBUG\n"
" -v, --verbose Increased verbosity (mostly in -i/-t)\n"
" -w <SECONDS> Wait this many seconds for a calculator to connect\n"
" -w Wait indefinitely for a calculator to connect\n"
" -f <FILTER> Filter which calculators we connect to (see below)\n"
" --libusb-log=LEVEL libusb log level (NONE, ERROR, WARNING, INFO, DEBUG)\n"
"\n"
"Mode-specific options:\n"
" --fxlink-log[=FILE] -i: Append fxlink text messages to FILE. Without\n"
" argument, a unique name is generated.\n"
" -r, --repeat -i: Reconnect if the calc disconnects (implies -w)\n"
"\n"
"Device filters:\n"
" A device filter is a comma-separated list of properties that a device has\n"
" to match in order to be listed or used, such as 'p7,serial=00000001'.\n"
" Several filters can be separated with a semicolon, in which case a device\n"
" will be considered as long as it matches one of the filters. For example,\n"
" 'p7 ; mass_storage,serial=IGQcGRe9'.\n"
"\n"
" The following properties are defined; the libraries in which each can be\n"
" detected and used is indicated in brackets.\n"
" p7 Matches Protocol 7 calculators (all the FX models\n"
" except the G-III). [libusb, udisks2]\n"
" mass_storage Matches Mass Storage calculators (the CG series and\n"
" the G-III). [libusb, udisks2]\n"
" series_cg Matches CG-series calculators. [udisks2]\n"
" series_g3 Matches G-III series calculators. [udisks2]\n"
" serial_number=ID Matches this specific serial number. Requires write\n"
" access to the device in libusb. [libusb, udisks2]\n";
" A device filter narrows down what devices we list or connect to by\n"
" requiring a list of properties, such as \"p7,serial=00000001\". The\n"
" following properties can be tested:\n"
" - p7 Protocol 7 calcs (all fx models that use FA-124)\n"
" - mass_storage Mass Storage calcs (fx-CG models and the G-III)\n"
" - series_cg fx-CG models [udisks2 only]\n"
" - series_g3 G-III models [udisks2 only]\n"
" - serial=<SERIAL> This serial number (needs write access in libusb)\n";
/* Global options */
struct fxlink_options options;
int main(int argc, char **argv)
{
int rc=1, mode=0, error=0, option=0, loglevel=LIBUSB_LOG_LEVEL_ERROR;
int rc=1, mode=0, error=0, option=0, loglevel=LIBUSB_LOG_LEVEL_WARNING;
delay_t delay = delay_seconds(0);
filter_t *filter = NULL;
struct fxlink_filter *filter = NULL;
bool repeat = false;
options.log_file = NULL;
options.verbose = false;
setlocale(LC_ALL, "");
//---
// Command-line argument parsing
//---
enum { LIBUSB_LOG=1 };
enum { LIBUSB_LOG=1, LOG_TO_FILE=2 };
const struct option longs[] = {
{ "help", no_argument, NULL, 'h' },
{ "list", no_argument, NULL, 'l' },
{ "blocks", no_argument, NULL, 'b' },
{ "send", no_argument, NULL, 's' },
{ "interactive", no_argument, NULL, 'i' },
{ "help", no_argument, NULL, 'h' },
{ "list", no_argument, NULL, 'l' },
{ "blocks", no_argument, NULL, 'b' },
{ "send", no_argument, NULL, 's' },
{ "interactive", no_argument, NULL, 'i' },
{ "tui", no_argument, NULL, 't' },
{ "push", no_argument, NULL, 'p' },
{ "libusb-log", required_argument, NULL, LIBUSB_LOG },
{ "fxlink-log", optional_argument, NULL, LOG_TO_FILE },
{ "repeat", no_argument, NULL, 'r' },
{ "verbose", no_argument, NULL, 'v' },
/* Deprecated options ignored for compatibility: */
{ "quiet", no_argument, NULL, 'q' },
{ "unmount", no_argument, NULL, 'u' },
{ NULL },
};
while(option >= 0 && option != '?')
switch((option = getopt_long(argc, argv, "hlbsif:w::", longs, NULL)))
switch((option = getopt_long(argc, argv, "hlbsitpquf:w::rv", longs, NULL)))
{
case 'h':
fprintf(stderr, help_string, argv[0]);
@ -85,6 +106,8 @@ int main(int argc, char **argv)
case 'b':
case 's':
case 'i':
case 't':
case 'p':
mode = option;
break;
case LIBUSB_LOG:
@ -101,6 +124,31 @@ int main(int argc, char **argv)
else fprintf(stderr, "warning: ignoring log level '%s'; should be "
"NONE, ERROR, WARNING, INFO or DEBUG\n", optarg);
break;
case 'q':
/* Ignored: -q, --quiet used to control some messages but is now
supplanted by not setting -v */
break;
case 'u':
/* Ignored: -u, --unmount used to force unmounting filesystems after -s
(which is now the only behavior) */
break;
case 'r':
repeat = true;
delay = delay_infinite();
break;
case 'v':
options.verbose = true;
break;
case LOG_TO_FILE:
if(optarg)
options.log_file = fopen(optarg, "a");
else {
char *name = fxlink_gen_file_name(".", "logfile", ".log");
printf("--fxlink-log will output in '%s'\n", name);
options.log_file = fopen(name, "a");
free(name);
}
break;
case 'w':
if(!optarg) {
delay = delay_infinite();
@ -109,20 +157,25 @@ int main(int argc, char **argv)
char *end;
int seconds = strtol(optarg, &end, 10);
if(seconds < 0 || *end != 0) {
error = err("invalid delay '%s'\n", optarg);
error = elog("invalid delay '%s'\n", optarg);
break;
}
delay = delay_seconds(seconds);
break;
case 'f':
filter = filter_parse(optarg);
filter = fxlink_filter_parse(optarg);
break;
case '?':
error = 1;
}
if(mode == 's' && optind == argc)
error = err("send mode requires additional arguments (file names)");
error = elog("send mode requires additional arguments (file names)\n");
if(mode == 'p' && optind == argc)
error = elog("push mode requires a file name\n");
if(mode == 'p' && optind < argc-1)
error = elog("push mode only accepts one file name\n");
/* No arguments or bad arguments */
if(error)
@ -132,6 +185,10 @@ int main(int argc, char **argv)
return 1;
}
/* Default filter */
if(filter == NULL)
filter = calloc(1, sizeof *filter);
//---
// libusb initialization
//---
@ -139,10 +196,11 @@ int main(int argc, char **argv)
libusb_context *context = NULL;
/* Initialize libusb for corresponding modes */
if(mode == 'l' || mode == 'i') {
if(mode == 'l' || mode == 'i' || mode == 't' || mode == 'p') {
if((rc = libusb_init(&context)))
return libusb_err(rc, "error initializing libusb");
return elog_libusb(rc, "error initializing libusb");
libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, loglevel);
fxlink_log_grab_libusb_logs();
}
//---
@ -156,106 +214,33 @@ int main(int argc, char **argv)
#ifndef FXLINK_DISABLE_UDISKS2
rc = main_blocks(filter, &delay);
#else
rc = err("this fxlink was built without UDisks2; -b is disabled");
rc = elog("this fxlink was built without UDisks2; -b is disabled");
#endif
}
else if(mode == 's') {
#ifndef FXLINK_DISABLE_UDISKS2
rc = main_send(filter, &delay, argv + optind);
#else
rc = err("this fxlink was built without UDisks2; -s is disabled");
rc = elog("this fxlink was built without UDisks2; -s is disabled");
#endif
}
else if(mode == 'i') {
rc = main_interactive(filter, &delay, context);
do {
rc = main_interactive(filter, &delay, context);
}
while(repeat);
}
else if(mode == 't') {
rc = main_tui_interactive(context);
}
else if(mode == 'p') {
rc = main_push(filter, &delay, context, argv + optind);
}
if(context) libusb_exit(context);
fxlink_filter_free(filter);
if(context)
libusb_exit(context);
if(options.log_file)
fclose(options.log_file);
return rc;
}
//---
// Device list
//---
int main_list(filter_t *filter, delay_t *delay, libusb_context *context)
{
/* Wait for a device to be connected */
filter_clean_libusb(filter);
usb_unique_wait(filter, delay, context, NULL);
int total_devices = 0;
bool error;
for_libusb_devices(it, context, &error) {
if(!filter_match(&it.props, filter)) continue;
if(total_devices > 0) printf("\n");
if(it.dc.idProduct == 0x6101)
printf("fx-9860G series (Protocol 7) calculator\n");
else if(it.dc.idProduct == 0x6102)
printf("fx-CG or G-III series (USB Mass Storage) calculator\n");
else
printf("Unknown calculator (idProduct: %04x)\n", it.dc.idProduct);
printf(" Device location: Bus %d, Port %d, Device %d\n",
libusb_get_bus_number(it.dev),
libusb_get_port_number(it.dev),
libusb_get_device_address(it.dev));
printf(" Identification: idVendor: %04x, idProduct: %04x\n",
it.dc.idVendor, it.dc.idProduct);
/* FIXME: This assumes a short path (no hub or dual-device) */
printf(" Guessed sysfs path: /sys/bus/usb/devices/%d-%d/\n",
libusb_get_bus_number(it.dev),
libusb_get_port_number(it.dev));
char *serial = it.dh ? usb_serial_number(it.dh) : NULL;
if(serial)
printf(" Serial number: %s\n", serial);
free(serial);
printf(" Properties: ");
properties_print(stdout, &it.props);
printf("\n");
total_devices++;
}
if(!error && !total_devices)
printf("No%s device found.\n", filter ? " matching" : "");
return 0;
}
//---
// libudev tests; work but not useful yet
//---
#if 0
#include <libudev.h>
int main_udev_test(libusb_device *dev)
{
struct udev *udev = NULL;
struct udev_device *udev_device = NULL;
udev = udev_new();
if(!udev) return err("cannot create udev context");
static char sys_path[128];
sprintf(sys_path, "/sys/bus/usb/devices/%d-%d",
libusb_get_bus_number(dev),
libusb_get_port_number(dev));
udev_device = udev_device_new_from_syspath(udev, sys_path);
if(!udev_device) {
udev_unref(udev);
return err("cannot get udev device for %s", sys_path);
}
printf("Device number: %ld\n", udev_device_get_devnum(udev_device));
printf("Device devnode: %s\n", udev_device_get_devnode(udev_device));
if(udev) udev_unref(udev);
return 0;
}
#endif

167
fxlink/modes/interactive.c Normal file
View File

@ -0,0 +1,167 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "../fxlink.h"
#include <fxlink/filter.h>
#include <fxlink/logging.h>
#include <fxlink/protocol.h>
#include <fxlink/devices.h>
#include <fxlink/tooling/libpng.h>
#include <fxlink/tooling/sdl2.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
static void handle_new_message(struct fxlink_device *fdev,
struct fxlink_message *msg)
{
char const *path = ".";
if(fxlink_message_is_fxlink_image(msg)) {
struct fxlink_message_image_header *img = msg->data;
char *filename = fxlink_gen_file_name(path, msg->type, ".png");
struct fxlink_message_image_raw *raw =
fxlink_message_image_decode(msg);
if(raw) {
fxlink_libpng_save_raw(raw, filename);
fxlink_message_image_raw_free(raw);
hlog("calculators %s", fxlink_device_id(fdev));
log_("saved image (%dx%d, format=%d) to '%s'\n",
img->width, img->height, img->pixel_format, filename);
}
free(filename);
return;
}
if(fxlink_message_is_fxlink_text(msg)) {
char const *str = msg->data;
if(options.verbose)
printf("------------------\n");
fwrite(str, 1, msg->size, stdout);
if(str[msg->size - 1] != '\n') {
if(!options.verbose)
printf("\e[30;47m%%\e[0m");
printf("\n");
}
if(options.verbose) {
printf("------------------\n");
}
if(options.verbose) {
for(size_t i = 0; i < msg->size; i++) {
printf(" %02x", str[i]);
if((i & 15) == 15 || i == msg->size - 1)
printf("\n");
}
}
if(options.log_file)
fwrite(str, 1, msg->size, options.log_file);
return;
}
if(fxlink_message_is_fxlink_video(msg)) {
struct fxlink_message_image_raw *raw =
fxlink_message_image_decode(msg);
if(raw) {
fxlink_sdl2_display_raw(raw);
fxlink_message_image_raw_free(raw);
}
return;
}
/* Default to saving to a blob */
static char combined_type[48];
sprintf(combined_type, "%.16s-%.16s", msg->application, msg->type);
char *filename = fxlink_gen_file_name(path, combined_type, ".bin");
FILE *fp = fopen(filename, "wb");
if(!fp) {
elog("could not save to '%s': %m\n", filename);
return;
}
fwrite(msg->data, 1, msg->size, fp);
fclose(fp);
log_("saved blob to '%s'\n", filename);
free(filename);
}
int main_interactive(struct fxlink_filter *filter, delay_t *delay,
libusb_context *ctx)
{
/* Wait for a device to be connected */
fxlink_filter_clean_libusb(filter);
filter->intf_fxlink = true;
struct fxlink_device *fdev = fxlink_device_find_wait(ctx, filter, delay);
if(!fdev) {
printf("No device found.\n");
return 1;
}
if(!fxlink_device_claim_fxlink(fdev)) {
fxlink_device_cleanup(fdev);
free(fdev);
return 1;
}
hlog("interactive");
log_("connected to %s\n", fxlink_device_id(fdev));
/* Buffer used to receive messages */
static uint8_t buffer[2048];
/* Current message */
struct fxlink_transfer *tr = NULL;
while(1) {
fxlink_sdl2_handle_events();
int transferred = -1;
int rc = libusb_bulk_transfer(fdev->dh, fdev->comm->ep_bulk_IN, buffer,
sizeof buffer, &transferred, 500 /* ms */);
if(rc == LIBUSB_ERROR_NO_DEVICE) {
hlog("interactive");
log_("disconnected, leaving\n");
break;
}
else if(rc && rc != LIBUSB_ERROR_TIMEOUT) {
elog_libusb(rc, "bulk transfer failed on %s",
fxlink_device_id(fdev));
continue;
}
if(transferred <= 0)
continue;
/* Either start a new message or continue an unfinished one */
if(tr == NULL)
tr = fxlink_transfer_make_IN(buffer, transferred);
else
fxlink_transfer_receive(tr, buffer, transferred);
if(tr && fxlink_transfer_complete(tr)) {
struct fxlink_message *msg = fxlink_transfer_finish_IN(tr);
if(msg) {
handle_new_message(fdev, msg);
fxlink_message_free(msg, true);
}
tr = NULL;
}
}
/* Warning for unfinished transfer */
if(tr) {
wlog("unfinished transfer interrupted by disconnection\n");
fxlink_transfer_free(tr);
}
fxlink_device_cleanup(fdev);
free(fdev);
return 0;
}

104
fxlink/modes/list.c Normal file
View File

@ -0,0 +1,104 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "../fxlink.h"
#include <fxlink/devices.h>
#include <fxlink/filter.h>
#include <fxlink/logging.h>
#include <stdio.h>
static int print_devices(struct fxlink_device_list const *list,
struct fxlink_filter const *filter)
{
int total_devices = 0;
for(int i = 0; i < list->count; i++) {
struct fxlink_device const *fdev = &list->devices[i];
if(!fdev->calc)
continue;
struct fxlink_filter properties;
fxlink_device_get_properties(fdev, &properties);
if(!fxlink_filter_match(&properties, filter))
continue;
if(total_devices > 0)
printf("\n");
if(fdev->idProduct == 0x6101)
printf("fx-9860G series (Protocol 7) calculator\n");
else if(fdev->idProduct == 0x6102)
printf("fx-CG or G-III series (USB Mass Storage) calculator\n");
else
printf("Unknown calculator (idProduct: %04x)\n", fdev->idProduct);
printf(" Device location: Bus %d, Port %d, Device %d\n",
libusb_get_bus_number(fdev->dp),
libusb_get_port_number(fdev->dp),
libusb_get_device_address(fdev->dp));
printf(" Identification: idVendor: %04x, idProduct: %04x\n",
fdev->idVendor, fdev->idProduct);
/* FIXME: This assumes a short path (no hub or dual-device) */
printf(" Guessed sysfs path: /sys/bus/usb/devices/%d-%d/\n",
libusb_get_bus_number(fdev->dp),
libusb_get_port_number(fdev->dp));
if(fdev->calc->serial)
printf(" Serial number: %s\n", fdev->calc->serial);
printf(" System: %s (%s)\n",
fxlink_device_system_string(fdev),
fxlink_device_status_string(fdev));
printf(" Interfaces: ");
for(int i = 0; i < fdev->calc->interface_count; i++)
printf(" %02x.%02x",
fdev->calc->classes[i] >> 8, fdev->calc->classes[i] & 0xff);
printf("\n");
printf(" Properties: ");
fxlink_filter_print(stdout, &properties);
printf("\n");
total_devices++;
}
return total_devices;
}
static void discard_logs(int display_fmt, char const *str)
{
(void)display_fmt;
(void)str;
}
int main_list(struct fxlink_filter *filter, delay_t *delay,
libusb_context *ctx)
{
/* Silence all logs for this mode */
fxlink_log_set_handler(discard_logs);
struct fxlink_device_list list;
struct timeval zero_tv = { 0 };
fxlink_device_list_track(&list, ctx);
while(1) {
libusb_handle_events_timeout(ctx, &zero_tv);
fxlink_device_list_refresh(&list);
int n = print_devices(&list, filter);
if(n > 0)
break;
if(delay_cycle(delay)) {
printf("No%s device found.\n", filter ? " matching" : "");
break;
}
}
fxlink_device_list_stop(&list);
return 0;
}

126
fxlink/modes/push.c Normal file
View File

@ -0,0 +1,126 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "../fxlink.h"
#include <fxlink/filter.h>
#include <fxlink/logging.h>
#include <fxlink/devices.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int main_push(struct fxlink_filter *filter, delay_t *delay,
libusb_context *ctx, char **files)
{
int rc = 1;
struct fxlink_device *fdev = NULL;
/* Load binary file */
FILE *fp = fopen(files[0], "rb");
if (!fp) {
printf("error: Unable to open file %s\n", files[0]);
goto end;
}
fseek(fp, 0, SEEK_END);
long fsize = ftell(fp);
// If more than 6MB, abort
if (fsize > 6 * 1024 * 1024) {
printf("error: File is too large (max 6MB)\n");
goto end;
}
fseek(fp, 0, SEEK_SET);
uint8_t *filebuf = malloc(fsize);
fread(filebuf, fsize, 1, fp);
fclose(fp);
/* Wait for a device to be connected */
fxlink_filter_clean_libusb(filter);
filter->intf_cesg502 = true;
fdev = fxlink_device_find_wait(ctx, filter, delay);
if(!fdev) {
printf("No device found.\n");
return 1;
}
/* The device uses CESG502, so drive the interface manually */
if((rc = libusb_claim_interface(fdev->dh, 0))) {
hlog("calculators %s", fxlink_device_id(fdev));
rc = elog_libusb(rc, "cannot claim interface 0");
goto end;
}
hlog("push");
log_("connected to %s\n", fxlink_device_id(fdev));
// Wait to receive "USB loader ready" over USB bulk transfer
uint8_t buf[18];
while (1)
{
int actual_length;
rc = libusb_bulk_transfer(fdev->dh, 0x82, buf, sizeof(buf) - 1,
&actual_length, 0);
buf[sizeof(buf) - 1] = 0;
// if (rc == LIBUSB_ERROR_TIMEOUT) continue;
if (rc) {
rc = elog_libusb(rc, "cannot receive data");
goto end;
}
if (actual_length == 0) continue;
if (actual_length != 17) {
printf("error: Received %d bytes, expected 17\n", actual_length);
goto end;
}
// See if it's the "USB loader ready" message with strcmp
if (strcmp((char*) buf, "USB loader ready") == 0) {
printf("Ready to send!\n");
break;
} else {
printf("error: Unknown message received: %s\n", buf);
goto end;
}
}
// Send the contents of the passed file over USB bulk transfer
// First send the size of the file
uint8_t sizebuf[4];
sizebuf[0] = (fsize >> 24) & 0xFF;
sizebuf[1] = (fsize >> 16) & 0xFF;
sizebuf[2] = (fsize >> 8) & 0xFF;
sizebuf[3] = fsize & 0xFF;
rc = libusb_bulk_transfer(fdev->dh, 0x01, sizebuf, sizeof(sizebuf), NULL,
0);
if (rc) {
rc = elog_libusb(rc, "cannot send size");
goto end;
}
// Then send the file contents
printf("Sending %ld bytes\n", fsize);
int sent = 0;
while (sent < fsize) {
int actual_length;
rc = libusb_bulk_transfer(fdev->dh, 0x01, filebuf + sent, fsize - sent,
&actual_length, 0);
if (rc) {
rc = elog_libusb(rc, "cannot send data");
goto end;
}
sent += actual_length;
}
printf("Sent %d bytes\n", sent);
end:
if(fdev) {
libusb_release_interface(fdev->dh, 0);
fxlink_device_cleanup(fdev);
free(fdev);
}
return rc;
}

125
fxlink/modes/udisks2.c Normal file
View File

@ -0,0 +1,125 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/config.h>
#ifndef FXLINK_DISABLE_UDISKS2
#include "../fxlink.h"
#include <fxlink/tooling/udisks2.h>
#include <fxlink/filter.h>
#include <fxlink/logging.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main_send(struct fxlink_filter *filter, delay_t *delay, char **files)
{
fxlink_filter_clean_udisks2(filter);
GError *error = NULL;
char **argv = NULL;
int rc = 0;
UDisksClient *udc = NULL;
UDisksManager *udm = NULL;
if(ud2_start(&udc, &udm)) return 1;
UDisksBlock *block = NULL;
UDisksDrive *drive = NULL;
UDisksFilesystem *fs = NULL;
rc = ud2_unique_wait(filter, delay, udc, udm, &block, &drive, &fs);
if(rc != FXLINK_FILTER_UNIQUE) {
rc = 1;
goto end;
}
/* Determine a mount folder, mounting the volume if needed */
gchar *folder = NULL;
gchar const *dev = udisks_block_get_device(block);
gchar const * const * mount_points =
udisks_filesystem_get_mount_points(fs);
if(!mount_points || !mount_points[0]) {
GVariant *args = g_variant_new("a{sv}", NULL);
udisks_filesystem_call_mount_sync(fs, args, &folder, NULL, &error);
if(error) {
rc = elog("cannot mount %s: %s\n", dev, error->message);
goto end;
}
printf("Mounted %s to %s.\n", dev, folder);
}
else {
folder = strdup(mount_points[0]);
printf("Already mounted at %s.\n", folder);
}
/* Copy files with external cp(1) */
int file_count = 0;
while(files[file_count]) file_count++;
argv = malloc((file_count + 3) * sizeof *argv);
if(!argv) {
rc = elog("cannot allocate argv array for cp(1)\n");
goto end;
}
argv[0] = "cp";
for(int i = 0; files[i]; i++)
argv[i+1] = files[i];
argv[file_count+1] = folder;
argv[file_count+2] = NULL;
/* Print command */
printf("Running cp");
for(int i = 1; argv[i]; i++) printf(" '%s'", argv[i]);
printf("\n");
pid_t pid = fork();
if(pid == 0) {
execvp("cp", argv);
}
else if(pid == -1) {
rc = elog("failed to fork to invoke cp\n");
goto end;
}
else {
int wstatus;
waitpid(pid, &wstatus, 0);
if(!WIFEXITED(wstatus))
elog("process did not terminate normally\n");
else if(WEXITSTATUS(wstatus) != 0) {
elog("process terminated with error %d\n", WEXITSTATUS(wstatus));
}
}
/* Unmount the filesystem and eject the device */
GVariant *args = g_variant_new("a{sv}", NULL);
udisks_filesystem_call_unmount_sync(fs, args, NULL, &error);
if(error)
elog("while unmounting %s: %s\n", dev, error->message);
else
printf("Unmounted %s.\n", dev);
args = g_variant_new("a{sv}", NULL);
udisks_drive_call_power_off_sync(drive, args, NULL, &error);
if(error)
elog("while ejecting %s: %s\n", dev, error->message);
else
printf("Ejected %s.\n", dev);
end:
free(folder);
if(argv) free(argv);
if(fs) g_object_unref(fs);
if(drive) g_object_unref(drive);
if(block) g_object_unref(block);
if(udc && udm) ud2_end(udc, udm);
return rc;
}
#endif /* FXLINK_DISABLE_UDISKS2 */

View File

@ -1,42 +0,0 @@
#include "png.h"
#include "util.h"
#include <stdio.h>
/* fxlink_png_save(): Save a bitmap into a PNG file */
int fxlink_png_save(png_byte **row_pointers, int width, int height,
char const *path)
{
png_struct *png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL);
if(!png)
return err("failed to write PNG: png_create_write_struct");
png_infop info = png_create_info_struct(png);
if(!info)
return err("failed to write PNG: png_create_info_struct");
FILE *fp = fopen(path, "wb");
if(!fp) {
png_destroy_write_struct(&png, &info);
return err("failed to open '%s' to write PNG: %m", path);
}
if(setjmp(png_jmpbuf(png))) {
fclose(fp);
png_destroy_write_struct(&png, &info);
return 1;
}
png_init_io(png, fp);
png_set_IHDR(png, info,
width, height, 8,
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
png_write_image(png, row_pointers);
png_write_end(png, NULL);
png_destroy_write_struct(&png, &info);
fclose(fp);
return 0;
}

View File

@ -1,14 +0,0 @@
//---
// fxlink:png - Tools to output PNG images with libpng
//---
#ifndef FXLINK_PNG_H
#define FXLINK_PNG_H
#include <png.h>
/* fxlink_png_save(): Save a bitmap into a PNG file */
int fxlink_png_save(png_byte **row_pointers, int width, int height,
char const *path);
#endif /* FXLINK_PNG_H */

View File

@ -1,42 +0,0 @@
#include "properties.h"
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
bool properties_match(properties_t const *props, properties_t const *option)
{
if(option->p7 && !props->p7)
return false;
if(option->mass_storage && !props->mass_storage)
return false;
if(option->series_cg && !props->series_cg)
return false;
if(option->series_g3 && !props->series_g3)
return false;
if(option->serial_number && (!props->serial_number ||
strcmp(option->serial_number, props->serial_number)))
return false;
return true;
}
void properties_print(FILE *fp, properties_t const *props)
{
#define output(...) { \
if(sep) fprintf(fp, ", "); \
fprintf(fp, __VA_ARGS__); \
sep = true; \
}
bool sep = false;
if(props->p7)
output("p7");
if(props->mass_storage)
output("mass_storage");
if(props->series_cg)
output("series_cg");
if(props->series_g3)
output("series_g3");
if(props->serial_number)
output("serial_number=%s", props->serial_number);
}

View File

@ -1,65 +0,0 @@
//---
// fxlink:properties - Detected models and properties of devices
//---
#ifndef FXLINK_PROPERTIES_H
#define FXLINK_PROPERTIES_H
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
/* properties_t: Type of properties that can be detected on a device
This structure lists all the properties that fxlink can detect on connected
devices. These properties help identify the devices in interactive use, and
accurately specify which calculators to interact with when several models
are connected simultaneously or when the same command in run against
different models in a script.
Depending on the backend and access privileges, not all properties can be
detected. The backends supporting each property are listed in brackets at
the end of each description.
An instance of this structure can also be used as a filter. In order for the
semantics of filtering to work out, every attribute needs to have a "total
information order", meaning that two values can be compared for how specific
they are. A device will match a filter if and only if all of its properties
are more specific than the values provided in the filter.
Currently the order is "true more specific than false" for all booleans and
"any value more specific than NULL" for the serial number. Properties that
cannot be detected by back-ends are reset to their least specific value (ie.
ignored). */
typedef struct {
/* The calculator is a Protocol 7 calculator (idProduct: 0x6101). This
makes no sense in UDisks2 as a P7 calculator has no disks, therefore
this property is always false in UDisks2. [libusb, udisks2] */
bool p7;
/* The calculator is a Mass Storage calculator (idProduct: 0x6102). All
devices detected in UDisks2 are of this type. [libusb, udisks2] */
bool mass_storage;
/* The calculator is an fx-CG series. [udisks2] */
bool series_cg;
/* The calculator is a G-III series. [udisks2] */
bool series_g3;
/* Serial number. This can only be obtained in libusb if the user has
write access to the device, because libusb needs to send a request for
the STRING descriptor holding the serial number. [libusb, udisks2] */
char *serial_number;
} properties_t;
/* properties_match(): Check whether some properties match a given option
Returns true if (props) is more specific than (option), meaning that every
property mentioned in (option) is indeed set in (props). This is a building
block for filter_match() and probably doesn't need to be used directly. */
bool properties_match(properties_t const *props, properties_t const *option);
/* properties_print(): Print a property set (one-line) */
void properties_print(FILE *fp, properties_t const *props);
#endif /* FXLINK_PROPERTIES_H */

View File

@ -1,141 +0,0 @@
#include "protocol.h"
#include "util.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <endian.h>
//---
// Image format
//---
static int img_bytes_per_row(int format, int width)
{
if(format == USB_FXLINK_IMAGE_RGB565)
return 2 * width;
if(format == USB_FXLINK_IMAGE_MONO)
return (width + 7) >> 3;
if(format == USB_FXLINK_IMAGE_GRAY)
return 2 * ((width + 7) >> 3);
return 0;
}
static void decode_rgb565(void *pixels, int width, int height, int size,
uint8_t **row_pointers)
{
int bpr = img_bytes_per_row(USB_FXLINK_IMAGE_RGB565, width);
for(int y = 0; y < height; y++) {
void *row = pixels + y * bpr;
for(int x = 0; x < width; x++) {
/* Don't read past the read buffer if the image is incomplete */
void *input = row + 2 * x;
uint16_t color = 0;
if(input - pixels + 2 <= size) color = *(uint16_t *)input;
color = be16toh(color);
row_pointers[y][3*x+0] = (color >> 11) << 3;
row_pointers[y][3*x+1] = ((color >> 5) & 0x3f) << 2;
row_pointers[y][3*x+2] = (color & 0x1f) << 3;
}
}
}
static void decode_mono(void *pixels, int width, int height, int size,
uint8_t **row_pointers)
{
int bpr = img_bytes_per_row(USB_FXLINK_IMAGE_MONO, width);
for(int y = 0; y < height; y++) {
void *row = pixels + y * bpr;
for(int x = 0; x < width; x++) {
/* Don't read past the read buffer if the image is incomplete */
void *input = row + (x >> 3);
int byte = 0;
if(input - pixels + 1 <= size) byte = *(uint8_t *)input;
int color = (byte & (0x80 >> (x & 7))) ? 0 : 255;
row_pointers[y][3*x+0] = color;
row_pointers[y][3*x+1] = color;
row_pointers[y][3*x+2] = color;
}
}
}
static void decode_gray(void *pixels, int width, int height, int size,
uint8_t **row_pointers)
{
int bpr = img_bytes_per_row(USB_FXLINK_IMAGE_MONO, width);
for(int k = 0; k < 2 * height; k++) {
void *row = pixels + k * bpr;
int y = k % height;
for(int x = 0; x < width; x++) {
/* Don't read past the read buffer if the image is incomplete */
void *input = row + (x >> 3);
int byte = 0;
if(input - pixels + 1 <= size) byte = *(uint8_t *)input;
int color = (byte & (0x80 >> (x & 7)));
/* Everything is inverted */
if(!color) color = (k >= height) ? 0xaa : 0x55;
else color = 0x00;
row_pointers[y][3*x+0] += color;
row_pointers[y][3*x+1] += color;
row_pointers[y][3*x+2] += color;
}
}
}
uint8_t **fxlink_protocol_decode_image(message_t *msg)
{
usb_fxlink_image_t *img = (void *)msg->output;
void *pixels = msg->output + sizeof *img;
/* Compute the amount of data for the specified image format and size */
int bytes_per_row = img_bytes_per_row(img->pixel_format, img->width);
int expected_size = img->height * bytes_per_row;
/* Check that the correct amount of data was sent */
int size = msg->size_read - sizeof *img;
if(size < expected_size)
printf("warning: got %d bytes but needed %d, image will be "
"incomplete\n", size, expected_size);
if(size > expected_size)
printf("warning: got %d bytes but needed %d for image, dropping extra"
"\n", size, expected_size);
/* Allocate row pointers */
uint8_t **row_pointers = malloc(img->height*sizeof *row_pointers);
if(!row_pointers) {
err("failed to write allocate memory to decode image");
return NULL;
}
for(size_t y = 0; y < img->height; y++) {
row_pointers[y] = calloc(img->width, 3);
if(!row_pointers[y]) {
err("failed to write allocate memory to decode image");
for(size_t i = 0 ; i < y; i++) free(row_pointers[i]);
free(row_pointers);
return NULL;
}
}
/* Decode image */
if(img->pixel_format == USB_FXLINK_IMAGE_RGB565)
decode_rgb565(pixels, img->width, img->height, size, row_pointers);
if(img->pixel_format == USB_FXLINK_IMAGE_MONO)
decode_mono(pixels, img->width, img->height, size, row_pointers);
if(img->pixel_format == USB_FXLINK_IMAGE_GRAY)
decode_gray(pixels, img->width, img->height, size, row_pointers);
return row_pointers;
}

View File

@ -1,71 +0,0 @@
//---
// fxlink:protocol - Custom fxlink protocol
//---
#ifndef FXLINK_PROTOCOL_H
#define FXLINK_PROTOCOL_H
#include <stdint.h>
#include <stdbool.h>
/* See the gint source for details on the protocol */
typedef struct
{
uint32_t version;
uint32_t size;
uint32_t transfer_size;
char application[16];
char type[16];
} usb_fxlink_header_t;
/* Subheader for the fxlink built-in "image" type */
typedef struct
{
uint32_t width;
uint32_t height;
int pixel_format;
} usb_fxlink_image_t;
/* Pixel formats */
typedef enum
{
/* Image is an array of *big-endian* uint16_t with RGB565 format */
USB_FXLINK_IMAGE_RGB565 = 0,
/* Image is an array of bits in mono format */
USB_FXLINK_IMAGE_MONO,
/* Image is two consecutive mono arrays, one for light, one for dark */
USB_FXLINK_IMAGE_GRAY,
} usb_fxlink_image_format_t;
//---
// Utilities in this implementation
//---
/* Message currently being transferred */
typedef struct
{
usb_fxlink_header_t header;
/* Valid when we are reading a message */
bool valid;
/* Data already read in this message */
uint32_t size_read;
/* Data buffer */
char *output;
} message_t;
/* fxlink_protocol_decode_image(): Decode an image into RGB888 format
This function decodes the message into an RGB888 image and returns an array
of row pointers with the image data (free the array and each element of the
array after use).
If there are not enough bytes in the input, it pads with zeros, and if there
are extra bytes, they are dropped; in both cases a warning is printed. */
uint8_t **fxlink_protocol_decode_image(message_t *msg);
#endif /* FXLINK_PROTOCOL_H */

View File

@ -1,82 +0,0 @@
#include "config.h"
#ifndef FXLINK_DISABLE_SDL2
#include "sdl2.h"
#include "util.h"
static SDL_Window *window = NULL;
static int init(size_t width, size_t height)
{
if(!SDL_WasInit(SDL_INIT_VIDEO)) {
int rc = SDL_Init(SDL_INIT_VIDEO);
if(rc < 0)
return err("Cannot initialize SDL: %s\n", SDL_GetError());
}
window = SDL_CreateWindow("fxlink", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, width, height, 0);
return 0;
}
__attribute__((destructor))
static void quit(void)
{
if(!window)
return;
SDL_DestroyWindow(window);
window = NULL;
}
/* Generate an RGB888 surface from image data. */
static SDL_Surface *surface_for_image(uint8_t **RGB888_rows, int width,
int height)
{
/* Little endian setup for RGB */
SDL_Surface *s = SDL_CreateRGBSurface(0, width, height, 24,
0x000000ff, 0x0000ff00, 0x0000ff00, 0);
if(!s) {
err("Cannot create surface for image");
return NULL;
}
for(int i = 0; i < height; i++)
memcpy(s->pixels + i * s->pitch, RGB888_rows[i], width * 3);
return s;
}
void sdl2_stream(uint8_t **RGB888_rows, int width, int height)
{
if(!window && init(width, height))
return;
int current_w, current_h;
SDL_GetWindowSize(window, &current_w, &current_h);
if(current_w != width || current_h != height)
SDL_SetWindowSize(window, width, height);
SDL_Surface *src = surface_for_image(RGB888_rows, width, height);
if(!src)
return;
SDL_Surface *dst = SDL_GetWindowSurface(window);
SDL_BlitSurface(src, NULL, dst, NULL);
SDL_FreeSurface(src);
SDL_UpdateWindowSurface(window);
}
void sdl2_tick(void)
{
if(!window)
return;
SDL_Event e;
while(SDL_PollEvent(&e)) {
if(e.type == SDL_QUIT)
quit();
}
}
#endif /* FXLINK_DISABLE_SDL2 */

View File

@ -1,23 +0,0 @@
//---
// fxlink:sdl2 - SDL2 functions
//---
#ifndef FXLINK_SDL2_H
#define FXLINK_SDL2_H
#ifndef FXLINK_DISABLE_SDL2
#include <SDL2/SDL.h>
/* sdl2_stream(): Display a streaming image on the window
This function opens or reuses an SDL2 window to display an image. */
void sdl2_stream(uint8_t **RGB888_rows, int width, int height);
/* sdl2_tick(): Handle SDL events
This just needs to be called regularly from the main thread, to respond to
events on the window. */
void sdl2_tick(void);
#endif /* FXLINK_DISABLE_SDL2 */
#endif /* FXLINK_SDL2_H */

46
fxlink/tooling/libpng.c Normal file
View File

@ -0,0 +1,46 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/tooling/libpng.h>
#include <fxlink/logging.h>
int fxlink_libpng_save_raw(struct fxlink_message_image_raw *raw,
char const *path)
{
png_struct *png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL);
if(!png)
return elog("failed to write PNG: png_create_write_struct\n");
png_infop info = png_create_info_struct(png);
if(!info)
return elog("failed to write PNG: png_create_info_struct\n");
FILE *fp = fopen(path, "wb");
if(!fp) {
png_destroy_write_struct(&png, &info);
return elog("failed to open '%s' to write PNG: %m\n", path);
}
if(setjmp(png_jmpbuf(png))) {
fclose(fp);
png_destroy_write_struct(&png, &info);
return 1;
}
png_init_io(png, fp);
png_set_IHDR(png, info,
raw->width, raw->height, 8,
PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
png_write_image(png, raw->data);
png_write_end(png, NULL);
png_destroy_write_struct(&png, &info);
fclose(fp);
return 0;
}

98
fxlink/tooling/sdl2.c Normal file
View File

@ -0,0 +1,98 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/tooling/sdl2.h>
#include <fxlink/logging.h>
#ifdef FXLINK_DISABLE_SDL2
void fxlink_sdl2_display_raw(struct fxlink_message_image_raw const *raw)
{
log_("SDL2 disabled at compiled-time, skipping frame");
}
void fxlink_sdl2_handle_events(void)
{
}
#else /* FXLINK_DISABLE_SDL2 */
static SDL_Window *window = NULL;
static int init(size_t width, size_t height)
{
if(!SDL_WasInit(SDL_INIT_VIDEO)) {
int rc = SDL_Init(SDL_INIT_VIDEO);
if(rc < 0)
return elog("cannot initialize SDL: %s\n", SDL_GetError());
}
window = SDL_CreateWindow("fxlink", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, width, height, 0);
return 0;
}
__attribute__((destructor))
static void quit(void)
{
if(!window)
return;
SDL_DestroyWindow(window);
window = NULL;
}
/* Generate an RGB888 surface from image data. */
static SDL_Surface *surface_for_image(uint8_t **RGB888_rows, int width,
int height)
{
/* Little endian setup for RGB */
SDL_Surface *s = SDL_CreateRGBSurface(0, width, height, 24,
0x000000ff, 0x0000ff00, 0x0000ff00, 0);
if(!s) {
elog("cannot create surface for image\n");
return NULL;
}
for(int i = 0; i < height; i++)
memcpy(s->pixels + i * s->pitch, RGB888_rows[i], width * 3);
return s;
}
void fxlink_sdl2_display_raw(struct fxlink_message_image_raw const *raw)
{
if(!window && init(raw->width, raw->height))
return;
int current_w, current_h;
SDL_GetWindowSize(window, &current_w, &current_h);
if(current_w != raw->width || current_h != raw->height)
SDL_SetWindowSize(window, raw->width, raw->height);
SDL_Surface *src = surface_for_image(raw->data, raw->width, raw->height);
if(!src)
return;
SDL_Surface *dst = SDL_GetWindowSurface(window);
SDL_BlitSurface(src, NULL, dst, NULL);
SDL_FreeSurface(src);
SDL_UpdateWindowSurface(window);
}
void fxlink_sdl2_handle_events(void)
{
if(!window)
return;
SDL_Event e;
while(SDL_PollEvent(&e)) {
if(e.type == SDL_QUIT)
quit();
}
}
#endif /* FXLINK_DISABLE_SDL2 */

View File

@ -1,20 +1,15 @@
#include "config.h"
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/config.h>
#ifndef FXLINK_DISABLE_UDISKS2
#include "ud2.h"
#include "fxlink.h"
#include "util.h"
#include "properties.h"
#include "filter.h"
#include <udisks/udisks.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//---
// UDisks2 utility functions
//---
#include <fxlink/tooling/udisks2.h>
#include <fxlink/filter.h>
#include <fxlink/logging.h>
int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr)
{
@ -22,12 +17,12 @@ int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr)
UDisksClient *udc = udisks_client_new_sync(NULL, &error);
if(error)
return err("cannot open udisks2 client: %s", error->message);
return elog("cannot open udisks2 client: %s\n", error->message);
UDisksManager *udm = udisks_client_get_manager(udc);
if(!udm) {
g_object_unref(udc);
return err("udisks2 daemon does not seem to be running");
return elog("udisks2 daemon does not seem to be running\n");
}
*udc_ptr = udc;
@ -35,8 +30,9 @@ int ud2_start(UDisksClient **udc_ptr, UDisksManager **udm_ptr)
return 0;
}
void ud2_end(UDisksClient *udc, __attribute__((unused)) UDisksManager *udm)
void ud2_end(UDisksClient *udc, UDisksManager *udm)
{
(void)udm;
g_object_unref(udc);
}
@ -47,9 +43,8 @@ gchar **ud2_block_devices(UDisksManager *udm)
GError *error = NULL;
udisks_manager_call_get_block_devices_sync(udm,args,&blocks,NULL,&error);
if(error) {
err("cannot list udisks2 block devices: %s", error->message);
}
if(error)
elog("cannot list udisks2 block devices: %s\n", error->message);
return blocks;
}
@ -81,9 +76,9 @@ bool is_casio_drive(UDisksDrive *drive)
return strstr(udisks_drive_get_vendor(drive), "CASIO") != NULL;
}
properties_t ud2_properties(UDisksDrive *drive)
struct fxlink_filter ud2_properties(UDisksDrive *drive)
{
properties_t props = { 0 };
struct fxlink_filter props = { 0 };
props.p7 = false;
props.mass_storage = true;
@ -94,17 +89,17 @@ properties_t ud2_properties(UDisksDrive *drive)
gchar const *s = udisks_drive_get_serial(drive);
/* LINK sends a 12-byte serial number with four leading 0. Remove them */
if(s && strlen(s) == 12 && !strncmp(s, "0000", 4)) s+= 4;
props.serial_number = (char *)s;
if(s && strlen(s) == 12 && !strncmp(s, "0000", 4)) s += 4;
props.serial = strdup(s);
return props;
}
int ud2_unique_matching(filter_t const *filter, UDisksClient *udc,
int ud2_unique_matching(struct fxlink_filter const *filter, UDisksClient *udc,
UDisksManager *udm, UDisksBlock **block_ptr, UDisksDrive **drive_ptr,
UDisksFilesystem **fs_ptr)
{
int status = FILTER_NONE;
int status = FXLINK_FILTER_NONE;
bool error;
UDisksBlock *block = NULL;
@ -112,11 +107,12 @@ int ud2_unique_matching(filter_t const *filter, UDisksClient *udc,
UDisksFilesystem *fs = NULL;
for_udisks2_devices(it, udc, udm, &error) {
if(!filter_match(&it.props, filter)) continue;
if(!fxlink_filter_match(&it.props, filter))
continue;
/* Already found a device before */
if(status == FILTER_UNIQUE) {
status = FILTER_MULTIPLE;
if(status == FXLINK_FILTER_UNIQUE) {
status = FXLINK_FILTER_MULTIPLE;
g_object_unref(fs);
g_object_unref(drive);
g_object_unref(block);
@ -130,31 +126,37 @@ int ud2_unique_matching(filter_t const *filter, UDisksClient *udc,
block = g_object_ref(it.block);
drive = g_object_ref(it.drive);
fs = g_object_ref(it.fs);
status = FILTER_UNIQUE;
status = FXLINK_FILTER_UNIQUE;
}
if(error)
return FILTER_ERROR;
return FXLINK_FILTER_ERROR;
if(block_ptr) *block_ptr = block;
else g_object_unref(block);
if(block_ptr)
*block_ptr = block;
else if(block)
g_object_unref(block);
if(drive_ptr) *drive_ptr = drive;
else g_object_unref(drive);
if(drive_ptr)
*drive_ptr = drive;
else if(drive)
g_object_unref(drive);
if(fs_ptr) *fs_ptr = fs;
else g_object_unref(fs);
if(fs_ptr)
*fs_ptr = fs;
else if(fs)
g_object_unref(fs);
return status;
}
int ud2_unique_wait(filter_t const *filter, delay_t *delay, UDisksClient *udc,
UDisksManager *udm, UDisksBlock **block, UDisksDrive **drive,
UDisksFilesystem **fs)
int ud2_unique_wait(struct fxlink_filter const *filter, delay_t *delay,
UDisksClient *udc, UDisksManager *udm, UDisksBlock **block,
UDisksDrive **drive, UDisksFilesystem **fs)
{
while(true) {
int rc = ud2_unique_matching(filter, udc, udm, block, drive, fs);
if(rc != FILTER_NONE) return rc;
if(delay_cycle(delay)) return FILTER_NONE;
if(rc != FXLINK_FILTER_NONE) return rc;
if(delay_cycle(delay)) return FXLINK_FILTER_NONE;
udisks_client_settle(udc);
}
}
@ -223,9 +225,9 @@ void ud2_iter_next(ud2_iterator_t *it)
// Main functions
//---
int main_blocks(filter_t *filter, delay_t *delay)
int main_blocks(struct fxlink_filter *filter, delay_t *delay)
{
filter_clean_udisks2(filter);
fxlink_filter_clean_udisks2(filter);
UDisksClient *udc = NULL;
UDisksManager *udm = NULL;
@ -237,7 +239,8 @@ int main_blocks(filter_t *filter, delay_t *delay)
bool error;
for_udisks2_devices(it, udc, udm, &error) {
if(!filter_match(&it.props, filter)) continue;
if(!fxlink_filter_match(&it.props, filter))
continue;
if(total_devices > 0) printf("\n");
@ -254,8 +257,8 @@ int main_blocks(filter_t *filter, delay_t *delay)
udisks_drive_get_vendor(it.drive),
udisks_drive_get_model(it.drive));
if(it.props.serial_number)
printf(" Serial number: %s\n", it.props.serial_number);
if(it.props.serial)
printf(" Serial number: %s\n", it.props.serial);
gchar const * const * mount_points =
udisks_filesystem_get_mount_points(it.fs);
@ -267,7 +270,7 @@ int main_blocks(filter_t *filter, delay_t *delay)
}
printf(" Properties: ");
properties_print(stdout, &it.props);
fxlink_filter_print(stdout, &it.props);
printf("\n");
total_devices++;
@ -280,104 +283,4 @@ int main_blocks(filter_t *filter, delay_t *delay)
return 0;
}
int main_send(filter_t *filter, delay_t *delay, char **files)
{
filter_clean_udisks2(filter);
GError *error = NULL;
char **argv = NULL;
int rc = 0;
UDisksClient *udc = NULL;
UDisksManager *udm = NULL;
if(ud2_start(&udc, &udm)) return 1;
UDisksBlock *block = NULL;
UDisksDrive *drive = NULL;
UDisksFilesystem *fs = NULL;
rc = ud2_unique_wait(filter, delay, udc, udm, &block, &drive, &fs);
if(rc != FILTER_UNIQUE) {
rc = 1;
goto end;
}
/* Determine a mount folder, mounting the volume if needed */
gchar *folder = NULL;
bool mounted_here = false;
gchar const *dev = udisks_block_get_device(block);
gchar const * const * mount_points =
udisks_filesystem_get_mount_points(fs);
if(!mount_points || !mount_points[0]) {
GVariant *args = g_variant_new("a{sv}", NULL);
udisks_filesystem_call_mount_sync(fs, args, &folder, NULL, &error);
if(error) {
rc = err("cannot mount %s: %s", dev, error->message);
goto end;
}
printf("Mounted %s to %s.\n", dev, folder);
mounted_here = true;
}
else {
folder = strdup(mount_points[0]);
printf("Already mounted at %s.\n", folder);
mounted_here = false;
}
/* Copy files with external cp(1) */
int file_count = 0;
while(files[file_count]) file_count++;
argv = malloc((file_count + 3) * sizeof *argv);
if(!argv) {
rc = err("cannot allocate argv array for cp(1)");
goto end;
}
argv[0] = "cp";
for(int i = 0; files[i]; i++)
argv[i+1] = files[i];
argv[file_count+1] = folder;
argv[file_count+2] = NULL;
/* Print command */
printf("Running cp");
for(int i = 1; argv[i]; i++) printf(" '%s'", argv[i]);
printf("\n");
pid_t pid = fork();
if(pid == 0) {
execvp("cp", argv);
}
else if(pid == -1) {
rc = err("failed to fork to invoke cp");
goto end;
}
else {
waitpid(pid, NULL, 0);
}
/* Unmount the filesystem and eject the device if we mounted it */
if(mounted_here) {
GVariant *args = g_variant_new("a{sv}", NULL);
udisks_filesystem_call_unmount_sync(fs, args, NULL, &error);
if(error) err("while unmounting %s: %s", dev, error->message);
else printf("Unmounted %s.\n", dev);
args = g_variant_new("a{sv}", NULL);
udisks_drive_call_power_off_sync(drive, args, NULL, &error);
if(error) err("while ejecting %s: %s", dev, error->message);
else printf("Ejected %s.\n", dev);
}
end:
free(folder);
if(argv) free(argv);
if(fs) g_object_unref(fs);
if(drive) g_object_unref(drive);
if(block) g_object_unref(block);
if(udc && udm) ud2_end(udc, udm);
return rc;
}
#endif /* FXLINK_DISABLE_UDISKS2 */

379
fxlink/tui/command-util.c Normal file
View File

@ -0,0 +1,379 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "tui.h"
#include "command-util.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>
//---
// Command parsing utilities
//---
struct fxlink_tui_cmd fxlink_tui_cmd_parse(char const *input)
{
struct fxlink_tui_cmd cmd;
cmd.argc = 0;
cmd.argv = NULL;
cmd.data = malloc(strlen(input) + 1);
if(!cmd.data)
return cmd;
char const *escapes1 = "\\nter";
char const *escapes2 = "\\\n\t\e\r";
/* Whether a new word needs to be created at the next character */
bool word_finished = true;
/* Offset into cmd.data */
int i = 0;
/* Read words eagerly, appending to cmd.data as we go */
for(int j = 0; input[j]; j++) {
int c = input[j];
/* Stop words at spaces */
if(isspace(c)) {
if(!word_finished)
cmd.data[i++] = 0;
word_finished = true;
continue;
}
/* Translate escapes */
if(c == '\\') {
char *p = strchr(escapes1, input[j+1]);
if(p) {
c = escapes2[p - escapes1];
j++;
}
}
/* Add a new word if necessary */
if(word_finished) {
cmd.argv = realloc(cmd.argv, (++cmd.argc) * sizeof *cmd.argv);
cmd.argv[cmd.argc - 1] = cmd.data + i;
word_finished = false;
}
/* Copy literals */
cmd.data[i++] = c;
}
cmd.data[i++] = 0;
cmd.argv = realloc(cmd.argv, (cmd.argc + 1) * sizeof *cmd.argv);
cmd.argv[cmd.argc] = 0;
return cmd;
}
void fxlink_tui_cmd_dump(struct fxlink_tui_cmd const *cmd)
{
print(TUI.wConsole, "[%d]", cmd->argc);
for(int i = 0; i < cmd->argc; i++) {
char const *arg = cmd->argv[i];
print(TUI.wConsole, " '%s'(%d)", arg, (int)strlen(arg));
}
print(TUI.wConsole, "\n");
}
void fxlink_tui_cmd_free(struct fxlink_tui_cmd const *cmd)
{
free(cmd->argv);
free(cmd->data);
}
static struct fxlink_device *find_connected_device(void)
{
/* TODO: Use the "selected" device */
for(int i = 0; i < TUI.devices.count; i++) {
if(TUI.devices.devices[i].status == FXLINK_FDEV_STATUS_CONNECTED)
return &TUI.devices.devices[i];
}
return NULL;
}
bool fxlink_tui_parse_args(int argc, char const **argv, char const *fmt, ...)
{
va_list args;
va_start(args, fmt);
int i = 0;
char const *spec = fmt;
bool got_variadic = false;
for(; *spec; i++, spec++) {
/* Implicit/silent specifiers */
if(*spec == 'd') {
struct fxlink_device **ptr = va_arg(args, struct fxlink_device **);
*ptr = find_connected_device();
if(!*ptr) {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "no device connected\n");
goto failure;
}
/* Bad */
i--;
continue;
}
/* No specifier that consumes stuff allowed after '*' */
if(got_variadic) {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "got specifiers '%s' after '*'\n", spec);
goto failure;
}
/* Specifiers allowed even when there is no argument left */
if(*spec == '*') {
char const ***ptr = va_arg(args, char const ***);
*ptr = argv + i;
got_variadic = true;
continue;
}
/* Argument required beyond this point */
if(i >= argc && *spec != '*') {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "too few arguments\n");
goto failure;
}
/* Standard specifiers */
if(*spec == 's') {
char const **ptr = va_arg(args, char const **);
*ptr = argv[i];
}
else if(*spec == 'i') {
int *ptr = va_arg(args, int *);
char *endptr;
long l = strtol(argv[i], &endptr, 0);
if(*endptr) {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "not a valid integer: '%s'\n", argv[i]);
goto failure;
}
*ptr = l;
}
}
va_end(args);
return true;
failure:
va_end(args);
return false;
}
//---
// Command tree
//---
struct node {
/* Command or subtree name */
char *name;
/* true if tree node, false if raw command */
bool is_tree;
union {
struct node *children; /* is_subtree = true */
int (*func)(int argc, char const **argv); /* is_subtree = false */
};
/* Next sibling */
struct node *next;
};
static struct node *node_mkcmd(char const *name, int (*func)())
{
assert(name);
struct node *cmd = calloc(1, sizeof *cmd);
cmd->name = strdup(name);
cmd->func = func;
return cmd;
}
static struct node *node_mktree(char const *name)
{
assert(name);
struct node *tree = calloc(1, sizeof *tree);
tree->is_tree = true;
tree->name = strdup(name);
return tree;
}
static void node_free(struct node *node);
static void node_free_chain(struct node *node)
{
struct node *next;
while(node) {
next = node->next;
node_free(node);
node = next;
}
}
static void node_free(struct node *node)
{
free(node->name);
if(node->is_tree) {
node_free_chain(node->children);
free(node);
}
}
static void node_tree_add(struct node *tree, struct node *node)
{
assert(tree->is_tree);
node->next = tree->children;
tree->children = node;
}
static struct node *node_tree_get(struct node const *tree, char const *name)
{
assert(tree->is_tree);
for(struct node *n = tree->children; n; n = n->next) {
if(!strcmp(n->name, name))
return n;
}
return NULL;
}
static struct node *node_tree_get_or_make_subtree(struct node *tree,
char const *name)
{
assert(tree->is_tree);
struct node *n = node_tree_get(tree, name);
if(n)
return n;
n = node_mktree(name);
node_tree_add(tree, n);
return n;
}
static struct node *node_tree_get_path(struct node *tree, char const **path,
int *path_end_index)
{
assert(tree->is_tree);
struct node *n = node_tree_get(tree, path[0]);
if(!n)
return NULL;
(*path_end_index)++;
if(!n->is_tree)
return n;
if(!path[1]) {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "'%s' takes a sub-command argument\n", path[0]);
return NULL;
}
return node_tree_get_path(n, path+1, path_end_index);
}
static void node_insert_command(struct node *tree, char const **path,
int (*func)(), int i)
{
assert(tree->is_tree);
if(!path[i]) {
fprintf(stderr, "error: cannot register empty command!\n");
return;
}
else if(!path[i+1]) {
struct node *cmd = node_tree_get(tree, path[i]);
if(cmd) {
fprintf(stderr, "error: '%s' already registred!\n", path[i]);
return;
}
node_tree_add(tree, node_mkcmd(path[i], func));
}
else {
struct node *subtree = node_tree_get_or_make_subtree(tree, path[i]);
if(!subtree->is_tree) {
fprintf(stderr, "error: '%s' is not a category!\n", path[i]);
return;
}
return node_insert_command(subtree, path, func, i+1);
}
}
static void node_dump(struct node const *node, int indent)
{
print(TUI.wConsole, "%*s", 2*indent, "");
if(node->is_tree) {
print(TUI.wConsole, "%s\n", node->name);
struct node *child = node->children;
while(child) {
node_dump(child, indent+1);
child = child->next;
}
}
else {
print(TUI.wConsole, "%s: %p\n", node->name, node->func);
}
}
static struct node *cmdtree = NULL;
void fxlink_tui_register_cmd(char const *name,
int (*func)(int argc, char const **argv))
{
int i = 0;
while(name[i] && (isalpha(name[i]) || strchr("?/-_ ", name[i])))
i++;
if(name[i] != 0) {
fprintf(stderr, "error: invalid command path '%s'\n", name);
return;
}
if(!cmdtree)
cmdtree = node_mktree("(root)");
/* Parse as a command because why not */
struct fxlink_tui_cmd path = fxlink_tui_cmd_parse(name);
node_insert_command(cmdtree, path.argv, func, 0);
fxlink_tui_cmd_free(&path);
}
__attribute__((destructor))
static void free_command_tree(void)
{
node_free(cmdtree);
cmdtree = NULL;
}
void TUI_execute_command(char const *command)
{
struct fxlink_tui_cmd cmd = fxlink_tui_cmd_parse(command);
if(cmd.argc < 1)
goto end;
int args_index = 0;
struct node *node = node_tree_get_path(cmdtree, cmd.argv, &args_index);
if(node) {
node->func(cmd.argc - args_index, cmd.argv + args_index);
/* ignore return code? */
}
else {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "unrecognized command: ");
fxlink_tui_cmd_dump(&cmd);
}
end:
fxlink_tui_cmd_free(&cmd);
}
FXLINK_COMMAND("?cmdtree")
{
node_dump(cmdtree, 0);
return 0;
}

157
fxlink/tui/command-util.h Normal file
View File

@ -0,0 +1,157 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tui.command-util: Preprocessor black magic for command definition
//
// This header provides the following method for declaring TUI commands that
// are automatically registered at startup, and are invoked with arguments
// pre-parsed:
//
// FXLINK_COMMAND("<NAME>", <TYPE>(<NAME>), <TYPE>(<NAME>), ...) {
// /* normal code... */
// return <STATUS>;
// }
//
// The command name is a string. It can have multiple space-separated words as
// in "gintctl test", in which case it is matched against argv[0], argv[1], etc
// and the prefix ("gintctl") is automatically made into a sub-category command
// with relevant error messages.
//
// Each argument has a type and a name, as in INT(x). The type carries
// information on the parsing method, the acceptable range, and of course the
// actual runtime type of the argument. Available types are:
//
// Name Runtime type Meaning and range
// --------------------------------------------------------------------------
// INT int Any integer
// STRING char const * Any string argument from argv[]
// VARIADIC char const ** End of the argv array (NULL-terminated)
// --------------------------------------------------------------------------
// DEVICE struct fxlink_device * Selected device (implicit; never NULL)
// --------------------------------------------------------------------------
//
// The function returns a status code, which is an integer. The entire command
// declaration might look like:
//
// FXLINK_COMMAND("gintctl test", INT(lower_bound), INT(upper_bound)) {
// int avg = (lower_bound + upper_bound) / 2;
// return 0;
// }
//
// I considered doing the entire thing in C++, but absolute preprocessor abuse
// is fun once in a while.
//---
#include <fxlink/defs.h>
//---
// Shell-like command parsing (without the features)
//---
struct fxlink_tui_cmd {
int argc;
char const **argv;
char *data;
};
/* Parse a string into an argument vector */
struct fxlink_tui_cmd fxlink_tui_cmd_parse(char const *input);
/* Dump a command to TUI console for debugging */
void fxlink_tui_cmd_dump(struct fxlink_tui_cmd const *cmd);
/* Free a command */
void fxlink_tui_cmd_free(struct fxlink_tui_cmd const *cmd);
//---
// Command registration and argument scanning
//---
/* Parse a list of arguments into structured data. The format is a string of
argument specifiers, each of which can be:
s String (char *)
d Integer (int)
* Other variadic arguments (char **)
(-- will probably be expanded later.)
Returns true if parsing succeeded, false otherwise (including if arguments
are missing) after printing an error message. */
bool fxlink_tui_parse_args(int argc, char const **argv, char const *fmt, ...);
/* Register a command with the specified name and invocation function. This can
be called manually or generated (along with the parser) using the macro
FXLINK_COMMAND. */
void fxlink_tui_register_cmd(char const *name,
int (*func)(int argc, char const **argv));
/* Apply a macro to every variadic argument. _M1 is applied to the first
argument and _Mn is applied to all subsequent arguments. */
#define MAPn(_M1,_Mn,...) __VA_OPT__(MAP_1(_M1,_Mn,__VA_ARGS__))
#define MAP_1(_M1,_Mn,_X,...) _M1(_X) __VA_OPT__(MAP_2(_M1,_Mn,__VA_ARGS__))
#define MAP_2(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_3(_M1,_Mn,__VA_ARGS__))
#define MAP_3(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_4(_M1,_Mn,__VA_ARGS__))
#define MAP_4(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_5(_M1,_Mn,__VA_ARGS__))
#define MAP_5(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_6(_M1,_Mn,__VA_ARGS__))
#define MAP_6(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_7(_M1,_Mn,__VA_ARGS__))
#define MAP_7(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_8(_M1,_Mn,__VA_ARGS__))
#define MAP_8(_M1,_Mn,_X,...) _Mn(_X) __VA_OPT__(MAP_MAX_8_ARGS(_))
#define MAP_MAX_8_ARGS()
/* Simpler version where the same macro is applied to all arguments */
#define MAP(_M, ...) MAPn(_M, _M, ##__VA_ARGS__)
/* Command declaration macro. Builds an invocation function and a registration
function so the command name doesn't have to be repeated. */
#define FXLINK_COMMAND(_NAME, ...) DO_COMMAND1(_NAME, __COUNTER__, __VA_ARGS__)
/* This call forces __COUNTER__ to expand */
#define DO_COMMAND1(...) DO_COMMAND(__VA_ARGS__)
#define DO_COMMAND(_NAME, _COUNTER, ...) \
static int ___command_ ## _COUNTER(); \
static int ___invoke_command_ ## _COUNTER \
(int ___argc, char const **___argv) { \
MAP(MKVAR, ##__VA_ARGS__) \
if(!fxlink_tui_parse_args(___argc, ___argv, \
"" MAP(MKFMT, ##__VA_ARGS__) \
MAP(MKPTR, ##__VA_ARGS__))) return 1; \
return ___command_ ## _COUNTER( \
MAPn(MKCALL_1, MKCALL_n, ##__VA_ARGS__)); \
} \
__attribute__((constructor)) \
static void ___declare_command_ ## _COUNTER (void) { \
fxlink_tui_register_cmd(_NAME, ___invoke_command_ ## _COUNTER); \
} \
static int ___command_ ## _COUNTER(MAPn(MKFML_1, MKFML_n, ##__VA_ARGS__))
/* Make the format string for an argument */
#define MKFMT(_TV) MKFMT_ ## _TV
#define MKFMT_INT(_X) "i"
#define MKFMT_STRING(_X) "s"
#define MKFMT_VARIADIC(_X) "*"
#define MKFMT_DEVICE(_X) "d"
/* Make the formal function parameter for an argument */
#define MKFML_1(_TV) MKFML_ ## _TV
#define MKFML_n(_TV) , MKFML_1(_TV)
#define MKFML_INT(_X) int _X
#define MKFML_STRING(_X) char const * _X
#define MKFML_VARIADIC(_X) char const ** _X
#define MKFML_DEVICE(_X) struct fxlink_device * _X
/* Create a variable */
#define MKVAR(_TV) MKFML_1(_TV);
/* Make a pointer to an argument (sadly we can't get the name directly) */
#define MKPTR(_TV) , MKPTR_ ## _TV
#define MKPTR_INT(_X) &_X
#define MKPTR_STRING(_X) &_X
#define MKPTR_VARIADIC(_X) &_X
#define MKPTR_DEVICE(_X) &_X
/* Pass a variable as a function argument */
#define MKCALL_1(_TV) MKCALL_ ## _TV
#define MKCALL_n(_TV) , MKCALL_1(_TV)
#define MKCALL_INT(_X) _X
#define MKCALL_STRING(_X) _X
#define MKCALL_VARIADIC(_X) _X
#define MKCALL_DEVICE(_X) _X

273
fxlink/tui/commands.c Normal file
View File

@ -0,0 +1,273 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "tui.h"
#include "command-util.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>
//---
// Standard commands
//---
FXLINK_COMMAND("/echo", DEVICE(fdev), VARIADIC(argv))
{
int l = 5, j = 5;
for(int i = 0; argv[i]; i++)
l += strlen(argv[i]) + 1;
char *concat = malloc(l + 1);
strcpy(concat, "echo ");
for(int i = 0; argv[i]; i++) {
strcpy(concat + j, argv[i]);
j += strlen(argv[i]);
concat[j++] = (argv[i+1] == NULL) ? '\n' : ' ';
}
concat[j] = '\0';
fxlink_device_start_bulk_OUT(fdev,
"fxlink", "command", concat, l, true);
return 0;
}
FXLINK_COMMAND("/identify", DEVICE(fdev))
{
fxlink_device_start_bulk_OUT(fdev,
"fxlink", "command", "identify", 8, false);
return 0;
}
//---
// gintctl commands
//---
static char const *lipsum =
"When the war of the beasts brings about the world's end,\n"
"The goddess descends from the sky.\n"
"Wings of light and dark spread afar,\n"
"She guides us to bliss, her gift everlasting.\n"
"\n"
"Infinite in mystery is the gift of the goddess.\n"
"We seek it thus, and take to the sky.\n"
"Ripples form on the water's surface;\n"
"The wandering soul knows no rest.\n"
"\n"
"There is no hate, only joy,\n"
"For you are beloved by the goddess.\n"
"Hero of the dawn, healer of worlds,\n"
"Dreams of the morrow hath the shattered soul.\n"
"Pride is lost -- wings stripped away, the end is nigh.\n"
"\n"
"My friend, do you fly away now?\n"
"To a world that abhors you and I?\n"
"All that awaits you is a somber morrow\n"
"No matter where the winds may blow.\n"
"My friend, your desire\n"
"Is the bringer of life, the gift of the goddess.\n"
"Even if the morrow is barren of promises,\n"
"Nothing shall forestall my return.\n"
"\n"
"My friend, the fates are cruel.\n"
"There are no dreams, no honor remains.\n"
"The arrow has left the bow of the goddess.\n"
"My soul, corrupted by vengeance,\n"
"Hath endured torment to find the end of the journey\n"
"In my own salvation and your eternal slumber.\n"
"Legend shall speak of sacrifice at world's end\n"
"The wind sails over the water's surface, quietly, but surely.\n"
"\n"
"Even if the morrow is barren of promises,\n"
"Nothing shall forestall my return.\n"
"To become the dew that quenches the lands,\n"
"To spare the sands, the seas, the skies,\n"
"I offer thee this silent sacrifice.\n";
FXLINK_COMMAND("gintctl echo-bounds", DEVICE(fdev), INT(count))
{
if(count < 0 || count > 8192) {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "count should be 0..8192 (not %d)\n", count);
return 1;
}
uint32_t *data = malloc(count * 4);
for(int i = 0; i < count; i++)
data[i] = i;
fxlink_device_start_bulk_OUT(fdev,
"gintctl", "echo-bounds", data, count * 4, true);
return 0;
}
FXLINK_COMMAND("gintctl garbage", DEVICE(fdev), INT(count))
{
if(count < 0 || count > 8192) {
fprint(TUI.wConsole, FMT_RED, "error: ");
print(TUI.wConsole, "count should be 0..8192 (not %d)\n", count);
return 1;
}
uint32_t *data = malloc(count * 4);
for(int i = 0; i < count; i++)
data[i] = i + 0xdead0000;
fxlink_device_start_bulk_OUT(fdev,
"gintctl", "garbage", data, count * 4, true);
return 0;
}
static void status(bool b, char const *fmt, ...)
{
va_list args;
va_start(args, fmt);
fprint(TUI.wConsole, b ? FMT_GREEN : FMT_RED, b ? "<PASSED> ":"<FAILED> ");
vw_printw(TUI.wConsole, fmt, args);
va_end(args);
}
static void unit_echo(struct fxlink_device *fdev, char const *str,
char const *description)
{
char *echo = malloc(5 + strlen(str) + 1);
strcpy(echo, "echo ");
strcat(echo, str);
fxlink_device_start_bulk_OUT(fdev,
"fxlink", "command", echo, strlen(echo), true);
struct fxlink_message *msg = NULL;
while(TUI_wait_message(fdev, "fxlink", "text", &msg)) {
bool success =
msg->size == strlen(str)
&& !strncmp(msg->data, str, msg->size);
if(description)
status(success, "%s\n", description);
else
status(success, "echo of '%s': '%.*s' (%d)\n", str, msg->size,
(char *)msg->data, msg->size);
}
}
static void unit_echo_bounds(struct fxlink_device *fdev, int count)
{
char reference[256];
sprintf(reference, "first=%08x last=%08x total=%d B\n",
0, count-1, 4*count);
uint32_t *data = malloc(count * 4);
for(int i = 0; i < count; i++)
data[i] = i;
fxlink_device_start_bulk_OUT(fdev,
"gintctl", "echo-bounds", data, count * 4, true);
struct fxlink_message *msg = NULL;
while(TUI_wait_message(fdev, "fxlink", "text", &msg)) {
bool success =
msg->size == strlen(reference)
&& !strncmp(msg->data, reference, msg->size);
status(success, "echo bounds %d B\n", count * 4);
}
}
static void unit_read_unaligned(struct fxlink_device *fdev, char const *str,
int kind)
{
char *payload = malloc(strlen(str) + 2);
sprintf(payload, "%c%s", kind, str);
fxlink_device_start_bulk_OUT(fdev,
"gintctl", "read-unaligned", payload, strlen(payload), true);
struct fxlink_message *msg = NULL;
while(TUI_wait_message(fdev, "fxlink", "text", &msg)) {
bool success =
msg->size == strlen(str)
&& !strncmp(msg->data, str, msg->size);
if(strlen(str) < 20)
status(success, "unaligned echo type '%c' of '%s'\n", kind, str);
else
status(success, "unaligned echo type '%c' of %d-byte string\n",
kind, strlen(str));
}
}
static void test_read_basic(struct fxlink_device *fdev)
{
unit_echo(fdev, "123", NULL);
unit_echo(fdev, "1234", NULL);
unit_echo(fdev, "12345", NULL);
unit_echo(fdev, "123456", NULL);
unit_echo(fdev, lipsum, "echo of better lorem ipsum");
}
static void test_read_buffers(struct fxlink_device *fdev)
{
/* 128 and 384 bytes -> less than a packet */
unit_echo_bounds(fdev, 32);
unit_echo_bounds(fdev, 96);
/* 512 bytes -> exactly one packet */
unit_echo_bounds(fdev, 128);
unit_echo_bounds(fdev, 128);
unit_echo_bounds(fdev, 128);
/* 516 and 768 -> one packet and a short one */
unit_echo_bounds(fdev, 129);
unit_echo_bounds(fdev, 192);
/* 1024 bytes -> exactly two packets */
unit_echo_bounds(fdev, 256);
/* 2044 bytes -> just shy of a full buffer */
unit_echo_bounds(fdev, 511);
/* 2048 bytes -> a full buffer */
unit_echo_bounds(fdev, 512);
unit_echo_bounds(fdev, 512);
/* 2300 bytes -> more than a full buffer */
unit_echo_bounds(fdev, 575);
/* 6000 bytes -> non-integral number of full buffers but more than 2 */
unit_echo_bounds(fdev, 1500);
/* 8192 bytes -> "large" amount of full buffers */
unit_echo_bounds(fdev, 2048);
}
static void test_read_unaligned(struct fxlink_device *fdev)
{
char const *alpha = "aBcDeFgHiJkLmNoPqR";
for(int i = 1; i <= 9; i++)
unit_read_unaligned(fdev, alpha, '0' + i);
unit_read_unaligned(fdev, alpha, 'i');
unit_read_unaligned(fdev, alpha, 'r');
for(int i = 1; i <= 9; i++)
unit_read_unaligned(fdev, lipsum, '0' + i);
unit_read_unaligned(fdev, lipsum, 'i');
unit_read_unaligned(fdev, lipsum, 'r');
}
FXLINK_COMMAND("gintctl test read-basic", DEVICE(fdev))
{
test_read_basic(fdev);
return 0;
}
FXLINK_COMMAND("gintctl test read-buffers", DEVICE(fdev))
{
test_read_buffers(fdev);
return 0;
}
FXLINK_COMMAND("gintctl test read-unaligned", DEVICE(fdev))
{
test_read_unaligned(fdev);
return 0;
}
FXLINK_COMMAND("gintctl test all", DEVICE(fdev))
{
test_read_basic(fdev);
test_read_buffers(fdev);
test_read_unaligned(fdev);
return 0;
}

338
fxlink/tui/input.c Normal file
View File

@ -0,0 +1,338 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/tui/input.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
//---
// Text manipulation functions
//---
bool fxlink_TUI_input_init(struct fxlink_TUI_input *in, WINDOW *win,
int prealloc_size)
{
char *data = malloc(prealloc_size + 1);
if(!data)
return false;
data[0] = 0;
in->data = data;
in->size = 0;
in->alloc_size = prealloc_size + 1;
in->cursor = 0;
in->win = win;
getyx(win, in->wy, in->wx);
scrollok(win, false);
return true;
}
void fxlink_TUI_input_free(struct fxlink_TUI_input *in)
{
free(in->data);
memset(in, 0, sizeof *in);
}
bool fxlink_TUI_input_alloc(struct fxlink_TUI_input *in, int n)
{
if(in->alloc_size >= n + 1)
return true;
/* Always increase the size by at least 16 so we can insert many times in a
row without worrying about excessive allocations. */
int newsize = (in->alloc_size + 16 > n + 1) ? in->alloc_size + 16 : n + 1;
char *newdata = realloc(in->data, newsize);
if(!newdata)
return false;
in->data = newdata;
in->alloc_size = newsize;
return true;
}
bool fxlink_TUI_input_insert(struct fxlink_TUI_input *in, int p,
char const *str, int n)
{
if(p < 0 || p > in->size)
return false;
if(!fxlink_TUI_input_alloc(in, in->size + n))
return false;
/* Move the end of the string (plus the NUL) n bytes forward */
memmove(in->data + p + n, in->data + p, in->size - p + 1);
memcpy(in->data + p, str, n);
in->size += n;
return true;
}
int fxlink_TUI_input_delete(struct fxlink_TUI_input *in, int p, int n)
{
if(n >= in->size - p)
n = in->size - p;
if(n < 0)
return 0;
/* Move the end of the string (plus the NUL) n bytes backwards */
memmove(in->data + p, in->data + p + n, in->size - n - p + 1);
in->size -= n;
return n;
}
//--
// Rendering functions
//---
bool fxlink_TUI_input_echo(struct fxlink_TUI_input *in, chtype ch)
{
int x, y, w, h;
getyx(in->win, y, x);
getmaxyx(in->win, h, w);
bool needs_scroll = (x == w-1 && y == h-1);
bool can_scroll = in->wy > 0;
if(!needs_scroll) {
waddch(in->win, ch);
return true;
}
else if(can_scroll) {
waddch(in->win, ch);
scrollok(in->win, true);
scroll(in->win);
scrollok(in->win, false);
wmove(in->win, y, 0);
in->wy--;
return true;
}
else {
return false;
}
}
void fxlink_TUI_input_clear(struct fxlink_TUI_input *in)
{
int w, h;
getmaxyx(in->win, h, w);
/* Clear from the end of screen to the start of the input */
int current_y = h - 1;
while(current_y > in->wy) {
mvwhline(in->win, current_y, 0, ' ', w);
current_y--;
}
mvwhline(in->win, in->wy, in->wx, ' ', w - in->wx);
}
void fxlink_TUI_input_redraw(struct fxlink_TUI_input *in)
{
fxlink_TUI_input_clear(in);
int cursor_x, cursor_y;
waddnstr(in->win, in->data, in->cursor);
getyx(in->win, cursor_y, cursor_x);
waddnstr(in->win, in->data + in->cursor, in->size - in->cursor);
wmove(in->win, cursor_y, cursor_x);
}
void fxlink_TUI_input_clearscreen(struct fxlink_TUI_input *in)
{
int current_y, current_x;
getyx(in->win, current_y, current_x);
scrollok(in->win, true);
wscrl(in->win, in->wy);
scrollok(in->win, false);
wmove(in->win, current_y - in->wy, current_x);
in->wy = 0;
}
struct CSI {
int param1;
int param2;
char cmd;
char record[32];
};
static struct CSI parse_CSI(WINDOW *win, WINDOW *logWindow)
{
struct CSI csi = { 0 };
int i = 0;
size_t j = 0;
csi.param1 = -1;
csi.param2 = -1;
while(!csi.cmd && j < sizeof csi.record) {
chtype ch = wgetch(win);
if((int)ch == ERR) {
wprintw(logWindow, "error: invalid CSI escape: ");
waddnstr(logWindow, csi.record, j);
waddch(logWindow, '\n');
return (struct CSI){ 0 };
}
int c = ch & A_CHARTEXT;
if(isdigit(c)) {
if(i == 0) {
csi.param1 = csi.param1 * 10 + (c - '0');
}
else {
csi.param2 = csi.param2 * 10 + (c - '0');
}
}
else if(c == ';' && i == 0)
i = 1;
else if(strchr("ABCD", c))
csi.cmd = c;
else {
wprintw(logWindow, "unknow CSI escape\n");
return (struct CSI){ 0 };
}
}
return csi;
}
bool fxlink_TUI_input_getch(struct fxlink_TUI_input *in, WINDOW *logWindow)
{
chtype ch = wgetch(in->win);
int c = ch & A_CHARTEXT;
struct CSI csi = { 0 };
/* Parse CSI escapes */
if(c == '\e') {
chtype next = wgetch(in->win);
if((int)next == ERR)
return false;
int n = next & A_CHARTEXT;
/* CSI */
if(n == '[')
csi = parse_CSI(in->win, logWindow);
/* Scrolling (ignored) */
else if(n == 'O' || n == 'P')
wgetch(in->win);
else
wprintw(logWindow, "error: invalid escape start ^[%c\n", n);
c = 0;
}
/* Event left after a SIGWINCH */
if(ch == KEY_RESIZE)
return false;
/* <Backspace>: Remove last letter */
if(c == 0x7f) {
if(in->cursor > 0) {
fxlink_TUI_input_delete(in, in->cursor - 1, 1);
in->cursor--;
fxlink_TUI_input_redraw(in);
}
}
/* C-d: Delete to the right */
else if(c == 'D'-64) {
if(in->cursor < in->size) {
fxlink_TUI_input_delete(in, in->cursor, 1);
fxlink_TUI_input_redraw(in);
}
}
/* C-b: Back one character */
else if(c == 'B'-64) {
if(in->cursor > 0) {
in->cursor--;
fxlink_TUI_input_redraw(in);
}
}
/* C-f: Forward one character */
else if(c == 'F'-64) {
if(in->cursor < in->size) {
in->cursor++;
fxlink_TUI_input_redraw(in);
}
}
/* C-a: Move cursor to start of line */
else if(c == 'A'-64) {
if(in->cursor != 0) {
in->cursor = 0;
fxlink_TUI_input_redraw(in);
}
}
/* C-e: Move cursor to end of line */
else if(c == 'E'-64) {
if(in->cursor != in->size) {
in->cursor = in->size;
fxlink_TUI_input_redraw(in);
}
}
/* C-l: Clear the screen (moves prompt to first line) */
else if(c == 'L'-64) {
fxlink_TUI_input_clearscreen(in);
}
/* C-k: Kill to end of line */
else if(c == 'K'-64) {
fxlink_TUI_input_delete(in, in->cursor, in->size - in->cursor);
fxlink_TUI_input_redraw(in);
}
/* <LEFT>: Move cursor to the left */
else if(csi.cmd == 'D') {
int distance = (csi.param1 >= 0) ? csi.param1 : 1;
if(in->cursor > 0) {
in->cursor = max(0, in->cursor - distance);
fxlink_TUI_input_redraw(in);
}
}
/* <RIGHT>: Move cursor to the right */
else if(csi.cmd == 'C') {
int distance = (csi.param1 >= 0) ? csi.param1 : 1;
if(in->cursor < in->size) {
in->cursor = min(in->size, in->cursor + distance);
fxlink_TUI_input_redraw(in);
}
}
// <DEL>: Delete to the right
// Meta-F: Move forward a word
// Meta-B: Move backward a word
// Meta-<DEL> or Meta-<D>: Kill to end of current word or eat next word
// Ctrl-W: Kill to previous whitespace
else if(c == '\n') {
in->cursor = in->size;
fxlink_TUI_input_redraw(in);
scrollok(in->win, true);
waddch(in->win, '\n');
return true;
}
else if(c != 0) {
if(fxlink_TUI_input_echo(in, ch)) {
char c_char = c;
fxlink_TUI_input_insert(in, in->cursor, &c_char, 1);
in->cursor++;
fxlink_TUI_input_redraw(in);
}
}
else if(csi.cmd) {
wprintw(logWindow, "unhandled escape: %c (%d,%d)\n",
csi.cmd, csi.param1, csi.param2);
}
/* Debug input as it is being typed
if(c != 0) {
wprintw(logWindow, "ch:%04x [%.*s|%.*s]\n",
ch, in->cursor, in->data,
in->size - in->cursor, in->data + in->cursor);
} */
return false;
}

452
fxlink/tui/layout.c Normal file
View File

@ -0,0 +1,452 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/tui/layout.h>
#include <stdlib.h>
#include <math.h>
typedef unsigned int uint;
//---
// Flexbox-like allocation algorithm (copy/pasted from JustUI)
//
// The basic idea of space redistribution is to give each widget extra space
// proportional to their stretch rates in the relevant direction. However, the
// addition of maximum size constraints means that widgets can decline some of
// the extra space being allocated.
//
// This system defines the result of expansion as a function of the "expansion
// factor". As the expansion factor increases, every widget stretches at a
// speed proportional to its stretch rate, until it reaches its maximum size.
//
// Extra widget size
// |
// + .-------- Maximum size
// | .`
// | .` <- Slope: widget stretch rate
// |.`
// 0 +-------+------> Expansion factor
// 0 ^
// Breaking point
//
// The extra space allocated to widgets is the sum of this function for every
// widget considered for expansion. Since every widget has a possibly different
// breaking point, a maximal interval of expansion factor that has no breaking
// point is called a "run". During each run, the slope for the total space
// remains constant, and a unit of expansion factor corresponds to one pixel
// being allocated in the container. Thus, whenever the expansion factor
// increases of (slope), every widget (w) gets (w->stretch) new pixels.
//
// The functions below simulate the expansion by determining the breaking
// points of the widgets and allocating extra space during each run. Once the
// total extra space allocated reaches the available space, simulation stops
// and the allocation is recorded by assigning actual size to widgets.
//---
/* This "expansion" structure tracks information relating to a single child
widget during the space distribution process. */
typedef struct {
/* Child index */
uint8_t id;
/* Stretch rate, sum of stretch rates is the "slope" */
uint8_t stretch;
/* Maximum size augmentation */
int16_t max;
/* Extra space allocate in the previous runs, in pixels */
float allocated;
/* Breaking point for the current run, as a number of pixels to distribute
to the whole system */
float breaking_point;
} exp_t;
/* Determine whether a widget can expand any further. */
static bool can_expand(exp_t *e)
{
return (e->stretch > 0 && e->allocated < e->max);
}
/* Compute the slope for the current run. */
static uint compute_slope(exp_t elements[], size_t n)
{
uint slope = 0;
for(size_t i = 0; i < n; i++) {
if(can_expand(&elements[i])) slope += elements[i].stretch;
}
return slope;
}
/* Compute the breaking point for every expanding widget. Returns the amount of
pixels to allocate in order to reach the next breaking point. */
static float compute_breaking_points(exp_t elements[], size_t n, uint slope)
{
float closest = HUGE_VALF;
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
if(!can_expand(e)) continue;
/* Up to (e->max - e->allocated) pixels can be added to this widget.
With the factor of (slope / e->stretch), we get the number of pixels
to add to the container in order to reach the threshold. */
e->breaking_point = (e->max - e->allocated) * (slope / e->stretch);
closest = fminf(e->breaking_point, closest);
}
return closest;
}
/* Allocate floating-point space to widgets. This is the core of the
distribution system, it produces (e->allocated) for every element. */
static void allocate_space(exp_t elements[], size_t n, float available)
{
/* One iteration per run */
while(available > 0) {
/* Slope for this run; if zero, no more widget can grow */
uint slope = compute_slope(elements, n);
if(!slope) break;
/* Closest breaking point, amount of space to distribute this run */
float breaking = compute_breaking_points(elements, n, slope);
float run_budget = fminf(breaking, available);
/* Give everyone their share of run_budget */
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
if(!can_expand(e)) continue;
e->allocated += (run_budget * e->stretch) / slope;
}
available -= run_budget;
}
}
/* Stable insertion sort: order children by decreasing fractional allocation */
static void sort_by_fractional_allocation(exp_t elements[], size_t n)
{
for(size_t spot = 0; spot < n - 1; spot++) {
/* Find the element with the max fractional value in [spot..size] */
float max_frac = 0;
int max_frac_who = -1;
for(size_t i = spot; i < n; i++) {
exp_t *e = &elements[i];
float frac = e->allocated - floorf(e->allocated);
if(max_frac_who < 0 || frac > max_frac) {
max_frac = frac;
max_frac_who = i;
}
}
/* Give that element the spot */
exp_t temp = elements[spot];
elements[spot] = elements[max_frac_who];
elements[max_frac_who] = temp;
}
}
static int compare_ids(void const *ptr1, void const *ptr2)
{
exp_t const *e1 = ptr1;
exp_t const *e2 = ptr2;
return e1->id - e2->id;
}
/* Round allocations so that they add up to the available space */
static void round_allocations(exp_t elements[], size_t n, int available_space)
{
/* Prepare to give everyone the floor of their allocation */
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
available_space -= floorf(e->allocated);
}
/* Sort by decreasing fractional allocation then add one extra pixel to
the (available_space) children with highest fractional allocation */
sort_by_fractional_allocation(elements, n);
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
e->allocated = floorf(e->allocated);
if(can_expand(e) && (int)i < available_space) e->allocated += 1;
}
/* Sort back by IDs for final ordering */
qsort(elements, n, sizeof *elements, compare_ids);
}
//---
// TUI layout
//---
static struct fxlink_TUI_box *mkbox(void)
{
struct fxlink_TUI_box *b = calloc(1, sizeof *b);
if(b) {
b->max_w = 0xffff;
b->max_h = 0xffff;
b->stretch_x = 1;
b->stretch_y = 1;
}
return b;
}
struct fxlink_TUI_box *fxlink_TUI_box_mk_window(char const *title, WINDOW **w)
{
struct fxlink_TUI_box *b = mkbox();
if(b) {
b->type = FXLINK_TUI_BOX_WINDOW;
b->window.title = title;
b->window.win = w;
}
return b;
}
static struct fxlink_TUI_box *mkcontainer(int type,
struct fxlink_TUI_box *child, va_list args)
{
struct fxlink_TUI_box *b = mkbox();
if(b) {
b->type = type;
int i = 0;
while(child && i < FXLINK_TUI_BOX_MAXSIZE) {
b->children[i++] = child;
child = va_arg(args, struct fxlink_TUI_box *);
}
}
return b;
}
struct fxlink_TUI_box *fxlink_TUI_box_mk_vertical(
struct fxlink_TUI_box *child1, ...)
{
va_list args;
va_start(args, child1);
return mkcontainer(FXLINK_TUI_BOX_VERTICAL, child1, args);
va_end(args);
}
struct fxlink_TUI_box *fxlink_TUI_box_mk_horizontal(
struct fxlink_TUI_box *child1, ...)
{
va_list args;
va_start(args, child1);
return mkcontainer(FXLINK_TUI_BOX_HORIZONTAL, child1, args);
va_end(args);
}
void fxlink_TUI_box_minsize(struct fxlink_TUI_box *box, int min_w, int min_h)
{
box->min_w = min_w;
box->min_h = min_h;
}
void fxlink_TUI_box_maxsize(struct fxlink_TUI_box *box, int max_w, int max_h)
{
box->max_w = max_w;
box->max_h = max_h;
}
void fxlink_TUI_box_stretch(struct fxlink_TUI_box *box, int stretch_x,
int stretch_y, bool force)
{
box->stretch_x = stretch_x;
box->stretch_y = stretch_y;
box->stretch_force = force;
}
void fxlink_TUI_box_print(FILE *fp, struct fxlink_TUI_box const *b, int level)
{
for(int i = 0; i < level * 4; i++)
fputc(' ', fp);
fprintf(fp, "type=");
if(b->type == FXLINK_TUI_BOX_WINDOW)
fprintf(fp, "WINDOW '%s'", b->window.title);
if(b->type == FXLINK_TUI_BOX_VERTICAL)
fprintf(fp, "VERTICAL");
if(b->type == FXLINK_TUI_BOX_HORIZONTAL)
fprintf(fp, "HORIZONTAL");
fprintf(fp, " x=%d y=%d w=", b->x, b->y);
if(b->min_w > 0)
fprintf(fp, "(%d)<", b->min_w);
fprintf(fp, "%d", b->w);
if(b->max_w < 0xffff)
fprintf(fp, "<(%d)", b->max_w);
fprintf(fp, " h=");
if(b->min_h > 0)
fprintf(fp, "(%d)<", b->min_h);
fprintf(fp, "%d", b->h);
if(b->max_h < 0xffff)
fprintf(fp, "<(%d)", b->max_h);
fprintf(fp, "\n");
if(b->type != FXLINK_TUI_BOX_WINDOW) {
for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && b->children[i]; i++)
fxlink_TUI_box_print(fp, b->children[i], level+1);
}
}
static void box_do_layout(struct fxlink_TUI_box *box)
{
if(box->type == FXLINK_TUI_BOX_WINDOW)
return;
int horiz = (box->type == FXLINK_TUI_BOX_HORIZONTAL);
size_t child_count = 0;
while(child_count < FXLINK_TUI_BOX_MAXSIZE && box->children[child_count])
child_count++;
int spacing = 1;
/* Content width and height */
int cw = box->w;
int ch = box->h;
/* Allocatable width and height (which excludes spacing) */
int total_spacing = (child_count - 1) * spacing;
int aw = cw - (horiz ? total_spacing : 0);
int ah = ch - (horiz ? 0 : total_spacing);
/* Length along the main axis, including spacing */
int length = 0;
/* Expanding widgets' information for extra space distribution */
size_t n = child_count;
exp_t elements[n];
for(size_t i = 0; i < child_count; i++) {
struct fxlink_TUI_box *child = box->children[i];
/* Maximum size to enforce: this is the acceptable size closest to our
allocatable size */
int max_w = clamp(aw, child->min_w, child->max_w);
int max_h = clamp(ah, child->min_h, child->max_h);
/* Start by setting every child to an acceptable size */
child->w = clamp(child->w, child->min_w, max_w);
child->h = clamp(child->h, child->min_h, max_h);
/* Initialize expanding widgets' information */
elements[i].id = i;
elements[i].allocated = 0.0f;
elements[i].breaking_point = -1.0f;
/* Determine natural length along the container, and stretch child
along the perpendicular direction if possible */
if(i > 0)
length += spacing;
if(horiz) {
length += child->w;
if(child->stretch_y > 0) child->h = max_h;
elements[i].stretch = child->stretch_x;
elements[i].max = max(max_w - child->w, 0);
if(child->stretch_force && child->stretch_x > 0)
elements[i].max = max(aw - child->w, 0);
}
else {
length += child->h;
if(child->stretch_x > 0) child->w = max_w;
elements[i].stretch = child->stretch_y;
elements[i].max = max(max_h - child->h, 0);
if(child->stretch_force && child->stretch_y > 0)
elements[i].max = max(ah - child->h, 0);
}
}
/* Distribute extra space along the line */
int extra_space = (horiz ? cw : ch) - length;
allocate_space(elements, n, extra_space);
round_allocations(elements, n, extra_space);
/* Update widgets for extra space */
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
struct fxlink_TUI_box *child = box->children[e->id];
if(horiz)
child->w += e->allocated;
else
child->h += e->allocated;
}
/* Position everyone */
int position = 0;
for(size_t i = 0; i < n; i++) {
exp_t *e = &elements[i];
struct fxlink_TUI_box *child = box->children[e->id];
if(horiz) {
child->x = box->x + position;
child->y = box->y + (ch - child->h) / 2;
position += child->w + spacing;
}
else {
child->x = box->x + (cw - child->w) / 2;
child->y = box->y + position;
position += child->h + spacing;
}
}
for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && box->children[i]; i++)
box_do_layout(box->children[i]);
}
void fxlink_TUI_box_layout(struct fxlink_TUI_box *b,
int x, int y, int w, int h)
{
b->x = x + 1;
b->y = y + 1;
b->w = w - 2;
b->h = h - 2;
return box_do_layout(b);
}
static bool box_apply(struct fxlink_TUI_box *box,
int rx, int ry, int rw, int rh)
{
if(box->type == FXLINK_TUI_BOX_WINDOW) {
if(!box->window.win)
return true;
/* Ensure window is of non-zero size and in-bounds */
int x = clamp(box->x, rx, rx + rw - 1);
int y = clamp(box->y, ry, ry + rh - 1);
int w = clamp(box->w, 1, rw - (x - rx));
int h = clamp(box->h, 1, rh - (y - ry));
if(!*box->window.win) {
*box->window.win = newwin(h, w, y, x);
}
else {
wresize(*box->window.win, h, w);
mvwin(*box->window.win, y, x);
}
return (*box->window.win != NULL);
}
bool success = true;
for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && box->children[i]; i++)
success = success && box_apply(box->children[i], rx, ry, rw, rh);
return success;
}
bool fxlink_TUI_apply_layout(struct fxlink_TUI_box *root)
{
return box_apply(root, root->x, root->y, root->w, root->h);
}

108
fxlink/tui/render.c Normal file
View File

@ -0,0 +1,108 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/tui/render.h>
#include <time.h>
void aprint(WINDOW *win, int attr, char const *format, ...)
{
va_list args;
va_start(args, format);
wattron(win, attr);
vw_printw(win, format, args);
wattroff(win, attr);
va_end(args);
}
void fprint(WINDOW *win, int display_fmt, char const *format, ...)
{
int attr = fmt_to_ncurses_attr(display_fmt);
va_list args;
va_start(args, format);
wattron(win, attr);
vw_printw(win, format, args);
wattroff(win, attr);
va_end(args);
}
int fmt_to_ncurses_attr(int format)
{
/* Get the color pair for FG/BG following our custom encoding (which is
compatible with the default for colors without a background) */
int FG = fmt_FG(format);
int BG = fmt_BG(format);
int attr = COLOR_PAIR(9*BG + FG);
if(fmt_BOLD(format))
attr |= A_BOLD;
if(fmt_DIM(format))
attr |= A_DIM;
if(fmt_ITALIC(format))
attr |= A_ITALIC;
return attr;
}
enum { TOP=1, RIGHT=2, BOTTOM=4, LEFT=8 };
static void TUI_add_borders(int x, int y, int directions)
{
chtype borders[16] = {
' ', '\0', '\0', ACS_LLCORNER,
'\0', ACS_VLINE, ACS_ULCORNER, ACS_LTEE,
'\0', ACS_LRCORNER, ACS_HLINE, ACS_BTEE,
ACS_URCORNER, ACS_RTEE, ACS_TTEE, ACS_PLUS,
};
chtype ch = mvinch(y, x);
int dirs = 0;
for(int i = 0; i < 16; i++) {
if(borders[i] == ch)
dirs = i;
}
mvaddch(y, x, borders[dirs | directions]);
}
void fxlink_TUI_render_borders(struct fxlink_TUI_box const *b)
{
int x = b->x, y = b->y;
if(b->type == FXLINK_TUI_BOX_WINDOW) {
for(int dx = 0; dx < b->w; dx++) {
TUI_add_borders(x+dx, y-1, LEFT | RIGHT);
TUI_add_borders(x+dx, y+b->h, LEFT | RIGHT);
}
for(int dy = 0; dy < b->h; dy++) {
TUI_add_borders(x-1, y+dy, TOP | BOTTOM);
TUI_add_borders(x+b->w, y+dy, TOP | BOTTOM);
}
TUI_add_borders(x-1, y-1, RIGHT | BOTTOM);
TUI_add_borders(x+b->w, y-1, LEFT | BOTTOM);
TUI_add_borders(x-1, y+b->h, RIGHT | TOP);
TUI_add_borders(x+b->w, y+b->h, LEFT | TOP);
}
else {
for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && b->children[i]; i++)
fxlink_TUI_render_borders(b->children[i]);
}
}
void fxlink_TUI_render_titles(struct fxlink_TUI_box const *box)
{
if(box->type == FXLINK_TUI_BOX_WINDOW) {
attron(A_BOLD);
mvaddch(box->y-1, box->x, ' ');
addstr(box->window.title);
addch(' ');
attroff(A_BOLD);
return;
}
for(int i = 0; i < FXLINK_TUI_BOX_MAXSIZE && box->children[i]; i++)
fxlink_TUI_render_titles(box->children[i]);
}

View File

@ -0,0 +1,592 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include "../fxlink.h"
#include "tui.h"
#include <fxlink/tooling/libpng.h>
#include <fxlink/tooling/sdl2.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
/*
Plan for the TUI command set.
fxlink commands:
select Select the calculator we're talking to
remote-control Enable/disable/setup remote calculator control
Protocol commands (executed by the calculator):
/identify Send calculator identification information
/echo Echo a message
/screenshot Take a screenshot
/video Enable/disable video capture
Application-specific commands:
gintctl read-long Transmission tests (long messages)
gintctl read-alignment Pipe reading alignment
gintctl bench USB transfer speed benchmark
*/
struct TUIData TUI = { 0 };
//---
// TUI management and rendering
//---
static bool TUI_setup_windows(void)
{
struct fxlink_TUI_box
*bTransfers, *bConsole, *bStatus, *bLogs, *bTextOutput,
*bLeft, *bRight;
bTransfers = fxlink_TUI_box_mk_window("Transfers", &TUI.wTransfers);
bConsole = fxlink_TUI_box_mk_window("Console", &TUI.wConsole);
bStatus = fxlink_TUI_box_mk_window("Status", &TUI.wStatus);
bLogs = fxlink_TUI_box_mk_window("Logs", &TUI.wLogs);
bTextOutput = fxlink_TUI_box_mk_window("Text output from calculators",
&TUI.wTextOutput);
fxlink_TUI_box_stretch(bLogs, 1, 2, false);
bLeft = fxlink_TUI_box_mk_vertical(bTextOutput, bConsole, NULL);
bRight = fxlink_TUI_box_mk_vertical(bStatus, bLogs, bTransfers, NULL);
fxlink_TUI_box_stretch(bLeft, 2, 1, false);
fxlink_TUI_box_stretch(bRight, 3, 1, false);
TUI.bRoot = fxlink_TUI_box_mk_horizontal(bLeft, bRight, NULL);
fxlink_TUI_box_layout(TUI.bRoot, 0, 1, getmaxx(stdscr), getmaxy(stdscr)-1);
return fxlink_TUI_apply_layout(TUI.bRoot);
}
static void TUI_free_windows(void)
{
if(TUI.wStatus) delwin(TUI.wStatus);
if(TUI.wLogs) delwin(TUI.wLogs);
if(TUI.wTransfers) delwin(TUI.wTransfers);
if(TUI.wTextOutput) delwin(TUI.wTextOutput);
if(TUI.wConsole) delwin(TUI.wConsole);
}
static void TUI_refresh_all(bool refresh_bg)
{
if(refresh_bg)
wrefresh(stdscr);
wrefresh(TUI.wStatus);
wrefresh(TUI.wLogs);
wrefresh(TUI.wTransfers);
wrefresh(TUI.wTextOutput);
wrefresh(TUI.wConsole);
}
static void TUI_refresh_console(void)
{
wrefresh(TUI.wLogs);
wrefresh(TUI.wConsole);
}
static void TUI_render_status(void)
{
WINDOW *win = TUI.wStatus;
werase(win);
int w, h, y = 1;
getmaxyx(win, h, w);
wmove(win, 0, 1);
fprint(win, FMT_HEADER,
"Device Status ID System Classes");
if(TUI.devices.count == 0) {
mvwaddstr(win, 1, 1, "(no devices)");
y++;
}
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
struct fxlink_calc *calc = fdev->calc;
if(fdev->status == FXLINK_FDEV_STATUS_CONNECTED) {
wattron(win, fmt_to_ncurses_attr(FMT_BGSELECTED));
mvwhline(win, y, 0, ' ', w);
}
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, fxlink_device_status_string(fdev));
mvwprintw(win, y, 21, "%04x:%04x", fdev->idVendor, fdev->idProduct);
if(calc) {
mvwaddstr(win, y, 32, fxlink_device_system_string(fdev));
wmove(win, y, 41);
for(int i = 0; i < calc->interface_count; i++)
wprintw(win, " %02x.%02x", calc->classes[i] >> 8,
calc->classes[i] & 0xff);
}
wattroff(win, fmt_to_ncurses_attr(FMT_BGSELECTED));
y++;
}
bool has_comms = false;
for(int i = 0; i < TUI.devices.count; i++)
has_comms = has_comms || (TUI.devices.devices[i].comm != NULL);
wmove(win, y+1, 1);
fprint(win, FMT_HEADER, "Device Status Serial IN OUT");
y += 2;
if(!has_comms) {
mvwaddstr(win, y, 1, "(no communications)");
}
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
struct fxlink_calc *calc = fdev->calc;
struct fxlink_comm *comm = fdev->comm;
if(!comm)
continue;
if(y >= h) {
y++;
break;
}
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, fxlink_device_status_string(fdev));
mvwaddstr(win, y, 21, calc->serial ? calc->serial : "(null)");
mvwprintw(win, y, 31, "%02x%c", comm->ep_bulk_IN,
comm->tr_bulk_IN != NULL ? '*' : '.');
mvwprintw(win, y, 36, "%02x%c", comm->ep_bulk_OUT,
comm->tr_bulk_OUT != NULL ? '*' : '.');
y++;
}
if(y > h) {
wmove(win, h-1, w-6);
fprint(win, FMT_BGSELECTED, "(more)");
}
}
static void progress_bar(WINDOW *win, int width, int done, int total)
{
char const *ramp[9] = { " ", "", "", "", "", "", "", "", "" };
int progress = ((int64_t)done * width * 8) / total;
int percent = (int64_t)done * 100 / total;
wprintw(win, "%3d%% │", percent);
for(int i = 0; i < width; i++) {
int block_width = min(progress, 8);
waddstr(win, ramp[block_width]);
progress = max(progress-8, 0);
}
waddstr(win, "");
}
static void TUI_render_transfers(void)
{
WINDOW *win = TUI.wTransfers;
int y = 1;
werase(win);
wmove(win, 0, 1);
fprint(win, FMT_HEADER, "Device Dir. Size Progress");
bool has_transfers = false;
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
struct fxlink_comm *comm = fdev->comm;
if(!comm)
continue;
struct fxlink_transfer *IN = comm->ftransfer_IN;
struct fxlink_transfer *OUT = comm->ftransfer_OUT;
if(IN) {
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, "IN");
mvwaddstr(win, y, 16, fxlink_size_string(IN->msg.size));
wmove(win, y, 26);
progress_bar(win, 32, IN->processed_size, IN->msg.size);
has_transfers = true;
y++;
}
if(OUT) {
mvwaddstr(win, y, 1, fxlink_device_id(fdev));
mvwaddstr(win, y, 10, "OUT");
mvwaddstr(win, y, 16, fxlink_size_string(OUT->msg.size));
has_transfers = true;
y++;
}
}
if(!has_transfers)
mvwaddstr(win, 1, 1, "(no transfers)");
}
static void TUI_render_all(bool with_borders)
{
if(with_borders) {
erase();
fxlink_TUI_render_borders(TUI.bRoot);
fxlink_TUI_render_titles(TUI.bRoot);
/* Title bar */
int w = getmaxx(stdscr);
attron(fmt_to_ncurses_attr(FMT_BGSELECTED));
mvhline(0, 0, ' ', w);
char const *str = "fxlink " FXLINK_VERSION " (TUI interactive mode)";
mvaddstr(0, w/2 - strlen(str)/2, str);
attroff(fmt_to_ncurses_attr(FMT_BGSELECTED));
}
TUI_render_status();
TUI_render_transfers();
}
static void TUI_SIGWINCH_handler(int sig)
{
(void)sig;
TUI.resize_needed = true;
}
static bool TUI_setup(void)
{
memset(&TUI, 0, sizeof TUI);
/* Set up the SINGWINCH handler */
struct sigaction WINCH;
sigaction(SIGWINCH, NULL, &WINCH);
WINCH.sa_handler = TUI_SIGWINCH_handler;
sigaction(SIGWINCH, &WINCH, NULL);
/* Initialize the main screen */
initscr();
start_color();
use_default_colors();
/* Set up our color pairs. These are FG/BG combinations from the FMT_*
enumerated colors in <fxlink/defs.h>, ordered BG-major. */
for(int bg = 0; bg < 9; bg++) {
for(int fg = 0; fg < 9; fg++)
init_pair(9*bg + fg, fg - 1, bg - 1);
}
if(!TUI_setup_windows())
return false;
/* Allow ncurses to scroll text-based windows*/
scrollok(TUI.wConsole, 1);
scrollok(TUI.wLogs, 1);
scrollok(TUI.wTextOutput, 1);
wmove(TUI.wConsole, 0, 0);
wmove(TUI.wLogs, 0, 0);
wmove(TUI.wTextOutput, 0, 0);
/* Make getch() non-blocking (though it also doesn't interpret escape
sequences anymore!) */
cbreak();
wtimeout(TUI.wLogs, 0);
wtimeout(TUI.wTextOutput, 0);
wtimeout(TUI.wConsole, 0);
/* Disable echo so we can edit input as it's being typed */
noecho();
return true;
}
static void TUI_quit(void)
{
TUI_free_windows();
endwin();
}
//---
// Interactive TUI
//---
static void handle_image(struct fxlink_message *msg, char const *path)
{
struct fxlink_message_image_header *img = msg->data;
char *filename = fxlink_gen_file_name(path, msg->type, ".png");
struct fxlink_message_image_raw *raw = fxlink_message_image_decode(msg);
if(raw) {
fxlink_libpng_save_raw(raw, filename);
fxlink_message_image_raw_free(raw);
log_("saved image (%dx%d, format=%d) to '%s'\n",
img->width, img->height, img->pixel_format, filename);
}
free(filename);
}
static void handle_text(struct fxlink_message *msg)
{
char const *str = msg->data;
WINDOW *win = TUI.wTextOutput;
if(options.verbose)
waddstr(win, "------------------\n");
waddnstr(win, str, msg->size);
if(options.verbose) {
if(str[msg->size - 1] != '\n')
waddch(win, '\n');
waddstr(win, "------------------\n");
}
if(options.verbose) {
for(size_t i = 0; i < msg->size; i++) {
print(win, " %02x", str[i]);
if((i & 15) == 15 || i == msg->size - 1)
print(win, "\n");
}
}
if(options.log_file)
fwrite(str, 1, msg->size, options.log_file);
}
static void handle_video(struct fxlink_message *msg)
{
struct fxlink_message_image_raw *raw = fxlink_message_image_decode(msg);
if(!raw)
return;
fxlink_sdl2_display_raw(raw);
fxlink_message_image_raw_free(raw);
}
static void fxlink_interactive_handle_message(struct fxlink_message *msg)
{
char const *path = ".";
if(fxlink_message_is_fxlink_image(msg))
return handle_image(msg, path);
if(fxlink_message_is_fxlink_text(msg))
return handle_text(msg);
if(fxlink_message_is_fxlink_video(msg))
return handle_video(msg);
/* Default to saving to a blob */
static char combined_type[48];
sprintf(combined_type, "%.16s-%.16s", msg->application, msg->type);
char *filename = fxlink_gen_file_name(path, combined_type, ".bin");
FILE *fp = fopen(filename, "wb");
if(!fp) {
elog("could not save to '%s': %m\n", filename);
return;
}
fwrite(msg->data, 1, msg->size, fp);
fclose(fp);
log_("saved blob to '%s'\n", filename);
free(filename);
}
static void handle_fxlink_log(int display_fmt, char const *str)
{
int attr = fmt_to_ncurses_attr(display_fmt);
wattron(TUI.wLogs, attr);
waddstr(TUI.wLogs, str);
wattroff(TUI.wLogs, attr);
}
bool TUI_core_update(bool allow_console, bool auto_refresh, bool *has_command)
{
struct timeval zero_tv = { 0 };
struct timeval usb_timeout;
struct pollfd stdinfd = { .fd = STDIN_FILENO, .events = POLLIN };
int rc = libusb_get_next_timeout(TUI.ctx, &usb_timeout);
int timeout = -1;
if(rc > 0)
timeout = usb_timeout.tv_sec * 1000 + usb_timeout.tv_usec / 1000;
bool timeout_is_libusb = true;
/* Time out at least every 100 ms so we can handle SDL events */
if(timeout < 0 || timeout > 100) {
timeout = 100;
timeout_is_libusb = false;
}
if(has_command)
*has_command = false;
rc = fxlink_multipoll(timeout,
&stdinfd, 1, TUI.polled_fds.fds, TUI.polled_fds.count, NULL);
if(rc < 0 && errno != EINTR) {
elog("poll: %s\n", strerror(errno));
return false;
}
if(rc < 0 && errno == EINTR)
return false;
/* Handle SIGWINCH */
if(TUI.resize_needed) {
endwin();
refresh();
TUI_setup_windows();
TUI.resize_needed = false;
TUI_render_all(true);
TUI_refresh_all(true);
return false;
}
/* Determine which even source was activated */
bool stdin_activity = (stdinfd.revents & POLLIN) != 0;
bool usb_activity = false;
for(int i = 0; i < TUI.polled_fds.count; i++)
usb_activity |= (TUI.polled_fds.fds[i].revents != 0);
/* Determine what to do. We update the console on stdin activity. We
update libusb on USB activity or appropriate timeout. We update SDL
events on any timeout. */
bool update_console = stdin_activity;
bool update_usb = usb_activity || (rc == 0 && timeout_is_libusb);
bool update_sdl = (rc == 0);
if(allow_console && update_console) {
bool finished = fxlink_TUI_input_getch(&TUI.input, TUI.wLogs);
TUI_refresh_console();
if(has_command)
*has_command = finished;
}
if(update_usb) {
libusb_handle_events_timeout(TUI.ctx, &zero_tv);
fxlink_device_list_refresh(&TUI.devices);
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
/* Check for devices ready to connect to */
if(fxlink_device_ready_to_connect(fdev)
&& fxlink_device_has_fxlink_interface(fdev)) {
if(fxlink_device_claim_fxlink(fdev))
fxlink_device_start_bulk_IN(fdev);
}
}
}
if(update_sdl) {
fxlink_sdl2_handle_events();
}
bool refresh = update_console || update_usb;
if(auto_refresh) {
TUI_render_all(false);
TUI_refresh_all(false);
}
return refresh;
}
bool TUI_wait_message(struct fxlink_device *fdev,
char const *application, char const *type, struct fxlink_message **msg_ptr)
{
if(*msg_ptr) {
fxlink_message_free(*msg_ptr, true);
fxlink_device_start_bulk_IN(fdev);
return false;
}
while(1) {
TUI_core_update(false, true, NULL);
/* Check for new messages */
struct fxlink_message *msg = fxlink_device_finish_bulk_IN(fdev);
if(msg) {
if(fxlink_message_is_apptype(msg, application, type)) {
*msg_ptr = msg;
return true;
}
else {
fxlink_interactive_handle_message(msg);
fxlink_message_free(msg, true);
fxlink_device_start_bulk_IN(fdev);
}
}
}
}
int main_tui_interactive(libusb_context *ctx)
{
if(!TUI_setup())
return elog("error: failed to setup ncurses TUI o(x_x)o\n");
TUI.ctx = ctx;
/* Redirect fxlink logs to the logging window in the TUI */
fxlink_log_set_handler(handle_fxlink_log);
/* Set up hotplug notification */
fxlink_device_list_track(&TUI.devices, ctx);
/* Set up file descriptor tracking */
fxlink_pollfds_track(&TUI.polled_fds, ctx);
/* Initial render */
print(TUI.wConsole, "fxlink version %s (libusb/TUI interactive mode)\n",
FXLINK_VERSION);
char const *prompt = "> ";
print(TUI.wConsole, "%s", prompt);
TUI_render_all(true);
TUI_refresh_all(true);
fxlink_TUI_input_init(&TUI.input, TUI.wConsole, 16);
while(1) {
bool has_command;
bool activity = TUI_core_update(true, false, &has_command);
/* Check for devices with finished transfers */
for(int i = 0; i < TUI.devices.count; i++) {
struct fxlink_device *fdev = &TUI.devices.devices[i];
struct fxlink_message *msg = fxlink_device_finish_bulk_IN(fdev);
if(msg) {
fxlink_interactive_handle_message(msg);
fxlink_message_free(msg, true);
fxlink_device_start_bulk_IN(fdev);
}
}
/* Check for console commands */
if(has_command) {
char *command = TUI.input.data;
if(command[0] != 0)
log_("command: '%s'\n", command);
if(!strcmp(command, ""))
{}
else if(!strcmp(command, "q") || !strcmp(command, "quit"))
break;
else
TUI_execute_command(command);
fxlink_TUI_input_free(&TUI.input);
print(TUI.wConsole, "%s", prompt);
fxlink_TUI_input_init(&TUI.input, TUI.wConsole, 16);
}
if(activity) {
TUI_render_all(false);
TUI_refresh_all(false);
}
}
while(fxlink_device_list_interrupt(&TUI.devices))
libusb_handle_events(ctx);
fxlink_device_list_stop(&TUI.devices);
fxlink_pollfds_stop(&TUI.polled_fds);
fxlink_log_set_handler(NULL);
TUI_quit();
return 0;
}

68
fxlink/tui/tui.h Normal file
View File

@ -0,0 +1,68 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.tui.tui: Global data and main functions for the interactive TUI
#pragma once
#include <fxlink/tui/layout.h>
#include <fxlink/tui/render.h>
#include <fxlink/tui/input.h>
#include <fxlink/devices.h>
#include <fxlink/logging.h>
#include <libusb.h>
#include <ncurses.h>
struct TUIData {
/* libusb context */
libusb_context *ctx;
/* SIGWINCH flag */
bool resize_needed;
/* ncurses window panels */
WINDOW *wStatus;
WINDOW *wLogs;
WINDOW *wTransfers;
WINDOW *wTextOutput;
WINDOW *wConsole;
/* Root box */
struct fxlink_TUI_box *bRoot;
/* Application data */
struct fxlink_pollfds polled_fds;
struct fxlink_device_list devices;
/* Main console input */
struct fxlink_TUI_input input;
};
extern struct TUIData TUI;
/* Run a single asynchronous update. This polls a bunch of file descriptors
along with a short timeout (< 1s). Returns true if there is any activity.
If `allow_console` is true, console events are handled; otherwise they are
ignored so they can be collected by the main loop. Setting this parameter to
false is useful when waiting for messages in TUI commands. `has_command` is
set to whether there is a new command to be run at the console (only ever
true when `allow_console` is true).
If `auto_refresh` is true, this function will refresh the TUI upon relevant
activity. */
bool TUI_core_update(bool allow_console, bool auto_refresh, bool *has_command);
/* Run the specified TUI command. */
void TUI_execute_command(char const *command);
/* Wait for a message of a particular type to arrive, and then clean it up.
This function should be called in a loop, eg.
struct fxlink_message *msg = NULL;
while(TUI_wait_message(fdev, "fxlink", "text", &msg)) {
// Handle msg...
}
TUI_wait_message() will only return true once, however it will use the next
call to free the message, restart communication on the device, and reset
`msg` to NULL. */
bool TUI_wait_message(struct fxlink_device *fdev,
char const *application, char const *type, struct fxlink_message **msg);

View File

@ -1,159 +0,0 @@
#include "usb.h"
#include "fxlink.h"
#include "util.h"
#include <string.h>
#include <stdlib.h>
#include <errno.h>
char const *usb_id(libusb_device *dev)
{
static char id[32];
sprintf(id, "%d:%d",
libusb_get_bus_number(dev),
libusb_get_device_address(dev));
return id;
}
char *usb_serial_number(libusb_device_handle *dh)
{
struct libusb_device_descriptor dc;
libusb_device *dev = libusb_get_device(dh);
if(libusb_get_device_descriptor(dev, &dc))
return NULL;
if(!dc.iSerialNumber)
return NULL;
char serial[256];
int length = libusb_get_string_descriptor_ascii(dh, dc.iSerialNumber,
(unsigned char *)serial, 256);
if(length < 0)
return NULL;
/* LINK sends a 12-byte serial number with four leading 0. Remove them */
int start = (length == 12 && !strncmp(serial, "0000", 4)) ? 4 : 0;
return strndup(serial + start, length - start);
}
properties_t usb_properties(struct libusb_device_descriptor *dc,
libusb_device_handle *dh)
{
properties_t props = { 0 };
/* Type of calculator based on USB behavior, detected by idProduct */
if(dc->idProduct == 0x6101)
props.p7 = true;
if(dc->idProduct == 0x6102)
props.mass_storage = true;
if(dh)
props.serial_number = usb_serial_number(dh);
return props;
}
int usb_unique_matching(filter_t const *filter, libusb_context *context,
libusb_device **dev)
{
libusb_device *unique = NULL;
int status = FILTER_NONE;
bool error;
for_libusb_devices(it, context, &error) {
if(!filter_match(&it.props, filter)) continue;
/* Already found a device before */
if(unique) {
status = FILTER_MULTIPLE;
libusb_unref_device(unique);
unique = NULL;
break;
}
/* First device: record it */
unique = libusb_ref_device(it.dev);
status = FILTER_UNIQUE;
}
if(error)
return FILTER_ERROR;
/* Don't keep the reference to the device if we're not returning it */
if(unique && !dev)
libusb_unref_device(unique);
if(unique && dev)
*dev = unique;
return status;
}
int usb_unique_wait(filter_t const *filter, delay_t *delay,
libusb_context *context, libusb_device **dev)
{
while(true) {
int rc = usb_unique_matching(filter, context, dev);
/* If a device is found, multiple devices are found, or an error
occurs, forward the result; wait only if nothing was found */
if(rc != FILTER_NONE) return rc;
if(delay_cycle(delay)) return FILTER_NONE;
}
}
//---
// Iteration on libusb devices
//---
usb_iterator_t usb_iter_start(libusb_context *context, bool *error)
{
usb_iterator_t it = { 0 };
it.device_count = libusb_get_device_list(context, &it.devices);
if(it.device_count < 0) {
libusb_err(it.device_count, "cannot get libusb device list");
it.done = true;
if(error) *error = true;
return it;
}
it.index = -1;
usb_iter_next(&it);
if(error) *error = false;
return it;
}
void usb_iter_next(usb_iterator_t *it)
{
if(it->done == true) return;
int rc;
/* Free the resources from the previous iteration */
if(it->dh)
libusb_close(it->dh);
it->dev = NULL;
it->dh = NULL;
/* Load the next device */
if(++it->index >= it->device_count) {
it->done = true;
}
else {
it->dev = it->devices[it->index];
if((rc = libusb_get_device_descriptor(it->dev, &it->dc))) {
libusb_err(rc, "cannot get descriptor for device %s",
usb_id(it->dev));
return usb_iter_next(it);
}
/* Ignore non-CASIO devices */
if(it->dc.idVendor != 0x07cf)
return usb_iter_next(it);
if((rc = libusb_open(it->dev, &it->dh)))
libusb_wrn(rc, "cannot open device %s", usb_id(it->dev));
it->props = usb_properties(&it->dc, it->dh);
}
if(it->done)
libusb_free_device_list(it->devices, true);
}

View File

@ -1,137 +0,0 @@
//---
// fxlink:usb - libusb functions
//---
#ifndef FXLINK_USB_H
#define FXLINK_USB_H
#include "util.h"
#include "properties.h"
#include "filter.h"
#include <libusb.h>
/* usb_properties(): Determine as many properties of the device as possible
If the device can be opened, an open handle should be supplied as (dh). This
is used to determine the serial number; if the device cannot be opened, the
serial number is omitted from the device properties.
@dc Device descriptor
@dh Open handle if the device can be opened, or NULL
-> Returns detected properties of the device. */
properties_t usb_properties(struct libusb_device_descriptor *dc,
libusb_device_handle *dh);
/* usb_unique_matching(): Device that matches the provided filter, if unique
This function runs through the list of devices provided by libusb and
determines whether there is exactly one device matching the filter. If so,
a pointer to this device is set in (*dev) and FILTER_UNIQUE is returned. The
device is referenced and should be un-referenced after use for the data to
be freed. If (dev) is NULL, the pointer is not recorded and not referenced.
If there are no devices matching the filter, (*dev) is unchanged and this
function returns FILTER_NONE. If several devices match the filter, (*dev) is
unchanged and FILTER_MULTIPLE is returned. If an error occurs and the
function cannot complete, an error is printed and FILTER_ERROR is returned.
@filter Device filter to refine the search
@context Previously-initialized libusb context
@dev Output: unique device matching the filter (may be NULL)
-> Returns one of FILTER_{UNIQUE,NONE,MULTIPLE,ERROR}. */
int usb_unique_matching(filter_t const *filter, libusb_context *context,
libusb_device **dev);
/* usb_unique_wait(): Wait for a device matching the provided filter to connect
This function waits up to the provided delay for a device matching the
specified filter to be connected. It calls usb_unique_matching() several
times per second to check for new devices being attached and initialized.
If several devices are connected when usb_unique_wait() is first called, or
several devices are connected between two calls to usb_unique_matching(),
this function returns FILTER_MULTIPLE. As soon as a unique matching device
is found, the pointer is referenced and set in (*dev) if (dev) is not NULL,
and FILTER_UNIQUE is returned, regardless of whether other matching devices
are attached before the end of the wait period.
If no matching device is attached during the specified period, this function
returns FILTER_NONE. If an error occurs during scanning, it returns
FILTER_ERROR.
@filter Device filter to refine the search
@delay Time resource to use delay from
@context Previously-initialized libusb context
@dev Output: unique device matching the filter (can be NULL)
-> Returns one of FILTER_{UNIQUE,NONE,MULTIPLE,ERROR}. */
int usb_unique_wait(filter_t const *filter, delay_t *delay,
libusb_context *context, libusb_device **dev);
//---
// Iteration on libusb devices
//---
typedef struct {
/* Current device and its device descriptor */
libusb_device *dev;
struct libusb_device_descriptor dc;
/* If the device can be opened, its open handle, otherwise NULL */
libusb_device_handle *dh;
/* Device properties */
properties_t props;
/* Whether the iteration has finished */
bool done;
/* Internal indicators: list of devices and current index */
libusb_device **devices;
int device_count;
int index;
} usb_iterator_t;
/* usb_iter_start(): Start an iteration on libusb devices
If the first step fails, returns an iterator with (done = true) and sets
(*error) to true; otherwise, sets (*error) to false. */
usb_iterator_t usb_iter_start(libusb_context *context, bool *error);
/* usb_iter_next(): Iterate to the next libusb device */
void usb_iter_next(usb_iterator_t *it);
/* Convenience for-loop macro for iteration */
#define for_libusb_devices(NAME, context, error) \
for(usb_iterator_t NAME = usb_iter_start(context, error); \
!NAME.done; usb_iter_next(&NAME)) if(!NAME.done)
//---
// Miscellaneous
//---
/* usb_id(): Printable address-based identifier for error messages
This function is used in error messages to describe the device on which an
error occurred in a useful way. The pointer returned is to a static buffer
that changes at every call to this function, and should only be used briefly
to generate messages. */
char const *usb_id(libusb_device *dev);
/* usb_serial_number(): Serial number advertised by the device
This function returns the serial number (as presented with iSerialNumber in
the device descriptor) of the provided device, which may or may not be
present.
Serial numbers for CASIO calculators normally have 8 letters. The LINK
application presents a 12-character code with "0000" prepended. This
function detects this quirk and only returns the last 8 characters. gint's
driver doesn't send to "0000" prefix.
This function requires the device to be open in order to send the request
for the STRING descriptor, and cannot be used if the process user doesn't
have write access to the device.
@dh Open device handle
-> Returns a freshly-allocated copy of the serial number string, to be
free()'d after use, or NULL if the serial number is unspecified or cannot
be retrieved. */
char *usb_serial_number(libusb_device_handle *dh);
#endif /* FXLINK_USB_H */

View File

@ -1,32 +0,0 @@
#include "util.h"
#include <time.h>
#include <errno.h>
delay_t delay_none(void)
{
return 0;
}
delay_t delay_seconds(int seconds)
{
return seconds * 4;
}
delay_t delay_infinite(void)
{
return -1;
}
bool delay_cycle(delay_t *delay)
{
if(*delay == 0) return true;
struct timespec spec = { .tv_sec=0, .tv_nsec=250000000 };
int rc;
/* Account for interrupts in the nanosleep(2) call */
struct timespec req = spec;
do rc = nanosleep(&req, &req);
while(rc == -1 && errno == EINTR);
if(*delay > 0) (*delay)--;
return false;
}

View File

@ -1,60 +0,0 @@
//---
// fxlink:util - Utility functions and error reporting mechanisms
//---
#ifndef FXLINK_UTIL_H
#define FXLINK_UTIL_H
#include <stdio.h>
#include <stdbool.h>
/* Literal error message printed to stderr, evaluates to 1 for a combined
return/exit() call */
#define err(fmt, ...) ({ \
fprintf(stderr, "error: " fmt "\n", ##__VA_ARGS__); \
1; \
})
/* Fatal error that includes a libusb error message */
#define libusb_err(rc, fmt, ...) ({ \
fprintf(stderr, "error: " fmt ": %s\n", ##__VA_ARGS__, \
libusb_strerror(rc)); \
1; \
})
/* Warning message */
#define wrn(fmt, ...) \
fprintf(stderr, "warning: " fmt "\n", ##__VA_ARGS__)
/* Warning that includes a libusb error message */
#define libusb_wrn(rc, fmt, ...) \
fprintf(stderr, "error: " fmt ": %s\n", ##__VA_ARGS__, \
libusb_strerror(rc))
//---
// Delay
//---
/* delay_t: An expandable allocated time used to wait for devices */
typedef int delay_t;
/* delay_none(): No delay allowed */
delay_t delay_none(void);
/* delay_seconds(): Initial delay from a duration in seconds */
delay_t delay_seconds(int seconds);
/* delay_infinite(): Delay that can run through delay_cycle() indefinitely */
delay_t delay_infinite(void);
/* delay_cycle(): Wait for a short cycle
This function returns (true) if the delay has expired; otherwise, it waits
for a short while (250 ms), decreases the supplied delay pointer, and
returns (false).
Not returning (true) after waiting, even if the delay expires then, allows
the caller to perform the task they were waiting on one last time before
giving up.
@delay Input-output: Delay resource to take time from
-> Return (true) if (*delay) has expired, or (false) after waiting. */
bool delay_cycle(delay_t *delay);
#endif /* FXLINK_UTIL_H */

View File

@ -7,7 +7,7 @@ project(MyAddin)
include(GenerateG1A)
include(GenerateG3A)
include(Fxconv)
find_package(Gint 2.1 REQUIRED)
find_package(Gint 2.9 REQUIRED)
set(SOURCES
src/main.c

View File

@ -26,7 +26,7 @@ BINFLAGS := -R .bss -R .gint_bss
NAME_G1A ?= $(NAME)
NAME_G3A ?= $(NAME)
G1AF := -i "$(ICON_FX)" -n "$(NAME_G1A)" --internal="$(INTERNAL)"
G3AF := -n basic:"$(NAME_G3A)" -i uns:"$(ICON_CG_UNS)" -i sel:"$(ICON_CG_SEL)"
G3AF := -n "$(NAME_G3A)" --icon-uns="$(ICON_CG_UNS)" --icon-sel="$(ICON_CG_SEL)"
ifeq "$(TOOLCHAIN_FX)" ""
TOOLCHAIN_FX := sh3eb-elf
@ -104,13 +104,13 @@ $(TARGET_FX): $(obj-fx) $(deps-fx)
@ mkdir -p $(dir $@)
$(TOOLCHAIN_FX)-gcc -o $(ELF_FX) $(obj-fx) $(CFLAGSFX) $(LDFLAGSFX)
$(TOOLCHAIN_FX)-objcopy -O binary $(BINFLAGS) $(ELF_FX) $(BIN_FX)
fxg1a $(BIN_FX) -o $@ $(G1AF)
fxgxa --g1a $(BIN_FX) -o $@ $(G1AF)
$(TARGET_CG): $(obj-cg) $(deps-cg)
@ mkdir -p $(dir $@)
$(TOOLCHAIN_CG)-gcc -o $(ELF_CG) $(obj-cg) $(CFLAGSCG) $(LDFLAGSCG)
$(TOOLCHAIN_CG)-objcopy -O binary $(BINFLAGS) $(ELF_CG) $(BIN_CG)
mkg3a $(G3AF) $(BIN_CG) $@
fxgxa --g3a $(BIN_CG) -o $@ $(G3AF)
# C sources
build-fx/%.c.o: %.c

View File

@ -1,11 +1,12 @@
# Build files
/build-fx
/build-cg
/build-cg-push
/*.g1a
/*.g3a
# Python bytecode
__pycache__/
__pycache__/
# Common IDE files
*.sublime-project

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
fxsdk/assets/icon-cg.xcf Normal file

Binary file not shown.

View File

@ -0,0 +1,18 @@
set(FXCONV_COMPILER_NAME $ENV{FXCONV_PATH})
if(NOT FXCONV_COMPILER_NAME)
set(FXCONV_COMPILER_NAME "fxconv")
endif()
find_program(FXCONV_COMPILER_PATH "${FXCONV_COMPILER_NAME}")
if(FXCONV_COMPILER_PATH STREQUAL "FXCONV_COMPILER_PATH-NOTFOUND")
message(FATAL_ERROR "'${FXCONV_COMPILER}' not found!")
endif()
set(CMAKE_FXCONV_COMPILER "${FXCONV_COMPILER_PATH}")
set(CMAKE_FXCONV_COMPILER_ENV_VAR "FXCONV_PATH")
set(CMAKE_FXCONV_OUTPUT_EXTENSION ".o")
# Save these results
configure_file(
"${CMAKE_CURRENT_LIST_DIR}/CMakeFXCONVCompiler.cmake.in"
"${CMAKE_PLATFORM_INFO_DIR}/CMakeFXCONVCompiler.cmake")

View File

@ -0,0 +1,4 @@
set(CMAKE_FXCONV_COMPILER "@CMAKE_FXCONV_COMPILER@")
set(CMAKE_FXCONV_COMPILER_LOADED 1)
set(CMAKE_FXCONV_OUTPUT_EXTENSION "@CMAKE_FXCONV_OUTPUT_EXTENSION@")
set(CMAKE_FXCONV_COMPILER_ENV_VAR "@CMAKE_FXCONV_COMPILER_ENV_VAR@")

View File

@ -0,0 +1,15 @@
# The compiler command is a bit shaky because we want to dynamically add
# --converters flags but too much user code carelessly sets C/C++ flags for all
# languages globally, meaning our own flags are mostly unusable.
#
# We work around this problem by using CMAKE_FXCONV_COMPILER_ARG1, a special
# variable whose value is automatically inserted as an argument after the
# <CMAKE_FXCONV_COMPILER> placeholder in the COMPILE_OBJECT rule. We then
# modify CMAKE_FXCONV_COMPILER_ARG1 from the fxconv_declare_converters()
# function to supply the --converters flag.
set(CMAKE_FXCONV_COMPILER_ARG1)
set(CMAKE_FXCONV_COMPILE_OBJECT
"<CMAKE_FXCONV_COMPILER> --toolchain=sh-elf --${FXSDK_PLATFORM} \
<SOURCE> -o <OBJECT>")
set(CMAKE_FXCONV_INFORMATION_LOADED 1)

View File

@ -0,0 +1 @@
set(CMAKE_FXCONV_COMPILER_WORKS 1 CACHE INTERNAL "")

View File

@ -9,6 +9,7 @@ set(CMAKE_SYSTEM_PROCESSOR sh)
set(FXSDK_PLATFORM fx)
set(FXSDK_PLATFORM_LONG fx9860G)
set(FXSDK_TOOLCHAIN sh-elf-)
set(CMAKE_C_COMPILER sh-elf-gcc)
set(CMAKE_CXX_COMPILER sh-elf-g++)
@ -16,7 +17,7 @@ set(CMAKE_C_FLAGS_INIT "")
set(CMAKE_CXX_FLAGS_INIT "")
add_compile_options(-mb -ffreestanding -nostdlib -Wa,--dsp)
add_link_options(-nostdlib)
add_link_options(-nostdlib -Wl,--no-warn-rwx-segments)
link_libraries(-lgcc)
add_compile_definitions(TARGET_FX9860G)
@ -38,5 +39,18 @@ endforeach()
execute_process(
COMMAND ${CMAKE_C_COMPILER} --print-file-name=.
OUTPUT_VARIABLE FXSDK_COMPILER_INSTALL
OUTPUT_STRIP_TRAILING_WHITESPACE
)
OUTPUT_STRIP_TRAILING_WHITESPACE)
# Provide fxSDK sysroot and standard install folders
execute_process(
COMMAND fxsdk path sysroot
OUTPUT_VARIABLE FXSDK_SYSROOT
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND fxsdk path include
OUTPUT_VARIABLE FXSDK_INCLUDE
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND fxsdk path lib
OUTPUT_VARIABLE FXSDK_LIB
OUTPUT_STRIP_TRAILING_WHITESPACE)

View File

@ -9,6 +9,7 @@ set(CMAKE_SYSTEM_PROCESSOR sh)
set(FXSDK_PLATFORM cg)
set(FXSDK_PLATFORM_LONG fxCG50)
set(FXSDK_TOOLCHAIN sh-elf-)
set(CMAKE_C_COMPILER sh-elf-gcc)
set(CMAKE_CXX_COMPILER sh-elf-g++)
@ -16,7 +17,7 @@ set(CMAKE_C_FLAGS_INIT "")
set(CMAKE_CXX_FLAGS_INIT "")
add_compile_options(-m4-nofpu -mb -ffreestanding -nostdlib -Wa,--dsp)
add_link_options(-nostdlib)
add_link_options(-nostdlib -Wl,--no-warn-rwx-segments)
link_libraries(-lgcc)
add_compile_definitions(TARGET_FXCG50)
@ -38,5 +39,18 @@ endforeach()
execute_process(
COMMAND ${CMAKE_C_COMPILER} --print-file-name=.
OUTPUT_VARIABLE FXSDK_COMPILER_INSTALL
OUTPUT_STRIP_TRAILING_WHITESPACE
)
OUTPUT_STRIP_TRAILING_WHITESPACE)
# Provide fxSDK sysroot and standard install folders
execute_process(
COMMAND fxsdk path sysroot
OUTPUT_VARIABLE FXSDK_SYSROOT
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND fxsdk path include
OUTPUT_VARIABLE FXSDK_INCLUDE
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND fxsdk path lib
OUTPUT_VARIABLE FXSDK_LIB
OUTPUT_STRIP_TRAILING_WHITESPACE)

View File

@ -1,13 +1,17 @@
function(find_simple_library _library _version_header _version_macro)
cmake_parse_arguments(CLV "" "PATH_VAR;VERSION_VAR" "" ${ARGN})
cmake_parse_arguments(CLV "" "PATH_VAR;VERSION_VAR" "OTHER_MACROS" ${ARGN})
# Find the library path
execute_process(
COMMAND ${CMAKE_C_COMPILER} "-print-file-name=${_library}"
OUTPUT_VARIABLE LIB_PATH OUTPUT_STRIP_TRAILING_WHITESPACE)
COMMAND fxsdk path lib
OUTPUT_VARIABLE FXSDK_LIB OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND fxsdk path include
OUTPUT_VARIABLE FXSDK_INCLUDE OUTPUT_STRIP_TRAILING_WHITESPACE)
if("${LIB_PATH}" STREQUAL "${_library}")
message("lib not found")
set(LIB_PATH "${FXSDK_LIB}/${_library}")
if(NOT EXISTS "${LIB_PATH}")
message(SEND_ERROR "find_simple_library: Library not found: ${LIB_PATH}")
return()
endif()
@ -16,11 +20,9 @@ function(find_simple_library _library _version_header _version_macro)
endif()
# Find the version header
execute_process(
COMMAND ${CMAKE_C_COMPILER} "-print-file-name=${_version_header}"
OUTPUT_VARIABLE HEADER_PATH OUTPUT_STRIP_TRAILING_WHITESPACE)
if("${HEADER_PATH}" STREQUAL "${_version_header}")
set(HEADER_PATH "${FXSDK_INCLUDE}/${_version_header}")
if(NOT EXISTS "${HEADER_PATH}")
message(SEND_ERROR "find_simple_library: Header not found: ${HEADER_PATH}")
return()
endif()
@ -33,4 +35,13 @@ function(find_simple_library _library _version_header _version_macro)
if(DEFINED CLV_VERSION_VAR)
set("${CLV_VERSION_VAR}" "${VERSION}" PARENT_SCOPE)
endif()
# Extract other macros from the header
foreach(MACRO_NAME IN LISTS CLV_OTHER_MACROS)
set(SED "s/^\\s*#\\s*define\\s*${MACRO_NAME}\\s*(.+)\\s*$/\\1/p; d")
execute_process(
COMMAND sed -E "${SED}" "${HEADER_PATH}"
OUTPUT_VARIABLE MACRO_VALUE OUTPUT_STRIP_TRAILING_WHITESPACE)
set("${MACRO_NAME}" "${MACRO_VALUE}" PARENT_SCOPE)
endforeach()
endfunction()

View File

@ -1,5 +1,7 @@
set(CMAKE_FXCONV_COMPILE_OBJECT
"fxconv <SOURCE> -o <OBJECT> --toolchain=sh-elf --${FXSDK_PLATFORM}")
# For the template of the custom language, see:
# - https://gitlab.kitware.com/cmake/cmake/-/blob/master/Modules/CMakeAddNewLanguage.txt
# - https://stackoverflow.com/questions/59536644/cmake-custom-compiler-linker-language-not-found
enable_language(FXCONV)
function(fxconv_declare_assets)
cmake_parse_arguments(CONV "WITH_METADATA" "" "" ${ARGN})
@ -11,6 +13,7 @@ function(fxconv_declare_assets)
# Set up a dependency to the local fxconv-metadata.txt
if(DEFINED CONV_WITH_METADATA)
get_filename_component(DIR "${ASSET}" DIRECTORY)
get_filename_component(DIR "${DIR}" ABSOLUTE)
set(METADATA "${DIR}/fxconv-metadata.txt")
get_filename_component(METADATA "${METADATA}" ABSOLUTE
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
@ -26,10 +29,9 @@ function(fxconv_declare_converters)
list(APPEND FXCONV_CONVERTERS "${CONVERTER_PATH}")
endforeach()
# Record the names in the list
# Record the full list in the parent scope
set(FXCONV_CONVERTERS "${FXCONV_CONVERTERS}" PARENT_SCOPE)
# Update the compile command
set(CMAKE_FXCONV_COMPILE_OBJECT
"fxconv <SOURCE> -o <OBJECT> --toolchain=sh-elf --${FXSDK_PLATFORM} --converters=${FXCONV_CONVERTERS}" PARENT_SCOPE)
# Also push a language flag through the special "ARG1" variable
set(CMAKE_FXCONV_COMPILER_ARG1 "--converters=${FXCONV_CONVERTERS}"
PARENT_SCOPE)
endfunction()

View File

@ -0,0 +1,7 @@
# Who doesn't love to deal with escaping?!
function(shell_escape _str _out)
string(REPLACE "'" "\\'" _str "${_str}")
string(REPLACE "(" "\\(" _str "${_str}")
string(REPLACE ")" "\\)" _str "${_str}")
set("${_out}" "${_str}" PARENT_SCOPE)
endfunction()

View File

@ -1,3 +1,5 @@
include(FxsdkUtils)
function(generate_g1a)
cmake_parse_arguments(G1A "" "TARGET;OUTPUT;NAME;INTERNAL;VERSION;DATE;ICON" "" ${ARGN})
@ -16,23 +18,33 @@ function(generate_g1a)
if(DEFINED G1A_OUTPUT)
get_filename_component(G1A_OUTPUT "${G1A_OUTPUT}" ABSOLUTE
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
shell_escape("${G1A_OUTPUT}" G1A_OUTPUT)
else()
set(G1A_OUTPUT "${G1A_TARGET}.g1a")
endif()
# Compute the set of fxg1a arguments
# Compute the set of fxgxa arguments
set(FXG1A_ARGS "")
set(FXGXA_ARGS "")
# Support empty names enen though they're not normally used in g1a files
# Support empty names even though they're not normally used in g1a files
if(DEFINED G1A_NAME OR "NAME" IN_LIST G1A_KEYWORDS_MISSING_VALUES)
list(APPEND FXG1A_ARGS "-n" "${G1A_NAME}")
list(APPEND FXGXA_ARGS "-n" "${G1A_NAME}")
endif()
if(DEFINED G1A_ICON)
get_filename_component(G1A_ICON "${G1A_ICON}" ABSOLUTE
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
list(APPEND FXG1A_ARGS "-i" "${G1A_ICON}")
shell_escape("${G1A_ICON}" G1A_ICONB)
list(APPEND FXGXA_ARGS "-i" "${G1A_ICONB}")
endif()
if(DEFINED G1A_INTERNAL)
list(APPEND FXGXA_ARGS "--internal=${G1A_INTERNAL}")
endif()
if(DEFINED G1A_VERSION)
list(APPEND FXGXA_ARGS "--version=${G1A_VERSION}")
endif()
string(REGEX REPLACE "sh-elf-gcc$" "sh-elf-objcopy" OBJCOPY "${CMAKE_C_COMPILER}")
@ -40,7 +52,7 @@ function(generate_g1a)
add_custom_command(
TARGET "${G1A_TARGET}" POST_BUILD
COMMAND "${OBJCOPY}" -O binary -R .bss -R .gint_bss "${G1A_TARGET}" "${G1A_TARGET}.bin"
COMMAND fxg1a ${FXG1A_ARGS} -o "${G1A_OUTPUT}" "${G1A_TARGET}.bin"
COMMAND fxgxa --g1a ${FXGXA_ARGS} -o "${G1A_OUTPUT}" "${G1A_TARGET}.bin"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
if(DEFINED G1A_ICON)

View File

@ -1,5 +1,7 @@
include(FxsdkUtils)
function(generate_g3a)
cmake_parse_arguments(G3A "" "TARGET;OUTPUT;NAME" "ICONS" ${ARGN})
cmake_parse_arguments(G3A "" "TARGET;OUTPUT;NAME;VERSION" "ICONS" ${ARGN})
# Check arguments
@ -21,19 +23,27 @@ function(generate_g3a)
if(DEFINED G3A_OUTPUT)
get_filename_component(G3A_OUTPUT "${G3A_OUTPUT}" ABSOLUTE
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
shell_escape("${G3A_OUTPUT}" G3A_OUTPUT)
else()
set(G3A_OUTPUT "${G3A_TARGET}.g3a")
endif()
# Compute the set of mkg3a arguments
# Compute the set of fxgxa arguments
set(MKG3A_ARGS "")
set(FXGXA_ARGS "")
# Empty names are commonly used to avoid having the file name printed over
# the icon, but using ARGN in cmake_parse_arguments() drops the empty string.
# Check in KEYWORDS_MISSING_VALUES as a complement.
if(DEFINED G3A_NAME OR "NAME" IN_LIST G3A_KEYWORDS_MISSING_VALUES)
list(APPEND MKG3A_ARGS "-n" "basic:${G3A_NAME}")
if("${G3A_NAME}" STREQUAL "")
set(G3A_NAME "''")
endif()
list(APPEND FXGXA_ARGS "-n" "${G3A_NAME}")
endif()
if(DEFINED G3A_VERSION)
list(APPEND FXGXA_ARGS "--version=${G3A_VERSION}")
endif()
if(DEFINED G3A_ICONS)
@ -43,16 +53,19 @@ function(generate_g3a)
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
get_filename_component(G3A_ICON2 "${G3A_ICON2}" ABSOLUTE
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
list(APPEND MKG3A_ARGS "-i" "uns:${G3A_ICON1}" "-i" "sel:${G3A_ICON2}")
shell_escape("${G3A_ICON1}" G3A_ICON1B)
shell_escape("${G3A_ICON2}" G3A_ICON2B)
list(APPEND FXGXA_ARGS "--icon-uns=${G3A_ICON1B}" "--icon-sel=${G3A_ICON2B}")
endif()
string(REPLACE "gcc" "objcopy" OBJCOPY "${CMAKE_C_COMPILER}")
add_custom_command(
TARGET "${G3A_TARGET}" POST_BUILD
COMMAND "${OBJCOPY}" -O binary -R .bss -R .gint_bss "${G3A_TARGET}" "${G3A_TARGET}.bin"
COMMAND mkg3a ${MKG3A_ARGS} "${G3A_TARGET}.bin" "${G3A_OUTPUT}"
COMMAND ${OBJCOPY} -O binary -R .bss -R .gint_bss ${G3A_TARGET} ${G3A_TARGET}.bin
COMMAND "$<IF:$<CONFIG:FastLoad>,,fxgxa;--g3a;${FXGXA_ARGS};${G3A_TARGET}.bin;-o;${G3A_OUTPUT}>"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
COMMAND_EXPAND_LISTS
)
if(DEFINED G3A_ICONS)
set_target_properties("${G3A_TARGET}" PROPERTIES

View File

@ -1,7 +1,8 @@
#! /usr/bin/env bash
# Note: this line is edited at compile time to insert the install folder
PREFIX=@FXSDK_PREFIX@
# These lines are substituted at install time
PREFIX="@CMAKE_INSTALL_PREFIX@"
VERSION="@CMAKE_PROJECT_VERSION@"
R=$(printf "\e[31;1m")
g=$(printf "\e[32m\e[3m")
@ -9,8 +10,9 @@ n=$(printf "\e[0m")
TAG=$(printf "\e[36m<fxSDK>\e[0m")
usage_string=$(cat << EOF
usage: ${R}fxsdk${n} (${R}new${n}|${R}build${n}|${R}build-fx${n}|${R}build-\
cg${n}|${R}send${n}|${R}send-fx${n}|${R}send-cg${n}) [${g}ARGUMENTS${n}...]
fxSDK version $VERSION
usage: ${R}fxsdk${n} (${R}new${n}|${R}build-fx${n}|${R}build-cg${n}|\
${R}send-fx${n}|${R}send-cg${n}|...) [${g}ARGUMENTS${n}...]
This program is a command-line helper for the fxSDK, a set of tools used in
conjunction with gint to develop add-ins for CASIO fx-9860G and fx-CG 50.
@ -26,15 +28,29 @@ ${R}fxsdk${n} (${R}build${n}|${R}build-fx${n}|${R}build-cg${n}) [${R}-c${n}] \
(usually for .g3a add-ins). The first form compiles in every existing build
folder, and configures for both if none exists.
With -c, reconfigure but don't build (CMake only).
With -c, reconfigure but do not build (CMake only).
With -s, also sends the resulting program to the calculator.
Other arguments are passed to CMake (if using -c) or make (otherwise). You
can pass -c or -s to CMake/make by specifying --.
${R}fxsdk${n} ${R}build-cg-push${n} [${R}-c${n}] [${R}-s${n}] [${R}--${n}] \
[${g}<ARGS>${n}...]
Builds the current project for fx-CG 50 for a "fast load" to the calculator.
This uses Add-In Push by circuit10, which immediately launches the add-in
without saving it to storage memory, and is much faster than LINK. Options
are identical to other build commands. Typical workflows will always set -s
(which requires libusb support in fxlink).
${R}fxsdk${n} (${R}send${n}|${R}send-fx${n}|${R}send-cg${n})
Sends the target file to the calculator. Uses p7 (which must be installed
externally) for fx-9860G. Currently not implemented for fx-CG 50, as it
requires detecting and mounting the calculator (same for the Graph 35+E II).
externally) for the fx-9860G, and fxlink for the fx-CG. For the G-III series,
call fxlink directly instead of this command.
${R}fxsdk${n} ${R}path${n} (${R}sysroot${n}|${R}include${n}|${R}lib${n})
Prints commonly-used paths in the SuperH sysroot:
${R}sysroot${n} The root folder of the SuperH toolchain and libraries
${R}include${n} Install folder for user's headers
${R}lib${n} Install folder for user's library files
EOF
)
@ -94,12 +110,14 @@ fxsdk_new_project() {
mkdir -p "$1"/{assets-fx,assets-cg}/img
cp -r "$assets"/assets-fx/* "$1"/assets-fx/img/
cp -r "$assets"/assets-cg/* "$1"/assets-cg/img/;;
cp -r "$assets"/assets-cg/* "$1"/assets-cg/img/
cp "$assets/icon-cg.xcf" "$1/assets-cg";;
"CMake")
cp "$assets/CMakeLists.txt" "$1"
cp -r "$assets"/assets-fx "$1"/
cp -r "$assets"/assets-cg "$1"/;;
cp -r "$assets"/assets-cg "$1"/
cp "$assets/icon-cg.xcf" "$1/assets-cg";;
esac
cp "$assets"/gitignore "$1"/.gitignore
@ -140,6 +158,9 @@ fxsdk_build_fx() {
fxsdk_build_cg() {
fxsdk_build_in "cg" "FXCG50" "$@"
}
fxsdk_build_cg_push() {
fxsdk_build_in "cg-push" "FXCG50" "$@"
}
fxsdk_build_in() {
platform="$1"
@ -168,11 +189,15 @@ fxsdk_build_in() {
fi
if [[ ! -e "build-$platform/Makefile" || ! -z "$configure" ]]; then
platform_args=()
if [[ "$platform" == *"-push" ]]; then
platform_args=( "-DCMAKE_BUILD_TYPE=FastLoad" )
fi
cmake -B "build-$platform" \
-DCMAKE_MODULE_PATH="$PREFIX/lib/cmake/fxsdk" \
-DCMAKE_TOOLCHAIN_FILE="$PREFIX/lib/cmake/fxsdk/$toolchain.cmake" \
-DFXSDK_CMAKE_MODULE_PATH="$PREFIX/lib/cmake/fxsdk" \
"${cmake_extra_args[@]}"
"${cmake_extra_args[@]}" "${platform_args[@]}"
if [[ $? != 0 ]]; then
return 1
fi
@ -210,13 +235,53 @@ fxsdk_send() {
}
fxsdk_send_fx() {
echo "$TAG Installing for fx9860g using p7"
make install-fx
echo "$TAG Installing for fx-9860G using p7"
if ! command -v p7 >/dev/null 2>&1; then
echo "error: p7 is not installed or not available"
return 1
fi
g1a_files=$(find -maxdepth 1 -name '*.g1a')
echo "$TAG Running: p7 send -f ${g1a_files}"
p7 send -f ${g1a_files}
}
fxsdk_send_cg() {
# TODO
echo "error: this is tricky and not implemented yet, sorry x_x"
echo "$TAG Installing for fx-CG using fxlink"
if ! command -v fxlink >/dev/null 2>&1; then
echo "error: fxlink is not installed or not available"
return 1
fi
g3a_files=$(find -maxdepth 1 -name '*.g3a')
echo "$TAG Running: fxlink -sw ${g3a_files}"
fxlink -sw ${g3a_files}
}
fxsdk_send_cg-push() {
echo "$TAG Fast loading to fx-CG using fxlink; open Add-In Push on the calc"
if ! command -v fxlink >/dev/null 2>&1; then
echo "error: fxlink is not installed or not available"
return 1
fi
bin_files=$(find "build-cg-push" -maxdepth 1 -name '*.bin')
echo "$TAG Running: fxlink -pw ${bin_files}"
fxlink -pw ${bin_files}
}
fxsdk_path() {
case "$1" in
"sysroot")
echo "$PREFIX/share/fxsdk/sysroot";;
"include")
echo "$PREFIX/share/fxsdk/sysroot/sh3eb-elf/include";;
"lib")
echo "$PREFIX/share/fxsdk/sysroot/sh3eb-elf/lib";;
"")
echo "error: no path specified; try 'fxsdk --help'" >&2
exit 1;;
*)
echo "error: unknown path '$1'; try 'fxsdk --help'" >&2
exit 1;;
esac
}
# Parse command name
@ -233,6 +298,8 @@ case "$1" in
fxsdk_build_fx "${@:2}";;
"build-cg"|"bc"|"bcg")
fxsdk_build_cg "${@:2}";;
"build-cg-push"|"bcgp")
fxsdk_build_cg_push "${@:2}";;
# Install
"send"|"s")
@ -242,9 +309,15 @@ case "$1" in
"send-cg"|"sc"|"scg")
fxsdk_send_cg;;
# Utilities
"path")
fxsdk_path "${@:2}";;
# Misc
-h|--help|-\?)
usage 0;;
--version)
echo "fxSDK version $VERSION";;
?*)
error "unknown command '$1'"
exit 1;;

View File

@ -18,8 +18,13 @@ install:
@ make -C build install
uninstall:
@ echo "<fxsdk> Removing the SuperH sysroot..."
@ if [ -d "$(shell fxsdk path sysroot)" ]; then \
rm -r "$(shell fxsdk path sysroot)"; \
fi
@ echo "<fxsdk> Uninstalling fxSDK tools..."
@ if [ -e build/install_manifest.txt ]; then \
xargs rm -f < build/install_manifest.txt; \
xargs rm -f < build/install_manifest.txt; \
fi
.PHONY: configure build install uninstall

View File

@ -0,0 +1,57 @@
# Locate the library file and includes
find_library(
LIBFXLINK_PATH "fxlink"
HINTS "$ENV{HOME}/.local/lib" "$ENV{FXSDK_PATH}/lib"
)
if(LIBFXLINK_PATH STREQUAL "LIBFXLINK_PATH-NOTFOUND")
message(SEND_ERROR
"Could not find libfxlink.a!\n"
"You can specify the install path with the environment variable "
"FXSDK_PATH, such as FXSDK_PATH=$HOME/.local")
else()
get_filename_component(LIBFXLINK_PATH "${LIBFXLINK_PATH}/../.." ABSOLUTE)
set(LIBFXLINK_LIB "${LIBFXLINK_PATH}/lib/libfxlink.a")
set(LIBFXLINK_INCLUDE "${LIBFXLINK_PATH}/include")
message("(libfxlink) Found libfxlink at: ${LIBFXLINK_LIB}")
message("(libfxlink) Will take includes from: ${LIBFXLINK_INCLUDE}")
endif()
# Find library version
if(NOT EXISTS "${LIBFXLINK_INCLUDE}/fxlink/config.h")
message(SEND_ERROR
"No <fxlink/config.h> exists at ${LIBFXLINK_INCLUDE}/fxlink/config.h\n"
"Is libfxlink installed alongside the headers?")
endif()
execute_process(
COMMAND sed "s/#define FXLINK_VERSION \"\\([^\"]\\{1,\\}\\)\"/\\1/p; d"
"${LIBFXLINK_INCLUDE}/fxlink/config.h"
OUTPUT_VARIABLE LIBFXLINK_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE)
message("(libfxlink) Library version found in header: ${LIBFXLINK_VERSION}")
# Handle find_package() arguments and find dependencies
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LibFxlink
REQUIRED_VARS LIBFXLINK_LIB LIBFXLINK_INCLUDE
VERSION_VAR LIBFXLINK_VERSION)
find_package(PkgConfig REQUIRED)
pkg_check_modules(libusb REQUIRED libusb-1.0 IMPORTED_TARGET)
# Generate targets
if(LibFxlink_FOUND)
if(NOT TARGET LibFxlink::LibFxlink)
add_library(LibFxlink::LibFxlink UNKNOWN IMPORTED)
endif()
set_target_properties(LibFxlink::LibFxlink PROPERTIES
IMPORTED_LOCATION "${LIBFXLINK_LIB}"
INTERFACE_INCLUDE_DIRECTORIES "${LIBFXLINK_INCLUDE}")
target_link_libraries(LibFxlink::LibFxlink INTERFACE PkgConfig::libusb)
endif()

148
libfxlink/defs.c Normal file
View File

@ -0,0 +1,148 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/defs.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
char const *fmt_to_ANSI(int format)
{
static char buf[64];
int n = 0;
strcpy(buf, "\e[0m");
n += 4;
int FG = fmt_FG(format);
int BG = fmt_BG(format);
if(FG != 0)
n += sprintf(buf+n, "\e[%dm", 30 + FG - 1);
if(BG != 0)
n += sprintf(buf+n, "\e[%dm", 40 + BG - 1);
if(fmt_BOLD(format))
strcpy(buf+n, "\e[1m"), n += 4;
if(fmt_DIM(format))
strcpy(buf+n, "\e[2m"), n += 4;
if(fmt_ITALIC(format))
strcpy(buf+n, "\e[3m"), n += 4;
return buf;
}
char *fxlink_gen_file_name(char const *path, char const *name,
char const *suffix)
{
char *filename = NULL;
int counter = 1;
time_t time_raw;
struct tm time_bd;
time(&time_raw);
localtime_r(&time_raw, &time_bd);
while(1) {
asprintf(&filename, "%s/fxlink-%.16s-%04d.%02d.%02d-%02dh%02d-%d%s",
path, name, time_bd.tm_year + 1900, time_bd.tm_mon + 1,
time_bd.tm_mday, time_bd.tm_hour, time_bd.tm_min, counter, suffix);
if(!filename)
continue;
/* Try to find a name for a file that doesn't exist */
if(access(filename, F_OK) == -1)
break;
free(filename);
counter++;
}
return filename;
}
int fxlink_multipoll(int timeout, struct pollfd *fds1, int count1, ...)
{
/* Convenience macro to iterate on file descriptor arrays */
#define FOREACH_FD_ARRAY(FDS, COUNT, COUNT_ACC, BODY) do { \
struct pollfd *FDS; int COUNT, COUNT_ACC; va_list args; \
va_start(args, count1); \
for(FDS = fds1, COUNT = count1, COUNT_ACC = 0; FDS; \
COUNT_ACC += COUNT, \
FDS = va_arg(args, struct pollfd *), \
COUNT = va_arg(args, int)) { BODY } \
va_end(args); } while(0)
/* Determine total number of file descriptors to watch */
int total = 0;
FOREACH_FD_ARRAY(fds, count, count_acc, {
total += count;
});
struct pollfd *concat = malloc(total * sizeof *concat);
if(!concat)
return -ENOMEM;
/* Copy from individual arrays to full array */
FOREACH_FD_ARRAY(fds, count, count_acc, {
memcpy(concat + count_acc, fds, count * sizeof *fds);
});
int rc = poll(concat, total, timeout);
/* Copy back from full array to individual arrays */
FOREACH_FD_ARRAY(fds, count, count_acc, {
memcpy(fds, concat + count_acc, count * sizeof *fds);
});
free(concat);
return rc;
}
char const *fxlink_size_string(int bytes)
{
static char str[32];
if(bytes > 1000000)
sprintf(str, "%d.%d MB", bytes / 1000000, (bytes % 1000000) / 100000);
else if(bytes > 1000)
sprintf(str, "%d.%d kB", bytes / 1000, (bytes % 1000) / 100);
else
sprintf(str, "%d B", bytes);
return str;
}
delay_t delay_none(void)
{
return 0;
}
delay_t delay_seconds(int seconds)
{
return seconds * 4;
}
delay_t delay_infinite(void)
{
return -1;
}
bool delay_cycle(delay_t *delay)
{
if(*delay == 0) return true;
struct timespec spec = { .tv_sec=0, .tv_nsec=250000000 };
int rc;
/* Account for interrupts in the nanosleep(2) call */
struct timespec req = spec;
do rc = nanosleep(&req, &req);
while(rc == -1 && errno == EINTR);
if(*delay > 0) (*delay)--;
return false;
}

927
libfxlink/devices.c Normal file
View File

@ -0,0 +1,927 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/devices.h>
#include <fxlink/logging.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
char const *fxlink_device_id(struct fxlink_device const *fdev)
{
static char str[32];
sprintf(str, "%03d:%03d", fdev->busNumber, fdev->deviceAddress);
return str;
}
char const *fxlink_device_status_string(struct fxlink_device const *fdev)
{
switch(fdev->status) {
case FXLINK_FDEV_STATUS_PENDING:
return "PENDING";
case FXLINK_FDEV_STATUS_IGNORED:
return "IGNORED";
case FXLINK_FDEV_STATUS_ERROR:
return "ERROR";
case FXLINK_FDEV_STATUS_IDLE:
return "IDLE";
case FXLINK_FDEV_STATUS_CONNECTED:
return "CONNECTED";
default:
return "<INVALID>";
}
}
char const *fxlink_device_system_string(struct fxlink_device const *fdev)
{
if(!fdev->calc)
return "<!CALC>";
switch(fdev->calc->system) {
case FXLINK_CALC_SYSTEM_UNKNOWN:
return "UNKNOWN";
case FXLINK_CALC_SYSTEM_LINKSCSI:
return "LINKSCSI";
case FXLINK_CALC_SYSTEM_CESG502:
return "CESG502";
case FXLINK_CALC_SYSTEM_GINT:
return "GINT";
default:
return "<INVALID>";
}
}
static bool is_casio_calculator(int idVendor, int idProduct)
{
return idVendor == 0x07cf && (idProduct == 0x6101 || idProduct == 0x6102);
}
static struct libusb_config_descriptor *active_config_descriptor(
struct fxlink_device const *fdev)
{
struct libusb_config_descriptor *cd = NULL;
int rc = libusb_get_active_config_descriptor(fdev->dp, &cd);
if(rc != 0) {
if(rc != LIBUSB_ERROR_NOT_FOUND)
elog_libusb(rc, "cannot request config descriptor");
return NULL;
}
return cd;
}
/* Gather a list of a classes; either one is specified for the entire device in
the device descriptor, either one is specified for each interface in
interface descriptors. */
static bool find_interface_classes(struct fxlink_device *fdev)
{
struct fxlink_calc *calc = fdev->calc;
struct libusb_device_descriptor dc;
libusb_get_device_descriptor(fdev->dp, &dc);
/* Fixed class specified in the device descriptor */
if(dc.bDeviceClass != LIBUSB_CLASS_PER_INTERFACE) {
calc->interface_count = 1;
calc->classes = malloc(1 * sizeof *calc->classes);
calc->classes[0] = (dc.bDeviceClass << 8) | dc.bDeviceSubClass;
return true;
}
/* Class specified by the interface descriptors */
struct libusb_config_descriptor *cd = active_config_descriptor(fdev);
if(!cd)
return false;
calc->interface_count = cd->bNumInterfaces;
calc->classes = malloc(cd->bNumInterfaces * sizeof *calc->classes);
for(int i = 0; i < cd->bNumInterfaces; i++) {
struct libusb_interface const *intf = &cd->interface[i];
struct libusb_interface_descriptor const *id = &intf->altsetting[0];
calc->classes[i] = (id->bInterfaceClass << 8) | id->bInterfaceSubClass;
if(calc->classes[i] == 0xff77 && calc->fxlink_inum < 0)
calc->fxlink_inum = i;
}
return true;
}
/* Find the endpoints for the fxlink interface. */
static bool find_fxlink_endpoints(struct fxlink_device *fdev, bool quiet)
{
struct fxlink_calc *calc = fdev->calc;
struct fxlink_comm *comm = calloc(1, sizeof *comm);
if(!comm) {
elog("find_fxlink_endpoints(): %m\n");
return false;
}
fdev->comm = comm;
comm->ep_bulk_IN = 0xff;
comm->ep_bulk_OUT = 0xff;
struct libusb_config_descriptor *cd = active_config_descriptor(fdev);
if(!cd) {
free(comm);
return false;
}
struct libusb_interface const *intf = &cd->interface[calc->fxlink_inum];
struct libusb_interface_descriptor const *id = &intf->altsetting[0];
if(!quiet) {
hlog("calculators %s", fxlink_device_id(fdev));
log_("fxlink interface has %d endpoints\n", id->bNumEndpoints);
}
for(int i = 0; i < id->bNumEndpoints; i++) {
struct libusb_endpoint_descriptor const *ed = &id->endpoint[i];
int dir = ed->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK;
int type = ed->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK;
if(dir == LIBUSB_ENDPOINT_OUT
&& type == LIBUSB_TRANSFER_TYPE_BULK)
comm->ep_bulk_OUT = ed->bEndpointAddress;
if(dir == LIBUSB_ENDPOINT_IN
&& type == LIBUSB_TRANSFER_TYPE_BULK)
comm->ep_bulk_IN = ed->bEndpointAddress;
}
return true;
}
/* Determine system based on interface heuristics */
static void determine_system_type(struct fxlink_device *fdev)
{
struct fxlink_calc *calc = fdev->calc;
if(!calc)
return;
calc->system = FXLINK_CALC_SYSTEM_UNKNOWN;
/* Single class of type SCSI -> LINKSCSI */
if(calc->interface_count == 1 && calc->classes[0] == 0x0806) {
calc->system = FXLINK_CALC_SYSTEM_LINKSCSI;
return;
}
/* Single class of type vendor-specific 00 -> CESG502 */
if(calc->interface_count == 1 && calc->classes[0] == 0xff00) {
calc->system = FXLINK_CALC_SYSTEM_CESG502;
return;
}
/* Has an fxlink interface -> GINT */
for(int i = 0; i < calc->interface_count; i++) {
if(calc->classes[i] == 0xff77) {
calc->system = FXLINK_CALC_SYSTEM_GINT;
return;
}
}
}
/* Retrieve the serial. Returns a duplicated copy, NULL on error. */
static char *retrieve_serial_number(struct fxlink_device *fdev)
{
if(!fdev->dh)
return NULL;
struct libusb_device_descriptor dc;
libusb_get_device_descriptor(fdev->dp, &dc);
if(!dc.iSerialNumber)
return NULL;
char str[256];
int rc = libusb_get_string_descriptor_ascii(fdev->dh, dc.iSerialNumber,
(void *)str, 256);
if(rc < 0) {
wlog_libusb(rc, "could not retrieve serial number");
return NULL;
}
/* LINK sends a 12-byte serial number with four leading 0. Remove them */
char *serial = str;
if(rc == 12 && !strncmp(serial, "0000", 4))
serial += 4;
return strdup(serial);
}
void fxlink_device_analysis_1(struct fxlink_device *fdev, bool quiet)
{
struct fxlink_calc *calc = calloc(1, sizeof *calc);
if(!calc) {
elog("analyze_calculator(): %m\n");
return;
}
fdev->calc = calc;
calc->system = FXLINK_CALC_SYSTEM_UNKNOWN;
calc->fxlink_inum = -1;
if(!find_interface_classes(fdev)) {
fdev->status = FXLINK_FDEV_STATUS_ERROR;
return;
}
if(!quiet) {
hlog("calculators %s", fxlink_device_id(fdev));
log_("%1$d interface%2$s, class code%2$s", calc->interface_count,
calc->interface_count != 1 ? "s" : "");
for(int i = 0; i < calc->interface_count; i++) {
log_(" %02x.%02x", calc->classes[i] >> 8, calc->classes[i] & 0xff);
if(i == calc->fxlink_inum)
log_("(*)");
}
log_("\n");
}
determine_system_type(fdev);
/* Don't open SCSI devices because we can't interact with them (the kernel
already claims the interface) and doing so will prevent them from
subspending and disconnecting */
if(calc->system == FXLINK_CALC_SYSTEM_LINKSCSI) {
fdev->status = FXLINK_FDEV_STATUS_IGNORED;
return;
}
if(calc->fxlink_inum >= 0 && !find_fxlink_endpoints(fdev, quiet)) {
hlog("calculators %s", fxlink_device_id(fdev));
elog("non-conforming fxlink interface!\n");
fdev->status = FXLINK_FDEV_STATUS_ERROR;
return;
}
int rc = libusb_open(fdev->dp, &fdev->dh);
if(!quiet)
hlog("calculators %s", fxlink_device_id(fdev));
if(rc != 0) {
elog("opening device failed: %s\n", libusb_strerror(rc));
fdev->status = FXLINK_FDEV_STATUS_ERROR;
return;
}
if(!quiet)
log_("successfully opened device!\n");
/* Don't detach kernel drivers to avoid breaking the Mass Storage
communications if fxlink is ever started while the native LINK
application is running! */
libusb_set_auto_detach_kernel_driver(fdev->dh, false);
}
void fxlink_device_analysis_2(struct fxlink_device *fdev)
{
if(fdev->status != FXLINK_FDEV_STATUS_PENDING)
return;
fdev->calc->serial = retrieve_serial_number(fdev);
fdev->status = FXLINK_FDEV_STATUS_IDLE;
}
bool fxlink_device_ready_to_connect(struct fxlink_device const *fdev)
{
bool status = (fdev->status == FXLINK_FDEV_STATUS_IDLE) ||
(fdev->status == FXLINK_FDEV_STATUS_CONNECTED);
return fdev->calc && fdev->comm && status;
}
bool fxlink_device_has_fxlink_interface(struct fxlink_device const *fdev)
{
return fdev->calc && fdev->comm && (fdev->comm->ep_bulk_IN != 0xff);
}
bool fxlink_device_claim_fxlink(struct fxlink_device *fdev)
{
if(!fxlink_device_ready_to_connect(fdev) ||
!fxlink_device_has_fxlink_interface(fdev) ||
fdev->comm->claimed)
return false;
/* Allocate transfer data */
fdev->comm->buffer_IN_size = 2048;
fdev->comm->buffer_IN = malloc(fdev->comm->buffer_IN_size);
if(!fdev->comm->buffer_IN) {
elog("could not allocate buffer for bulk IN transfer\n");
fdev->status = FXLINK_FDEV_STATUS_ERROR;
return false;
}
hlog("calculators %s", fxlink_device_id(fdev));
int rc = libusb_claim_interface(fdev->dh, fdev->calc->fxlink_inum);
if(rc != 0) {
elog_libusb(rc, "claiming fxlink interface #%d failed",
fdev->calc->fxlink_inum);
fdev->status = FXLINK_FDEV_STATUS_ERROR;
return false;
}
fdev->comm->claimed = true;
fdev->status = FXLINK_FDEV_STATUS_CONNECTED;
log_("successfully claimed interface!\n");
return true;
}
void fxlink_device_get_properties(struct fxlink_device const *fdev,
struct fxlink_filter *p)
{
memset(p, 0, sizeof *p);
if(fdev->idProduct == 0x6101)
p->p7 = true;
if(fdev->idProduct == 0x6102)
p->mass_storage = true;
if(fdev->calc) {
p->serial = fdev->calc->serial;
p->intf_cesg502 = (fdev->calc->system == FXLINK_CALC_SYSTEM_CESG502);
p->intf_fxlink = (fdev->calc->fxlink_inum >= 0);
}
}
void fxlink_device_interrupt_transfers(struct fxlink_device *fdev)
{
struct fxlink_comm *comm = fdev->comm;
if(!comm)
return;
/* Interrupt transfers if any are still running */
if(comm->tr_bulk_IN && !comm->cancelled_IN) {
libusb_cancel_transfer(comm->tr_bulk_IN);
comm->cancelled_IN = true;
}
if(comm->tr_bulk_OUT && !comm->cancelled_OUT) {
libusb_cancel_transfer(comm->tr_bulk_OUT);
comm->cancelled_OUT = true;
}
}
void fxlink_device_cleanup(struct fxlink_device *fdev)
{
/* Close the device if it's open */
if(fdev->dh) {
/* Release the fxlink interface if it's claimed */
if(fdev->comm && fdev->comm->claimed)
libusb_release_interface(fdev->dh, fdev->calc->fxlink_inum);
libusb_close(fdev->dh);
fdev->dh = NULL;
}
/* Free associated memory */
if(fdev->calc) {
free(fdev->calc->classes);
free(fdev->calc->serial);
free(fdev->calc);
}
free(fdev->comm);
/* Unreference libusb devices so it can also be freed */
libusb_unref_device(fdev->dp);
fdev->dp = NULL;
}
//---
// Bulk transfers
//---
/* Note: this function is run by the even handler and can't do any crazy libusb
stuff like sync I/O or getting descriptors. */
static void bulk_IN_callback(struct libusb_transfer *transfer)
{
struct fxlink_device *fdev = transfer->user_data;
struct fxlink_comm *comm = fdev->comm;
bool resubmit = false;
void *data = transfer->buffer;
int data_size = transfer->actual_length;
if(transfer->status != LIBUSB_TRANSFER_COMPLETED)
hlog("calculators %s", fxlink_device_id(fdev));
switch(transfer->status) {
case LIBUSB_TRANSFER_COMPLETED:
/* Start or continue an fxlink transfer. When finished, don't resubmit,
instead have the user pick up the message before continuing. */
resubmit = true;
if(comm->ftransfer_IN) {
fxlink_transfer_receive(comm->ftransfer_IN, data, data_size);
if(fxlink_transfer_complete(comm->ftransfer_IN))
resubmit = false;
}
else {
comm->ftransfer_IN = fxlink_transfer_make_IN(data, data_size);
}
break;
/* Error: drop data and don't resubmit */
case LIBUSB_TRANSFER_ERROR:
log_("transfer error (%d bytes dropped)\n", data_size);
break;
/* Timeout: drop data, but resubmit */
case LIBUSB_TRANSFER_TIMED_OUT:
log_("transfer timed out (%d bytes dropped)\n", data_size);
break;
/* Cancelled transfer: drop data and don't try to resubmit */
case LIBUSB_TRANSFER_CANCELLED:
log_("transfer cancelled (%d bytes dropped)\n", data_size);
break;
/* Stall: treat as an error */
case LIBUSB_TRANSFER_STALL:
log_("transfer stalled (%d bytes dropped)\n", data_size);
break;
/* Overflow: treat as an error (should not happen because we set our buffer
size to a multiple of the maximum packet size) */
case LIBUSB_TRANSFER_OVERFLOW:
log_("transfer overflowed (%d bytes dropped)\n", data_size);
break;
/* No device: this is normal */
case LIBUSB_TRANSFER_NO_DEVICE:
log_("stop listening (calculator disconnected)\n");
break;
}
/* Resubmit transfer so we can get new data as soon as possible */
if(resubmit) {
libusb_submit_transfer(comm->tr_bulk_IN);
}
else {
libusb_free_transfer(comm->tr_bulk_IN);
comm->tr_bulk_IN = NULL;
}
}
bool fxlink_device_start_bulk_IN(struct fxlink_device *fdev)
{
if(!fdev->comm || !fdev->comm->claimed || fdev->comm->tr_bulk_IN)
return false;
fdev->comm->tr_bulk_IN = libusb_alloc_transfer(0);
if(!fdev->comm->tr_bulk_IN) {
elog("allocation of bulk IN transfer failed\n");
return false;
}
libusb_fill_bulk_transfer(fdev->comm->tr_bulk_IN,
fdev->dh, /* Device handle */
fdev->comm->ep_bulk_IN, /* Endpoint */
fdev->comm->buffer_IN, /* Buffer */
fdev->comm->buffer_IN_size, /* Buffer size */
bulk_IN_callback, fdev, /* Callback function and argument */
-1 /* Timeout */
);
int rc = libusb_submit_transfer(fdev->comm->tr_bulk_IN);
if(rc < 0) {
elog_libusb(rc, "bulk IN transfer failed to submit");
fdev->status = FXLINK_FDEV_STATUS_ERROR;
return false;
}
// hlog("calculators %s", fxlink_device_id(fdev));
// log_("submitted new IN transfer (no timeout)\n");
return true;
}
struct fxlink_message *fxlink_device_finish_bulk_IN(struct fxlink_device *fdev)
{
struct fxlink_comm *comm = fdev->comm;
if(!comm || !comm->ftransfer_IN)
return NULL;
if(!fxlink_transfer_complete(comm->ftransfer_IN))
return NULL;
struct fxlink_message *msg = fxlink_transfer_finish_IN(comm->ftransfer_IN);
if(!msg)
return NULL;
int version_major = (msg->version >> 8) & 0xff;
int version_minor = msg->version & 0xff;
hlog("calculators %s", fxlink_device_id(fdev));
log_("new message (v%d.%d): %.16s:%.16s, %s\n",
version_major, version_minor,
msg->application, msg->type, fxlink_size_string(msg->size));
comm->ftransfer_IN = NULL;
return msg;
}
/* Note: this function is run by the even handler and can't do any crazy libusb
stuff like sync I/O or getting descriptors. */
static void bulk_OUT_callback(struct libusb_transfer *transfer)
{
struct fxlink_device *fdev = transfer->user_data;
struct fxlink_comm *comm = fdev->comm;
int data_size = transfer->actual_length;
bool send_more = true;
struct fxlink_transfer *tr = comm->ftransfer_OUT;
if(transfer->status != LIBUSB_TRANSFER_COMPLETED)
hlog("calculators %s", fxlink_device_id(fdev));
switch(transfer->status) {
case LIBUSB_TRANSFER_COMPLETED:
if(tr->processed_size < 0) {
if(data_size != FXLINK_MESSAGE_HEADER_SIZE) {
elog("OUT for header only partially completed");
send_more = false;
}
tr->processed_size = 0;
}
else {
tr->processed_size += data_size;
if(fxlink_transfer_complete(tr))
send_more = false;
}
if(send_more) {
libusb_fill_bulk_transfer(comm->tr_bulk_OUT, fdev->dh,
comm->ep_bulk_OUT, /* Endpoint */
tr->msg.data + tr->processed_size, /* Buffer */
tr->msg.size - tr->processed_size, /* Buffer size */
bulk_OUT_callback, fdev, -1); /* Callback and timeout */
libusb_submit_transfer(comm->tr_bulk_OUT);
}
else {
libusb_free_transfer(comm->tr_bulk_OUT);
comm->tr_bulk_OUT = NULL;
fxlink_transfer_free(comm->ftransfer_OUT);
comm->ftransfer_OUT = NULL;
}
break;
/* Typical errors */
case LIBUSB_TRANSFER_ERROR: log_("transfer error\n"); break;
case LIBUSB_TRANSFER_TIMED_OUT: log_("transfer timed out\n"); break;
case LIBUSB_TRANSFER_CANCELLED: log_("transfer cancelled\n"); break;
case LIBUSB_TRANSFER_STALL: log_("transfer stalled\n"); break;
case LIBUSB_TRANSFER_OVERFLOW: log_("transfer overflowed\n"); break;
/* No device: this is normal */
case LIBUSB_TRANSFER_NO_DEVICE:
log_("stop listening (calculator disconnected)\n");
break;
}
}
bool fxlink_device_start_bulk_OUT(struct fxlink_device *fdev,
char const *app, char const *type, void const *data, int size,
bool own_data)
{
struct fxlink_comm *comm = fdev->comm;
if(!comm || !comm->claimed || comm->ftransfer_OUT)
return false;
comm->ftransfer_OUT =
fxlink_transfer_make_OUT(app, type, data, size, own_data);
if(!comm->ftransfer_OUT) {
elog("allocation of OUT transfer (protocol) failed\n");
return false;
}
comm->tr_bulk_OUT = libusb_alloc_transfer(0);
if(!comm->tr_bulk_OUT) {
elog("allocation of bulk OUT transfer (libusb) failed\n");
free(comm->ftransfer_OUT);
return false;
}
libusb_fill_bulk_transfer(comm->tr_bulk_OUT, fdev->dh,
comm->ep_bulk_OUT, /* Endpoint */
(void *)&comm->ftransfer_OUT->msg, /* Buffer */
FXLINK_MESSAGE_HEADER_SIZE, /* Buffer size */
bulk_OUT_callback, fdev, -1); /* Callback and timeout */
/* The fxlink protocol generally doesn't rely on sizes and instead expects
zero-length packets to mark the ends of transactions */
comm->tr_bulk_OUT->flags = LIBUSB_TRANSFER_ADD_ZERO_PACKET;
int rc = libusb_submit_transfer(comm->tr_bulk_OUT);
if(rc < 0) {
elog_libusb(rc, "bulk OUT transfer failed to submit");
fdev->status = FXLINK_FDEV_STATUS_ERROR;
return false;
}
return true;
}
//---
// Polled file descriptor tracking
//---
static void generate_poll_fds(struct fxlink_pollfds *tracker)
{
/* Get the set of libusb file descriptors to poll for news */
struct libusb_pollfd const **usb_fds = libusb_get_pollfds(tracker->ctx);
int usb_n = 0;
if(!usb_fds) {
elog("libusb_get_pollfds() returned NULL, devices will probably not "
"be detected!\n");
free(tracker->fds);
tracker->fds = NULL;
tracker->count = 0;
return;
}
hlog("libusb");
log_("fds to poll:");
for(int i = 0; usb_fds[i] != NULL; i++) {
log_(" %d(%s%s)",
usb_fds[i]->fd,
(usb_fds[i]->events & POLLIN) ? "i" : "",
(usb_fds[i]->events & POLLOUT) ? "o" : "");
}
if(!usb_fds[0])
log_(" (none)");
log_("\n");
while(usb_fds[usb_n])
usb_n++;
/* Allocate a bunch of `struct pollfd` and also monitor STDIN_FILENO */
tracker->count = usb_n;
tracker->fds = realloc(tracker->fds, usb_n * sizeof *tracker->fds);
for(int i = 0; i < usb_n; i++) {
tracker->fds[i].fd = usb_fds[i]->fd;
tracker->fds[i].events = usb_fds[i]->events;
}
}
static void handle_add_poll_fd(int fd, short events, void *data)
{
(void)fd;
(void)events;
generate_poll_fds(data);
}
static void handle_remove_poll_fd(int fd, void *data)
{
(void)fd;
generate_poll_fds(data);
}
void fxlink_pollfds_track(struct fxlink_pollfds *tracker, libusb_context *ctx)
{
memset(tracker, 0, sizeof *tracker);
tracker->ctx = ctx;
libusb_set_pollfd_notifiers(ctx, handle_add_poll_fd, handle_remove_poll_fd,
tracker);
generate_poll_fds(tracker);
}
void fxlink_pollfds_stop(struct fxlink_pollfds *tracker)
{
libusb_set_pollfd_notifiers(tracker->ctx, NULL, NULL, NULL);
free(tracker->fds);
memset(tracker, 0, sizeof *tracker);
}
//---
// Device tracking
//---
static void enumerate_devices(libusb_context *ctx,
struct fxlink_device_list *list)
{
libusb_device **libusb_list = NULL;
int new_count = libusb_get_device_list(ctx, &libusb_list);
if(new_count < 0) {
elog("libusb_get_device_list() failed with error %d\n", new_count);
return;
}
/* We now diff the previous array with the current one */
struct fxlink_device *new_fdevs = calloc(new_count, sizeof *new_fdevs);
if(!new_fdevs) {
elog("enumerate_devices(): %m\n");
return;
}
int k = 0;
/* First copy over any device that is still connected */
for(int i = 0; i < list->count; i++) {
struct fxlink_device *fdev = &list->devices[i];
assert(fdev->dp != NULL);
bool still_connected = false;
for(int j = 0; j < new_count; j++)
still_connected = still_connected || (libusb_list[j] == fdev->dp);
if(still_connected)
new_fdevs[k++] = list->devices[i];
else {
fxlink_device_cleanup(fdev);
hlog("devices %s", fxlink_device_id(fdev));
log_("disconnected\n");
}
}
/* Then add all the new ones */
for(int j = 0; j < new_count; j++) {
libusb_device *dp = libusb_list[j];
struct libusb_device_descriptor dc;
bool already_known = false;
for(int i = 0; i < list->count; i++)
already_known = already_known || (list->devices[i].dp == dp);
if(already_known)
continue;
libusb_ref_device(dp);
libusb_get_device_descriptor(dp, &dc);
new_fdevs[k].dp = dp;
new_fdevs[k].status = FXLINK_FDEV_STATUS_PENDING;
new_fdevs[k].busNumber = libusb_get_bus_number(dp);
new_fdevs[k].deviceAddress = libusb_get_device_address(dp);
new_fdevs[k].idVendor = dc.idVendor;
new_fdevs[k].idProduct = dc.idProduct;
new_fdevs[k].calc = NULL;
new_fdevs[k].comm = NULL;
hlog("devices %s", fxlink_device_id(&new_fdevs[k]));
if(is_casio_calculator(dc.idVendor, dc.idProduct)) {
log_("new CASIO calculator (%04x:%04x)\n",
dc.idVendor, dc.idProduct);
fxlink_device_analysis_1(&new_fdevs[k], false);
}
else {
log_("new non-CASIO-calculator device (%04x:%04x)\n",
dc.idVendor, dc.idProduct);
new_fdevs[k].status = FXLINK_FDEV_STATUS_IGNORED;
}
k++;
}
assert(k == new_count);
free(list->devices);
list->devices = new_fdevs;
list->count = new_count;
libusb_free_device_list(libusb_list, true);
}
static int handle_hotplug(libusb_context *ctx, libusb_device *device,
libusb_hotplug_event event, void *user_data)
{
/* Note that due to threading considerations in libusb, a device may be
notified for hotplugging twice, or may depart without ever having been
notified for arrival. */
(void)device;
if(event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED)
enumerate_devices(ctx, user_data);
else if(event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT)
enumerate_devices(ctx, user_data);
else
wlog("unhandled libusb hotplug event of type %d\n", event);
return 0;
}
bool fxlink_device_list_track(struct fxlink_device_list *list,
libusb_context *ctx)
{
memset(list, 0, sizeof *list);
if(!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
elog("libusb doesn't handle hotplug; devices may not be detected\n");
return false;
}
list->ctx = ctx;
libusb_hotplug_register_callback(ctx,
/* Both arriving and departing devices */
LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
/* Perform an initial enumeration right now */
LIBUSB_HOTPLUG_ENUMERATE,
LIBUSB_HOTPLUG_MATCH_ANY, /* vendorId */
LIBUSB_HOTPLUG_MATCH_ANY, /* productId */
LIBUSB_HOTPLUG_MATCH_ANY, /* deviceClass */
handle_hotplug, list, &list->hotplug_handle);
return true;
}
void fxlink_device_list_refresh(struct fxlink_device_list *list)
{
for(int i = 0; i < list->count; i++) {
struct fxlink_device *fdev = &list->devices[i];
/* Finish analysis */
if(fdev->calc && fdev->status == FXLINK_FDEV_STATUS_PENDING)
fxlink_device_analysis_2(fdev);
}
}
bool fxlink_device_list_interrupt(struct fxlink_device_list *list)
{
bool still_running = false;
for(int i = 0; i < list->count; i++) {
struct fxlink_device *fdev = &list->devices[i];
fxlink_device_interrupt_transfers(fdev);
still_running |= fdev->comm && fdev->comm->tr_bulk_IN;
still_running |= fdev->comm && fdev->comm->tr_bulk_OUT;
}
return still_running;
}
void fxlink_device_list_stop(struct fxlink_device_list *list)
{
if(!list->ctx)
return;
libusb_hotplug_deregister_callback(list->ctx, list->hotplug_handle);
/* Now free the device list proper */
for(int i = 0; i < list->count; i++)
fxlink_device_cleanup(&list->devices[i]);
free(list->devices);
memset(list, 0, sizeof *list);
}
//---
// Simplified device enumeration
//---
struct fxlink_device *fxlink_device_find(libusb_context *ctx,
struct fxlink_filter const *filter)
{
libusb_device **list = NULL;
int count = libusb_get_device_list(ctx, &list);
struct fxlink_device *fdev = NULL;
if(count < 0) {
elog("libusb_get_device_list() failed with error %d\n", count);
return NULL;
}
/* Look for any suitable calculator */
for(int i = 0; i < count; i++) {
libusb_device *dp = list[i];
struct libusb_device_descriptor dc;
libusb_get_device_descriptor(dp, &dc);
if(!is_casio_calculator(dc.idVendor, dc.idProduct))
continue;
/* Since we found a calculator, make the fdev and check further */
fdev = calloc(1, sizeof *fdev);
if(!fdev) {
elog("failed to allocate device structure: %m\n");
continue;
}
libusb_ref_device(dp);
fdev->dp = dp;
fdev->status = FXLINK_FDEV_STATUS_PENDING;
fdev->busNumber = libusb_get_bus_number(dp);
fdev->deviceAddress = libusb_get_device_address(dp);
fdev->idVendor = dc.idVendor;
fdev->idProduct = dc.idProduct;
fdev->calc = NULL;
fdev->comm = NULL;
fxlink_device_analysis_1(fdev, true);
fxlink_device_analysis_2(fdev);
if(fdev->status == FXLINK_FDEV_STATUS_IDLE) {
/* Check the filter */
struct fxlink_filter properties;
fxlink_device_get_properties(fdev, &properties);
/* Success: return that device (no interfaces claimed) */
if(fxlink_filter_match(&properties, filter))
break;
}
/* Failure: free it and try the next one */
fxlink_device_cleanup(fdev);
free(fdev);
fdev = NULL;
}
libusb_free_device_list(list, true);
return fdev;
}
struct fxlink_device *fxlink_device_find_wait(libusb_context *ctx,
struct fxlink_filter const *filter, delay_t *delay)
{
while(true) {
struct fxlink_device *fdev = fxlink_device_find(ctx, filter);
if(fdev)
return fdev;
if(delay_cycle(delay))
return NULL;
}
}

180
libfxlink/filter.c Normal file
View File

@ -0,0 +1,180 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/filter.h>
#include <fxlink/logging.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
//---
// Filter parser
//---
/* Identify property separating characters to be skipped */
static bool issep(int c)
{
return (c == ' ' || c == '\t' || c == '\n' || c == ',');
}
/* Identify valid word characters for the filter */
static bool isword(int c)
{
return c && !strchr(" \t\n,=", c);
}
/* Copy the next word in the string, assumes word is non-empty */
static char *read_word(char const **input)
{
char const *str = *input;
while(**input && isword(**input)) (*input)++;
return strndup(str, *input - str);
}
/* Reads a property from the input source. Advances *input and sets *name and
*value to newly-allocated copies of the name and (optional) value of the
property (T_PROP). Both should be free()'d after use. At the end of the
input, returns false and sets *name = *value = NULL. */
static bool read_property(char const **input, char **name, char **value)
{
*name = *value = NULL;
while(issep(**input))
(*input)++;
if(!**input)
return false;
if(!isword(**input)) {
elog("expected property name in filter, found '%c'; stopping\n",
**input);
return false;
}
*name = read_word(input);
if(**input == '=') {
(*input)++;
*value = read_word(input);
}
return true;
}
struct fxlink_filter *fxlink_filter_parse(char const *input)
{
char *name=NULL, *value=NULL;
struct fxlink_filter *filter = calloc(1, sizeof *filter);
if(!filter)
return NULL;
while(read_property(&input, &name, &value)) {
/* Add a new property to the current option */
if(!strcmp(name, "p7") && !value)
filter->p7 = true;
else if(!strcmp(name, "mass_storage") && !value)
filter->mass_storage = true;
else if(!strcmp(name, "series_cg") && !value)
filter->series_cg = true;
else if(!strcmp(name, "series_g3") && !value)
filter->series_g3 = true;
else if(!strcmp(name, "intf_fxlink") && !value)
filter->intf_fxlink = true;
else if(!strcmp(name, "intf_cesg502") && !value)
filter->intf_cesg502 = true;
else if(!strcmp(name, "serial") && value)
filter->serial = strdup(value);
else if(!strcmp(name, "serial_number") && value) // Old name
filter->serial = strdup(value);
else wlog("ignoring invalid filter property: '%s' (%s value)\n",
name, value ? "with" : "without");
free(name);
free(value);
}
return filter;
}
//---
// Filtering API
//---
void fxlink_filter_clean_libusb(struct fxlink_filter *filter)
{
if(!filter)
return;
/* Suppress series_cg and series_g3, which are based off the SCSI metadata
provided only to UDisks2 */
if(filter->series_cg) {
wlog("ignoring series_cg in libusb filter (cannot be detected)\n");
filter->series_cg = false;
}
if(filter->series_g3) {
wlog("ignoring series_g3 in libusb filter (cannot be detected)\n");
filter->series_g3 = false;
}
}
void fxlink_filter_clean_udisks2(struct fxlink_filter *filter)
{
/* Every property can be used */
(void)filter;
}
bool fxlink_filter_match(
struct fxlink_filter const *props,
struct fxlink_filter const *filter)
{
/* No filter is a pass-through */
if(!filter)
return true;
if(filter->p7 && !props->p7)
return false;
if(filter->mass_storage && !props->mass_storage)
return false;
if(filter->intf_fxlink && !props->intf_fxlink)
return false;
if(filter->intf_cesg502 && !props->intf_cesg502)
return false;
if(filter->series_cg && !props->series_cg)
return false;
if(filter->series_g3 && !props->series_g3)
return false;
if(filter->serial &&
(!props->serial || strcmp(filter->serial, props->serial)))
return false;
return true;
}
void fxlink_filter_print(FILE *fp, struct fxlink_filter const *filter)
{
#define output(...) { \
if(sep) fprintf(fp, ", "); \
fprintf(fp, __VA_ARGS__); \
sep = true; \
}
bool sep = false;
if(filter->p7)
output("p7");
if(filter->mass_storage)
output("mass_storage");
if(filter->intf_fxlink)
output("intf_fxlink");
if(filter->intf_cesg502)
output("intf_cesg502");
if(filter->series_cg)
output("series_cg");
if(filter->series_g3)
output("series_g3");
if(filter->serial)
output("serial=%s", filter->serial);
}
void fxlink_filter_free(struct fxlink_filter *filter)
{
if(filter)
free(filter->serial);
free(filter);
}

View File

@ -0,0 +1,17 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.config: Compile-time configuration
#pragma once
/* Disables UDisks2 interfaces for systems that don't use it. */
#cmakedefine FXLINK_DISABLE_UDISKS2
/* Disable SDL2 interfaces. */
#cmakedefine FXLINK_DISABLE_SDL2
/* fxSDK version */
#define FXLINK_VERSION "@CMAKE_PROJECT_VERSION@"

View File

@ -0,0 +1,111 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.defs: Utility definitions and functions
#pragma once
#include <fxlink/config.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdarg.h>
#include <poll.h>
static inline int min(int x, int y)
{
return (x < y) ? x : y;
}
static inline int max(int x, int y)
{
return (x > y) ? x : y;
}
static inline int clamp(int x, int min, int max)
{
return (x < min) ? min : (x > max) ? max : x;
}
//---
// Text formatting
//
// We stick to ANSI style and provide 8 colors (foreground and background) with
// bold/italic/dim attributes. The standard ANSI escape translation is provided
// here while the ncurses versions is implemented by the TUI.
//---
enum {
/* Main colors */
FMT_BLACK = 0x01,
FMT_RED = 0x02,
FMT_GREEN = 0x03,
FMT_YELLOW = 0x04,
FMT_BLUE = 0x05,
FMT_MAGENTA = 0x06,
FMT_CYAN = 0x07,
FMT_WHITE = 0x08,
/* Background colors */
FMT_BGBLACK = 0x10,
FMT_BGRED = 0x20,
FMT_BGGREEN = 0x30,
FMT_BGYELLOW = 0x40,
FMT_BGBLUE = 0x50,
FMT_BGMAGENTA = 0x60,
FMT_BGCYAN = 0x70,
FMT_BGWHITE = 0x80,
/* Modifiers */
FMT_BOLD = 0x100,
FMT_DIM = 0x200,
FMT_ITALIC = 0x400,
};
#define fmt_FG(fmt) ((fmt) & 0xf)
#define fmt_BG(fmt) (((fmt) >> 4) & 0xf)
#define fmt_BOLD(fmt) (((fmt) & FMT_BOLD) != 0)
#define fmt_DIM(fmt) (((fmt) & FMT_DIM) != 0)
#define fmt_ITALIC(fmt) (((fmt) & FMT_ITALIC) != 0)
/* Returns the escape sequence that switches to the desired format. The
returned pointer is to a static buffer overwritten on the next call. */
char const *fmt_to_ANSI(int format);
//---
// Misc.
//---
/* Generates a unique name for a file to be stored in `path`, with `name` as a
component and the provided `suffix`. The generated path looks like
<path>/fxlink-<name>-2021.05.09-19h23-1<suffix>
with the `-1` suffix being chosen as to avoid overriding existing files.
Returns a newly-allocated string to be free()'d after use. */
char *fxlink_gen_file_name(char const *path, char const *name,
char const *suffix);
/* Modified poll with a variable number of arrays. */
int fxlink_multipoll(int timeout, struct pollfd *fds1, int count1, ...);
/* Write out the given size (in bytes) in a human-readable form. Returns a
pointer to a statically-allocated string. */
char const *fxlink_size_string(int bytes);
//---
// Delay
//---
/* An expandable allocated time used to wait for devices */
typedef int delay_t;
/* Builds an empty delay. */
delay_t delay_none(void);
/* Builds a delay that lasts the specified number of seconds. */
delay_t delay_seconds(int seconds);
/* Builds an infinite delay. */
delay_t delay_infinite(void);
/* Returns `true` if the delay has expired; otherwise, waits for a short while
(250 ms), decreases the supplied delay pointer, and returns `false`. Never
returns `true` after waiting (even if the delay just expired) so the caller
can attempt their task one last time before giving up on a timeout. */
bool delay_cycle(delay_t *delay);

View File

@ -0,0 +1,341 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.devices: Device management and state tracking
//
// This module provides definitions for fxlink's view of devices and their
// states. There are three "tiers" of devices depending on how much fxlink
// interacts with them:
//
// - Every connected USB device is inspected and given a `struct fxlink_device`
// which track metadata like vendor and product IDs. A device at this level
// isn't very useful so fxlink won't do anything with them.
//
// - Devices with the vendor and product IDs of CASIO calculators are opened;
// their configuration descriptor is analyzed to determine what interfaces
// they offer, and some communication is performed to retrieve information
// like the serial number of the calculator. This information is stored in a
// `struct fxlink_calc` structure linked in the `calc` field of the main
// device structure. Only after this stage can device filters be applied.
//
// The key operation to do next is claiming an interface so communication can
// start. fxlink devices can be used for manual communication using the device
// pointer `fdev->dp` and the device handle `fdev->dh`. This is useful for
// communication with arbitrary interfaces like the CESG502 interface used by
// CASIO's Comm syscalls. However, when gint is used and the fxlink-specific
// interface (protocol) is opened, this header provides more automatic tools.
//
// - When calculators expose an fxlink interface, that interface can be claimed
// with fxlink_device_claim_fxlink(). This module finds relevant endpoints,
// allocates transfer buffers/utilities, and exposes a higher-level message-
// based interface. Associated data is stored in a `struct fxlink_comm`
// structure linked in the `comm` field of the main device structure.
//
// In addition to handling single devices, this header provides two types of
// tools to find devices:
//
// 1. Tracking tools, which are used to watch connected devices in real-time.
// These are asynchronous/non-blocking, keep track of what devices we've
// seen before and overall are the most flexible. These are used by the TUI.
//
// 2. "Simplified enumeration" functions which simply search for a single
// device matching a filter. These are relevant when handling multiple
// connections are not a concern, and used eg. by legacy interactive mode.
//---
#pragma once
#include <fxlink/protocol.h>
#include <fxlink/filter.h>
#include <libusb.h>
#include <poll.h>
/* Device information tracked for every USB device. */
struct fxlink_device {
/* libusb device pointer. This is libusb_ref_device()'d for the entire
lifetime of the structure (either managed by the device list or obtained
by simplified enumeration and freed by fxlink_device_cleanup()). */
libusb_device *dp;
/* libusb device handle (NULL when the device is not open) */
libusb_device_handle *dh;
/* Device status (an FDEV_STATUS_* enumerated value) */
uint8_t status;
/* Standard USB information */
short busNumber;
short deviceAddress;
uint16_t idVendor;
uint16_t idProduct;
/* Calculator data. This field is present whenever a device is a CASIO
calculator. Information can be accessed freely. */
struct fxlink_calc *calc;
/* Communication data for the fxlink interface. This field is present when
calculators have an fxlink interface. It can be used when not NULL but
note that the device status might still be IGNORED (if filtered out) or
ERROR (if communication errors occurs) in which case communication might
not be allowed. */
struct fxlink_comm *comm;
};
enum {
/* The device has just been connected to the host, and we have yet to
inspect it or open it. We don't know yet whether there are any supported
interfaces. */
FXLINK_FDEV_STATUS_PENDING,
/* The device is ignored by fxlink. This is either because it's not a CASIO
calculator or because it was excluded by a device filter. The device
might have been opened (to find out the serial number) but it is now
closed and no interfaces are claimed. */
FXLINK_FDEV_STATUS_IGNORED,
/* The device could not be used due to an error: access denied, interface
already claimed, transient errors, etc. */
FXLINK_FDEV_STATUS_ERROR,
/* The device is a calculator and it's not ignored, but no interfaces have
been claimed yet. This header only sets the status to CONNECTED when
using an fxlink interface, but that status is mostly cosmetic and it's
entirely possible to manually claim and use an interface (eg. CESG502)
while the device is in the IDLE state. The `comm` field may or may not
be present depending on whether there is an fxlink interface. */
FXLINK_FDEV_STATUS_IDLE,
/* The device is a calculator, is not ignored, has an fxlink interface and
that interface was claimed. The `comm` field is non-NULL and can be
checked to be determine whether communication is going on. */
FXLINK_FDEV_STATUS_CONNECTED,
};
/* Return a string representation of the device's bus number and device
address, which make up a human-readable a locally unique device ID. The
address of a static string is returned. */
char const *fxlink_device_id(struct fxlink_device const *fdev);
/* Return a string representation of the current status. */
char const *fxlink_device_status_string(struct fxlink_device const *fdev);
/* Analyze a CASIO calculator device to reach "calculator" tier.
These functions can be called on CASIO calculators (idVendor 07cf, idProduct
6101 or 6102). They determine available interfaces, open the device, and
initialize the `calc` field with information about the device's interfaces.
If an fxlink interface is found, they also initialize the `comm` field with
static information (but doesn't claim the interface).
After analysis, either the device status is ERROR, or it is IDLE and
interfaces are ready to be claimed.
fxlink_device_analysis_1() is generally called in an event handling context,
so its capabilities are limited. fxlink_device_analysis_2() is called after
event handling finishes to finalize analysis. Both calls are managed by the
device list or simplified enumeration functions so direct calls are normally
not needed. `quiet` suppresses logs. */
void fxlink_device_analysis_1(struct fxlink_device *fdev, bool quiet);
void fxlink_device_analysis_2(struct fxlink_device *fdev);
/* Determine the filter properties of a calculator. This can be used for any
device at the calculator tier. This function only reads the device structure
and does not communicate. */
void fxlink_device_get_properties(struct fxlink_device const *fdev,
struct fxlink_filter *properties);
/* Device information tracked for CASIO calculators. */
struct fxlink_calc {
/* System running on the calculator (a CALC_SYSTEM_* enumerated value) */
uint8_t system;
/* Number of interfaces */
uint8_t interface_count;
/* fxlink interface number (-1 if not found) */
int8_t fxlink_inum;
/* List of interface classes (interface_count elements). Each element is a
two-byte value, MSB being class and LSB subclass. */
uint16_t *classes;
/* Serial number (obtained with string descriptor SETUP request) */
char *serial;
};
enum {
/* The device is running an unidentified system. */
FXLINK_CALC_SYSTEM_UNKNOWN,
/* The device is using LINK app's SCSI (Mass Storage) interface. */
FXLINK_CALC_SYSTEM_LINKSCSI,
/* The device is using the OS' native bulk interface with Comm syscalls. */
FXLINK_CALC_SYSTEM_CESG502,
/* The device is using gint's USB driver. */
FXLINK_CALC_SYSTEM_GINT,
};
/* Return a string representation of the calc determined system. */
char const *fxlink_device_system_string(struct fxlink_device const *fdev);
/* Device state tracked for communication targets. */
struct fxlink_comm {
/* Whether the fxlink interface could be claimed */
bool claimed;
/* Endpoints of the fxlink interface */
uint8_t ep_bulk_IN;
uint8_t ep_bulk_OUT;
/* Current IN transfer */
struct libusb_transfer *tr_bulk_IN;
/* IN transfer buffer and its size */
uint8_t *buffer_IN;
int buffer_IN_size;
/* fxlink message construction for the IN transfer */
struct fxlink_transfer *ftransfer_IN;
/* Cancellation flag */
bool cancelled_IN;
/* Completed transfer objects (to be checked by user every frame) */
struct fxlink_message *message_IN;
/* Current OUT transfer */
struct libusb_transfer *tr_bulk_OUT;
/* fxlink message construction for the OUT transfer */
struct fxlink_transfer *ftransfer_OUT;
/* Cancellation flag */
bool cancelled_OUT;
};
/* Check whether the device is ready to have interfaces claimed. This function
only checks that the device is a calculator and could be opened; it doesn't
guarantee that claiming the interfaces will succeed. This function returns
true even after an interface has been claimed since multiple interfaces can
be claimed at the same time. */
bool fxlink_device_ready_to_connect(struct fxlink_device const *fdev);
/* Check whether the device exposes an fxlink interface. */
bool fxlink_device_has_fxlink_interface(struct fxlink_device const *fdev);
/* Claim the fxlink interface (for a device that has one). Returns false and
sets the status to ERROR on failure. */
bool fxlink_device_claim_fxlink(struct fxlink_device *fdev);
/* Start an IN transfer on the device if none is currently running, so that the
device structure is always ready to receive data from the calculator. */
bool fxlink_device_start_bulk_IN(struct fxlink_device *fdev);
/* Finish an IN transfer and obtain the completed message. This function should
be checked every frame as it will return a non-NULL pointer as soon as the
message is completed and the device will only start a new bulk IN transfer
until the message is moved out by this function. */
struct fxlink_message *fxlink_device_finish_bulk_IN(
struct fxlink_device *fdev);
/* Start an OUT transfer on the device. If `own_data` is set, the transfer will
free(data) when it completes. */
bool fxlink_device_start_bulk_OUT(struct fxlink_device *fdev,
char const *app, char const *type, void const *data, int size,
bool own_data);
/* Interrupt any active transfers on the device. */
void fxlink_device_interrupt_transfers(struct fxlink_device *fdev);
/* Clean everything that needs to be cleaned for the device to be destroyed.
This closes the libusb device handle, frees dynamically allocated memory,
etc. This function finishes immediately so no transfers must be running
when it is call; use fxlink_device_interrupt_transfers() first. */
void fxlink_device_cleanup(struct fxlink_device *fdev);
//---
// Polled file descriptor tracking
//
// fxlink has some asynchronous main loops where events from libusb are mixed
// with user input. We poll libusb file descriptors, along with other event
// sources such as stdin, to listen to all event sources at the same time. This
// is possible on Linux because libusb exposes its active file descriptors. The
// following utility is a wrapper to track them, built around libusb's pollfd
// notification API.
//---
/* Tracker for libusb file descriptors to be polled in a main loop. */
struct fxlink_pollfds {
/* libusb context to be tracked (must remain constant) */
libusb_context *ctx;
/* Array of file descriptors currently being polled */
struct pollfd *fds;
/* Number of elements in `fds` */
int count;
};
/* Start tracking file descriptors for a given context. This sets up notifiers
so tracker->fds and tracker->count will be updated automatically every time
libusb events are processed. */
void fxlink_pollfds_track(struct fxlink_pollfds *tracker, libusb_context *ctx);
/* Stop tracking file descriptors and free tracker's resources. This must be
called before the tracker structure gets destroyed. */
void fxlink_pollfds_stop(struct fxlink_pollfds *tracker);
//---
// Device tracking
//
// For real-time interactions and device detection it is useful to keep an eye
// on connected devices, process incoming devices as well as properly free
// resources for disconnected devices. libusb has a hotplug notification
// mechanism, which we wrap here into a dynamic device list.
//
// The list stays constantly updated by means of the hotplug notification and
// initializes an fxlink_device structure for every device. It does all the
// "safe" work, ie. everything that doesn't involve actual communication. For
// instance, it fills in the fields of both the fxlink_device and fxlink_calc
// structures, but it doesn't query serial numbers.
//---
/* A dynamically updated list of all connected devices. */
struct fxlink_device_list {
/* libusb context that we track the devices for */
libusb_context *ctx;
/* Callback handle */
libusb_hotplug_callback_handle hotplug_handle;
/* Array of connected devices */
struct fxlink_device *devices;
/* Number of elements in `devices` */
int count;
};
/* Start tracking connected devices for the given context. The list will update
whenever libusb events are processed and new devices will be available (ie.
in the IDLE state) after calling fxlink_device_list_refresh(). */
bool fxlink_device_list_track(struct fxlink_device_list *list,
libusb_context *ctx);
/* Refresh devices. This should be called after handling libusb events; it
finishes analysis for new devices. After calling this function, devices are
ready to be filtered and interfaces claimed. */
void fxlink_device_list_refresh(struct fxlink_device_list *list);
/* Interrupt transfers on all devices in the list. This is non-blocking;
returns true if some transfers are still running. Call this in a loop until
it returns false, while handling libusb events at each iteration. */
bool fxlink_device_list_interrupt(struct fxlink_device_list *list);
/* Stop tracking connected devices, free the device list and disable the
hotplug notification. Make sure all devices are closed and communications
stopped before calling this function. */
void fxlink_device_list_stop(struct fxlink_device_list *list);
//---
// Simplified device enumeration
//
// These functions provides a simpler way to find devices. Given a filter they
// will try to find a device that matches, and build a device structure when
// they find one. These are only suitable to open a single device since they
// don't track which fxlink devices have been created.
//---
/* Finds a libusb device that matches the provided filter.
If one is found, returns a newly-allocated fxlink device. The device will be
opened and have its device handler in the `comm->dh` field. Additionally, if
there is an fxlink interface, that interface will be claimed and
communication tools from this header will be initialized.
If no matching device is found, NULL is returned; this is a non-blocking
function. Note that this function will interact with devices to determine
their property (ie. serial number) even when it returns NULL. */
struct fxlink_device *fxlink_device_find(libusb_context *ctx,
struct fxlink_filter const *filter);
/* Same as fxlink_device_find(), but waits for the specified delay. */
// TODO: fxlink_device_find_wait(): Use a struct timespec, get rid of delay_t?
struct fxlink_device *fxlink_device_find_wait(libusb_context *ctx,
struct fxlink_filter const *filter, delay_t *delay);

View File

@ -0,0 +1,84 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.filter: Backend-agnostic device filters
//
// Most automatic functions in fxlink only connect with a single calculator.
// When several calculators are connected it is useful to narrow down the
// targeted machine programmatically, which is done with a device filter.
//
// A device filter is a simple conjunction of properties detected with the
// generic metadata of the device, as provided by the backend's API. Depending
// on the backend not all properties can be detected; for instance, fx-CG and
// G-III calculators have the same idVendor/idProduct pair and cannot be
// distinguished on the device descriptor alone, however they report different
// identities in SCSI queries and can be differentiated with UDisks2.
//
// The same type `struct fxlink_properties` is used both to represents devices'
// properties and filters. Every property has a "total information order" where
// values can be compared for how specific they are. A device matches a filter
// if its detected properties are more specific than the filter's requirements.
// So far the order is:
// - For booleans: true more specific than false
// - For serial number: any string more specific than NULL
//---
#pragma once
#include <fxlink/defs.h>
#include <stdio.h>
/* Detected devices' properties; also used for filters. */
struct fxlink_filter {
/* The calculator uses Protocol 7/CESG502. Detected with idProduct 0x6101.
These calcs don't support SCSI, so this is always false in UDisks2. */
bool p7;
/* The calculator supports Mass Storage/SCSI. Detected with idProduct
0x6102. Always true in UDisks2. */
bool mass_storage;
/* The calculator has an fxlink interface. Always false in UDisks2. */
bool intf_fxlink;
/* The calculator has a CESG502 interface. Always false in UDisks2. */
bool intf_cesg502;
/* The calculator is from the fx-CG/G-III series. Detected with the SCSI
drive `model` metadata. Only available in UDisks2. */
bool series_cg;
bool series_g3;
/* Serial number. Requires write access to obtain in libusb (with a STRING
descriptor request) because it's not cached. free() after use. */
char *serial;
};
/* Return values for back-end specific matching functions. */
enum {
/* A unique calculator matching the filter was found */
FXLINK_FILTER_UNIQUE,
/* No calculator matching the filter was found */
FXLINK_FILTER_NONE,
/* Multiple calculators matching the filter were found */
FXLINK_FILTER_MULTIPLE,
/* An error occurred while trying to enumerate devices */
FXLINK_FILTER_ERROR,
};
/* Parse a filter string into a structure */
struct fxlink_filter *fxlink_filter_parse(char const *specification);
/* Disable filter properties not supported by each backend (with a warning
emitted for each such ignored property) */
void fxlink_filter_clean_libusb(struct fxlink_filter *filter);
void fxlink_filter_clean_udisks2(struct fxlink_filter *filter);
/* Determines whether a set of concrete properties matches a filter. Returns
whether all the fields of `props` are more specific than their counterparts
in `filter`. */
bool fxlink_filter_match(
struct fxlink_filter const *props,
struct fxlink_filter const *filter);
/* Print a set of properties to a stream. Outputs a single line. */
void fxlink_filter_print(FILE *fp, struct fxlink_filter const *props);
/* Free a filter structure and its fields */
void fxlink_filter_free(struct fxlink_filter *filter);

View File

@ -0,0 +1,49 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.logging: Logging interface and log redirection
//
// This header provides basic logging utilities. You've seen these a thousand
// times over. The only features are supporting colors from <fxlink/defs.h>,
// redirecting libusb logs to this interface automatically, and redirecting
// this interface to either the terminal or the ncurses TUI.
//---
#pragma once
#include <fxlink/defs.h>
#include <libusb.h>
#include <stdio.h>
/* Type of the log handling function out of this interface */
typedef void fxlink_log_handler_t(int display_fmt, char const *str);
/* Default handler; prints to stderr with ANSI escapes. */
fxlink_log_handler_t fxlink_log_stderr;
/* Set the logging function. Default is fxlink_log_stderr. */
void fxlink_log_set_handler(fxlink_log_handler_t *handler);
/* Redirect libusb log to this module's log hander. */
void fxlink_log_grab_libusb_logs(void);
/* Log a message. All of the logging functions accept a printf()-style format
with corresponding arguments, and return 1. */
int log_(char const *fmt, ...);
/* Log a message with a certain display format (color/bold/italic). */
int flog(int display_fmt, char const *fmt, ...);
/* Like log(), but adds a red "error:" in front. */
int elog(char const *fmt, ...);
/* Like log(), but adds a yellow "warning:" in front. */
int wlog(char const *fmt, ...);
/* Like log(), but for headers (with current time in gray in square brackets,
and provided text in yellow followed by ":"). */
int hlog(char const *fmt, ...);
/* Warning that includes a libusb error message. */
#define wlog_libusb(RC, FMT, ...) \
wlog(FMT ": %s\n", ##__VA_ARGS__, libusb_strerror(RC))
/* Error that includes a libusb error message. */
#define elog_libusb(RC, FMT, ...) \
elog(FMT ": %s\n", ##__VA_ARGS__, libusb_strerror(RC))

View File

@ -0,0 +1,150 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
// fxlink.protocol: Basic fxlink-specific communications procotol
//
// This header implements fxlink's run-of-the-mill communications protocol.
// Messages are typed with an application/type pair and their length must be
// announced in the header. In most modes when a message with an application
// field other than "fxlink" is specified, an external executable is invoked,
// which provides a basic form of automation.
//
// Message headers are transferred in little-endian format but some message
// types are defined to have big-endian contents to avoid excessive amounts of
// work on the calculator side.
//---
/* TODO: fxlink protocol: avoid having to specify message length in header */
#pragma once
#include <fxlink/defs.h>
/* Message. The header consists of every field but the `data` pointer, and it
must arrive entirely within the first transaction for a message to be
recognized. */
struct fxlink_message {
/* Protocol version, in format 0x0000MMmm */
uint32_t version;
/* Total size of message, in bytes (excluding this header) */
uint32_t size;
/* Size of individual transfers (usually 2048 bytes) */
/* TODO: fxlink protocol: get rid of transfer size field (blank it out) */
uint32_t transfer_size;
/* Application name (NUL-padded but might not be NUL-terminated) */
char application[16];
/* Message type (NUL-padded but might not be NUL-terminated) */
char type[16];
/** End of actual header in protocol **/
/* Padding for alignment */
int _padding;
/* Pointer to message data, with `size` bytes */
void *data;
};
#define FXLINK_MESSAGE_HEADER_SIZE (offsetof(struct fxlink_message, _padding))
/* Subheader for the built-in image message type. */
struct fxlink_message_image_header {
/* Image width and height, in pixels */
uint32_t width;
uint32_t height;
/* Pixel format, one of the FXLINK_MESSAGE_IMAGE_* values below */
int pixel_format;
};
enum {
/* Image is an array of big-endian uint16_t values with RGB565 format */
FXLINK_MESSAGE_IMAGE_RGB565,
/* Image is an array of bits in black-and-white format */
FXLINK_MESSAGE_IMAGE_MONO,
/* Image is two consecutive mono arrays, one for light, one for dark */
FXLINK_MESSAGE_IMAGE_GRAY,
};
/* Format for raw decoded images to be used with other APIs. */
struct fxlink_message_image_raw {
/* Width and height in pixels */
int width;
int height;
/* Allocated array of `height` pointers, each pointing to `3*width` bytes
of memory containing the RGB data of pixels from left to right. */
uint8_t **data;
};
/* Check whether a message has a certain application and type. If type is NULL,
checks the application only (any type is accepted). */
bool fxlink_message_is_apptype(struct fxlink_message const *message,
char const *application, char const *type);
/* Check if a message is one of common built-in fxlink messages. */
#define fxlink_message_is_fxlink_text(MESSAGE) \
fxlink_message_is_apptype(MESSAGE, "fxlink", "text")
#define fxlink_message_is_fxlink_image(MESSAGE) \
fxlink_message_is_apptype(MESSAGE, "fxlink", "image")
#define fxlink_message_is_fxlink_video(MESSAGE) \
fxlink_message_is_apptype(MESSAGE, "fxlink", "video")
/* Decode an image message into a raw image structure. */
struct fxlink_message_image_raw *fxlink_message_image_decode(
struct fxlink_message const *msg);
/* Free a raw image structure. */
void fxlink_message_image_raw_free(struct fxlink_message_image_raw *raw);
/* Free memory associated with a message. If free_data is true, also frees the
internal data buffer. You should set the flag for messages you received. */
void fxlink_message_free(struct fxlink_message *message, bool free_data);
//---
// Tools for crafting and receiving messages
//---
/* Data for an inbound or outbound transfer in progress. This structure can be
created as soon as a header has been received or filled. */
struct fxlink_transfer {
/* Message header and data buffer */
struct fxlink_message msg;
/* Transfer direction (FXLINK_TRANSFER_{IN,OUT}) */
uint8_t direction;
/* Size of data sent or received so far */
int processed_size;
/* Whether we own msg.data and should free(3) it */
bool own_data;
};
enum {
/* Transfer is inbound (calculator -> fxlink) */
FXLINK_TRANSFER_IN,
/* Transfer is outbound (fxlink -> calculator) */
FXLINK_TRANSFER_OUT,
};
/* Make an inbound transfer structure starting with the provided data (the data
obtained in the first IN transaction to be decoded). This function grabs the
header from the data, allocates a buffer of a suitable size and starts
saving the message's contents. Returns NULL on error (invalid or incomplete
header, out of memory, etc). */
struct fxlink_transfer *fxlink_transfer_make_IN(void *data, int size);
/* If the provided IN transfer is finished, extract the message and free the
transfer (do not fxlink_transfer_free(tr) later!). Returns NULL if the
transfer isn't finished yet. */
struct fxlink_message *fxlink_transfer_finish_IN(struct fxlink_transfer *tr);
/* Append data to a previously-initialized inbound transfer. */
void fxlink_transfer_receive(struct fxlink_transfer *tr, void *data, int size);
/* Make an outbound transfer structure. */
struct fxlink_transfer *fxlink_transfer_make_OUT(char const *application,
char const *type, void const *data, int size, bool own_data);
/* Check whether a transfer is complete. */
bool fxlink_transfer_complete(struct fxlink_transfer const *tr);
/* Free a transfer structure and associated data. */
void fxlink_transfer_free(struct fxlink_transfer *tr);

93
libfxlink/logging.c Normal file
View File

@ -0,0 +1,93 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/logging.h>
#include <stdarg.h>
#include <stdlib.h>
#include <time.h>
static fxlink_log_handler_t *log_handler = fxlink_log_stderr;
void fxlink_log_stderr(int display_fmt, char const *str)
{
fputs(fmt_to_ANSI(display_fmt), stderr);
fputs(str, stderr);
fputs(fmt_to_ANSI(0), stderr);
}
void fxlink_log_set_handler(fxlink_log_handler_t *handler)
{
log_handler = handler ? handler : fxlink_log_stderr;
}
static void handle_libusb_log(libusb_context *ctx, enum libusb_log_level level,
char const *str)
{
(void)ctx;
(void)level;
log_handler(0, str);
}
void fxlink_log_grab_libusb_logs(void)
{
libusb_set_log_cb(NULL, handle_libusb_log, LIBUSB_LOG_CB_GLOBAL);
}
int flogv(int display_fmt, char const *fmt, va_list args)
{
char *str = NULL;
vasprintf(&str, fmt, args);
if(str) {
log_handler(display_fmt, str);
free(str);
}
return 1;
}
#define LOG_VA_ARGS(FMT, STMTS) do { \
va_list _args; \
va_start(_args, FMT); \
STMTS; \
va_end(_args); \
} while(0)
int log_(char const *fmt, ...)
{
LOG_VA_ARGS(fmt, flogv(0, fmt, _args));
return 1;
}
int flog(int display_fmt, char const *fmt, ...)
{
LOG_VA_ARGS(fmt, flogv(display_fmt, fmt, _args));
return 1;
}
int elog(char const *fmt, ...)
{
flog(FMT_RED, "error: ");
LOG_VA_ARGS(fmt, flogv(0, fmt, _args));
return 1;
}
int wlog(char const *fmt, ...)
{
flog(FMT_YELLOW, "warning: ");
LOG_VA_ARGS(fmt, flogv(0, fmt, _args));
return 1;
}
int hlog(char const *fmt, ...)
{
time_t t = time(NULL);
struct tm tm;
localtime_r(&t, &tm);
flog(FMT_WHITE | FMT_DIM, "[%02d:%02d] ", tm.tm_hour, tm.tm_min);
LOG_VA_ARGS(fmt, flogv(FMT_YELLOW | FMT_DIM, fmt, _args));
flog(FMT_YELLOW | FMT_DIM, ": ");
return 1;
}

310
libfxlink/protocol.c Normal file
View File

@ -0,0 +1,310 @@
//---------------------------------------------------------------------------//
// ==>/[_]\ fxlink: A community communication tool for CASIO calculators. //
// |:::| Made by Lephe' as part of the fxSDK. //
// \___/ License: MIT <https://opensource.org/licenses/MIT> //
//---------------------------------------------------------------------------//
#include <fxlink/protocol.h>
#include <fxlink/logging.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
bool fxlink_message_is_apptype(struct fxlink_message const *msg,
char const *application, char const *type)
{
return memchr(application, '\0', 17) != NULL
&& memchr(type, '\0', 17) != NULL
&& !strncmp(msg->application, application, 16)
&& !strncmp(msg->type, type, 16);
}
void fxlink_message_free(struct fxlink_message *message, bool free_data)
{
if(free_data)
free(message->data);
free(message);
}
//---
// Image decoding
//---
static int img_bytes_per_row(int format, int width)
{
if(format == FXLINK_MESSAGE_IMAGE_RGB565)
return 2 * width;
if(format == FXLINK_MESSAGE_IMAGE_MONO)
return (width + 7) >> 3;
if(format == FXLINK_MESSAGE_IMAGE_GRAY)
return 2 * ((width + 7) >> 3);
return 0;
}
static struct fxlink_message_image_raw *decode_rgb565(void const *pixels,
int size, struct fxlink_message_image_raw *raw)
{
int bpr = img_bytes_per_row(FXLINK_MESSAGE_IMAGE_RGB565, raw->width);
for(int y = 0; y < raw->height; y++) {
void const *row = pixels + y * bpr;
for(int x = 0; x < raw->width; x++) {
/* Don't read past the read buffer if the image is incomplete */
void const *input = row + 2 * x;
uint16_t color = 0;
if(input - pixels + 2 <= size) color = *(uint16_t *)input;
color = ((color & 0xff00) >> 8) | ((color & 0x00ff) << 8);
raw->data[y][3*x+0] = (color >> 11) << 3;
raw->data[y][3*x+1] = ((color >> 5) & 0x3f) << 2;
raw->data[y][3*x+2] = (color & 0x1f) << 3;
}
}
return raw;
}
static struct fxlink_message_image_raw *decode_mono(void const *pixels,
int size, struct fxlink_message_image_raw *raw)
{
int bpr = img_bytes_per_row(FXLINK_MESSAGE_IMAGE_MONO, raw->width);
for(int y = 0; y < raw->height; y++) {
void const *row = pixels + y * bpr;
for(int x = 0; x < raw->width; x++) {
/* Don't read past the read buffer if the image is incomplete */
void const *input = row + (x >> 3);
int byte = 0;
if(input - pixels + 1 <= size) byte = *(uint8_t *)input;
int color = (byte & (0x80 >> (x & 7))) ? 0 : 255;
raw->data[y][3*x+0] = color;
raw->data[y][3*x+1] = color;
raw->data[y][3*x+2] = color;
}
}
return raw;
}
static struct fxlink_message_image_raw *decode_gray(void const *pixels,
int size, struct fxlink_message_image_raw *raw)
{
int bpr = img_bytes_per_row(FXLINK_MESSAGE_IMAGE_MONO, raw->width);
for(int k = 0; k < 2 * raw->height; k++) {
void const *row = pixels + k * bpr;
int y = k % raw->height;
for(int x = 0; x < raw->width; x++) {
/* Don't read past the read buffer if the image is incomplete */
void const *input = row + (x >> 3);
int byte = 0;
if(input - pixels + 1 <= size) byte = *(uint8_t *)input;
int color = (byte & (0x80 >> (x & 7)));
/* Everything is inverted */
if(!color) color = (k >= raw->height) ? 0xaa : 0x55;
else color = 0x00;
raw->data[y][3*x+0] += color;
raw->data[y][3*x+1] += color;
raw->data[y][3*x+2] += color;
}
}
return raw;
}
struct fxlink_message_image_raw *fxlink_message_image_decode(
struct fxlink_message const *msg)
{
if(!fxlink_message_is_fxlink_image(msg)
&& !fxlink_message_is_fxlink_video(msg))
return NULL;
struct fxlink_message_image_header *img = msg->data;
void *pixels = msg->data + sizeof *img;
int pixels_size = msg->size - sizeof *img;
/* Determine the expected data size */
int bytes_per_row = img_bytes_per_row(img->pixel_format, img->width);
int expected_size = img->height * bytes_per_row;
if(pixels_size < expected_size)
wlog("image has %d bytes but needs %d, it will be incomplete\n",
pixels_size, expected_size);
if(pixels_size > expected_size)
wlog("image has %d bytes but only needs %d, dropping extra\n",
pixels_size, expected_size);
/* Allocate memory for the decoded pixels */
struct fxlink_message_image_raw *raw = malloc(sizeof *raw);
uint8_t **data = calloc(img->height, sizeof(uint8_t *));
if(!raw || !data)
goto alloc_error;
for(size_t i = 0; i < img->height; i++) {
data[i] = calloc(img->width, 3);
if(!data[i])
goto alloc_error;
}
raw->width = img->width;
raw->height = img->height;
raw->data = data;
if(img->pixel_format == FXLINK_MESSAGE_IMAGE_RGB565)
return decode_rgb565(pixels, pixels_size, raw);
if(img->pixel_format == FXLINK_MESSAGE_IMAGE_MONO)
return decode_mono(pixels, pixels_size, raw);
if(img->pixel_format == FXLINK_MESSAGE_IMAGE_GRAY)
return decode_gray(pixels, pixels_size, raw);
elog("unknown pixel format: %d\n", img->pixel_format);
fxlink_message_image_raw_free(raw);
return NULL;
alloc_error:
elog("cannot allocate memory for decoded image: %m\n");
fxlink_message_image_raw_free(raw);
return NULL;
}
void fxlink_message_image_raw_free(struct fxlink_message_image_raw *raw)
{
if(raw->data){
for(int i = 0; i < raw->height; i++)
free(raw->data[i]);
}
free(raw->data);
free(raw);
}
//---
// Tools for crafting and receiving messages
//---
/* Check whether a header is valid. It's important for this function to be as
strict as possible so that picking up communication in the wrong spot
doesn't get out of control by recognizing messages everywhere. */
static bool header_valid(struct fxlink_message const *msg)
{
if(msg->version == 0x00000100) {
/* Transfer size larger than 16 MB: impossible for so many reasons */
if(msg->transfer_size > 0xffffff)
return false;
/* Non-printable characters in application/type names */
for(size_t i = 0; i < sizeof msg->application; i++) {
if(msg->application[i] && !isprint(msg->application[i]))
return false;
}
for(size_t i = 0; i < sizeof msg->type; i++) {
if(msg->type[i] && !isprint(msg->type[i]))
return false;
}
return true;
}
return false;
}
struct fxlink_transfer *fxlink_transfer_make_IN(void *data, int size)
{
int header_size = FXLINK_MESSAGE_HEADER_SIZE;
if(size < header_size) {
elog("cannot read message: header too short (%d < %zu bytes)\n",
size, header_size);
return NULL;
}
struct fxlink_transfer *tr = calloc(1, sizeof *tr);
if(!tr) {
elog("cannot allocate transfer structure: %m\n");
return NULL;
}
memcpy(&tr->msg, data, header_size);
if(!header_valid(&tr->msg)) {
elog("cannot read message: invalid header (%d bytes dropped)\n",
size);
free(tr);
return NULL;
}
tr->msg.data = malloc(tr->msg.size);
tr->direction = FXLINK_TRANSFER_IN;
tr->processed_size = 0;
tr->own_data = true;
if(!tr->msg.data) {
elog("cannot allocate buffer for %d bytes\n", tr->msg.size);
free(tr);
return NULL;
}
fxlink_transfer_receive(tr, data + header_size, size - header_size);
return tr;
}
struct fxlink_message *fxlink_transfer_finish_IN(struct fxlink_transfer *tr)
{
if(tr->direction != FXLINK_TRANSFER_IN || !fxlink_transfer_complete(tr))
return NULL;
struct fxlink_message *msg = malloc(sizeof *msg);
if(!msg) {
elog("cannot allocate message structure: %m\n");
return NULL;
}
/* Shallow copy the malloc()'d data pointer */
assert(tr->own_data && "Can't shallow copy data if we don't own it!");
memcpy(msg, &tr->msg, sizeof *msg);
free(tr);
return msg;
}
void fxlink_transfer_receive(struct fxlink_transfer *tr, void *data, int size)
{
int remaining_size = tr->msg.size - tr->processed_size;
if(size > remaining_size) {
elog("message too long, dropping %d bytes\n", size - remaining_size);
size = remaining_size;
}
memcpy(tr->msg.data + tr->processed_size, data, size);
tr->processed_size += size;
}
struct fxlink_transfer *fxlink_transfer_make_OUT(char const *application,
char const *type, void const *data, int size, bool own_data)
{
struct fxlink_transfer *tr = calloc(1, sizeof *tr);
if(!tr)
return NULL;
tr->msg.version = 0x00000100;
tr->msg.size = size;
tr->msg.transfer_size = 0;
strncpy(tr->msg.application, application, 16);
strncpy(tr->msg.type, type, 16);
tr->msg.data = (void *)data;
tr->direction = FXLINK_TRANSFER_OUT;
tr->processed_size = -1;
tr->own_data = own_data;
return tr;
}
bool fxlink_transfer_complete(struct fxlink_transfer const *tr)
{
return tr->processed_size >= (int)tr->msg.size;
}
void fxlink_transfer_free(struct fxlink_transfer *tr)
{
if(tr->own_data)
free(tr->msg.data);
free(tr);
}