├── .clang-format ├── .github └── workflows │ ├── buildFluidDial.yml │ └── buildRelease.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── ATTRIBUTION ├── FilesBG.svg ├── JogBG.svg ├── PCBackground.svg ├── Run.svg └── jogbg.xcf ├── build_merged.py ├── data ├── PCBackground.png ├── abouttp.png ├── disabled_button.png ├── filesbg.png ├── filestp.png ├── fluid_dial.png ├── green_button.png ├── hometp.png ├── jogbg.png ├── jogtp.png ├── macrostp.png ├── orange_button.png ├── powertp.png ├── probetp.png ├── red_button.png ├── statustp.png └── toolchangetp.png ├── git-version.py ├── include └── README ├── manifest.py ├── part_files ├── CYD_Dial_Bottom.stl ├── CYD_Dial_JC2432W328C.FCStd ├── CYD_Dial_JC2432W328C_bottom.3mf ├── CYD_Dial_JC2432W328C_top.3mf ├── CYD_Dial_Top.stl ├── FluidDialBottom.step ├── FluidDialLabel.pdf └── FluidDialTop.step ├── platformio.ini ├── src ├── AboutScene.cpp ├── AboutScene.h ├── Config.h ├── ConfigItem.cpp ├── ConfigItem.h ├── ConfirmScene.cpp ├── ConfirmScene.h ├── Drawing.cpp ├── Drawing.h ├── Encoder.cpp ├── Encoder.h ├── FileLinesParser.h ├── FileListParser,h ├── FileListParser2.h ├── FileMenu.cpp ├── FileMenu.h ├── FileParser.cpp ├── FileParser.h ├── FileParser2.cpp ├── FileParser2.h ├── FilePreviewScene.cpp ├── FileSelectScene.cpp ├── FilesLinesParser.cx ├── FluidNCModel.cpp ├── FluidNCModel.h ├── Hardware2432.cpp ├── Hardware2432.hpp ├── HardwareM5Dial.cpp ├── HardwareM5Dial.hpp ├── HelpScene.cpp ├── HomingScene.cpp ├── HomingScene.h ├── LGFX_SDL.hppx ├── MacroItem.h ├── MacroMenu.cpp ├── MacroMenu.cppxx ├── Menu.cpp ├── Menu.h ├── MenuScene.cpp ├── MultiJogScene.cpp ├── NVS.h ├── PieMenu.cpp ├── PieMenu.h ├── Point.cpp ├── Point.h ├── ProbingScene.cpp ├── Scene.cpp ├── Scene.h ├── StatusScene.cpp ├── System.cpp ├── System.h ├── SystemArduino.cpp ├── SystemWindows.cpp ├── Text.cpp ├── Text.h ├── ToolChangeScene.cpp ├── Touch_Class.cpp ├── Touch_Class.hpp ├── ardmain.cpp ├── polar.c ├── polar.h └── sdlmain.c └── test ├── M5_test.yaml └── README /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: '-4' 3 | AlignAfterOpenBracket: Align 4 | AlignConsecutiveAssignments: 'true' 5 | AlignConsecutiveDeclarations: 'true' 6 | AlignEscapedNewlines: Right 7 | AlignOperands: 'true' 8 | AlignTrailingComments: 'true' 9 | AllowShortBlocksOnASingleLine: 'true' 10 | AllowShortCaseLabelsOnASingleLine: 'false' 11 | AllowShortFunctionsOnASingleLine: Inline 12 | AllowShortIfStatementsOnASingleLine: 'false' 13 | AllowShortLoopsOnASingleLine: 'false' 14 | AlwaysBreakBeforeMultilineStrings: 'false' 15 | AlwaysBreakTemplateDeclarations: 'true' 16 | BinPackArguments: 'false' 17 | BinPackParameters: 'false' 18 | BreakBeforeBinaryOperators: None 19 | BraceWrapping: 20 | AfterClass: 'true' 21 | AfterControlStatement: 'true' 22 | AfterEnum: 'true' 23 | AfterFunction: 'true' 24 | AfterNamespace: 'true' 25 | AfterObjCDeclaration: 'true' 26 | AfterStruct: 'true' 27 | AfterUnion: 'true' 28 | AfterExternBlock: 'true' 29 | BeforeCatch: 'true' 30 | BeforeElse: 'true' 31 | SplitEmptyFunction: 'false' 32 | SplitEmptyRecord: 'false' 33 | SplitEmptyNamespace: 'false' 34 | BreakBeforeInheritanceComma: 'true' 35 | BreakBeforeTernaryOperators: 'false' 36 | BreakConstructorInitializers: AfterColon 37 | BreakInheritanceList: AfterColon 38 | ColumnLimit: '140' 39 | CommentPragmas: '^ :: ' 40 | CompactNamespaces: 'false' 41 | Cpp11BracedListStyle: 'false' 42 | FixNamespaceComments: 'false' 43 | IncludeCategories: 44 | - Regex: '^".*' 45 | Priority: 1 46 | - Regex: '^"(.*)/' 47 | Priority: 2 48 | - Regex: '^<(.*)/' 49 | Priority: 3 50 | - Regex: '.*' 51 | Priority: 4 52 | IncludeBlocks: Regroup 53 | IndentCaseLabels: 'true' 54 | IndentPPDirectives: AfterHash 55 | IndentWidth: '4' 56 | IndentWrappedFunctionNames: 'true' 57 | JavaScriptQuotes: Leave 58 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 59 | Language: Cpp 60 | MaxEmptyLinesToKeep: '1' 61 | NamespaceIndentation: All 62 | PenaltyBreakBeforeFirstCallParameter: 7 63 | PenaltyBreakAssignment: 8 64 | PenaltyBreakComment: 200 65 | PenaltyBreakFirstLessLess: 50 66 | PenaltyBreakString: 100 67 | PenaltyBreakTemplateDeclaration: 10 68 | PenaltyExcessCharacter: 10 69 | PenaltyReturnTypeOnItsOwnLine: 1000000 70 | PointerAlignment: Left 71 | ReflowComments: 'false' 72 | SortIncludes: 'false' 73 | SortUsingDeclarations: 'true' 74 | SpaceAfterTemplateKeyword: 'true' 75 | SpaceBeforeAssignmentOperators: 'true' 76 | SpaceBeforeCpp11BracedList: 'true' 77 | SpaceBeforeCtorInitializerColon: 'true' 78 | SpaceBeforeInheritanceColon: 'true' 79 | SpaceBeforeParens: ControlStatements 80 | SpaceBeforeRangeBasedForLoopColon: 'true' 81 | SpaceInEmptyParentheses: 'false' 82 | SpacesBeforeTrailingComments: '2' 83 | SpacesInAngles: 'false' 84 | SpacesInCStyleCastParentheses: 'false' 85 | SpacesInContainerLiterals: 'false' 86 | SpacesInParentheses: 'false' 87 | SpacesInSquareBrackets: 'false' 88 | Standard: Cpp11 89 | TabWidth: '4' 90 | UseTab: Never 91 | 92 | ... 93 | -------------------------------------------------------------------------------- /.github/workflows/buildFluidDial.yml: -------------------------------------------------------------------------------- 1 | name: Build FluidDial 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/cache@v4 13 | with: 14 | path: | 15 | ~/.cache/pip 16 | ~/.platformio/.cache 17 | key: ${{ runner.os }}-pio 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.11' 21 | - name: Install PlatformIO Core 22 | run: pip install --upgrade platformio 23 | 24 | - name: Build PlatformIO Project 25 | run: pio run && pio run -t buildfs && pio run -t build_merged 26 | - name: Upload binaries 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: FluidDial 30 | path: .pio/build/*/*.bin 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/buildRelease.yml: -------------------------------------------------------------------------------- 1 | name: Build FluidDial Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: 'Release version' 8 | required: true 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Cache pip 19 | uses: actions/cache@v4 20 | with: 21 | path: ~/.cache/pip 22 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 23 | restore-keys: | 24 | ${{ runner.os }}-pip- 25 | - name: Cache PlatformIO 26 | uses: actions/cache@v4 27 | with: 28 | path: ~/.platformio 29 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 30 | - name: Set up Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.11' 34 | - name: Install PlatformIO 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install --upgrade platformio 38 | - name: Set release version 39 | run: | 40 | git config user.email "wmb@firmworks.com" 41 | git config user.name "Mitch Bradley" 42 | git tag "${{ github.event.inputs.tag }}" -a -m "Release test" 43 | - name: Create release directory 44 | run: | 45 | mkdir -p release 46 | rm -f release/* 47 | - name: Build 48 | run: | 49 | for env in m5dial cyddial; do 50 | pio run -e $env 51 | pio run -e $env -t build_merged 52 | done 53 | python manifest.py 54 | ls release/* 55 | #- name: Upload binaries 56 | # uses: actions/upload-artifact@v4 57 | # with: 58 | # name: FluidDial_firmware 59 | # path: release/* 60 | - name: Create release 61 | uses: softprops/action-gh-release@v1 62 | with: 63 | tag_name: ${{ github.event.inputs.tag }} 64 | files: release/* 65 | draft: True 66 | - name: Deploy to fluidnc-releases 67 | uses: datalbry/copy_folder_to_another_repo_action@1.0.0 68 | env: 69 | API_TOKEN_GITHUB: ${{ secrets.FLUIDDIAL_RELEASE_COPY_TOKEN }} 70 | with: 71 | source_folder: 'release' 72 | destination_repo: 'bdring/fluiddial-releases' 73 | destination_branch: 'main' 74 | destination_folder: releases/${{ github.event.inputs.tag }} 75 | user_email: bdring@buildlog.net 76 | user_name: 'Bart Dring' 77 | commit_msg: Release ${{ github.event.inputs.tag }} 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | prefs 2 | .pio 3 | .vscode 4 | src/version.* 5 | *~ 6 | junk 7 | release 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FluidDial: A CNC Pendant for FluidNC Firmware. 2 | 3 | M5 Fluid DialCYD Dial Pendant 4 | 5 | Wiki pages for more information: [M5 FluidDial Pendant (left image)](http://wiki.fluidnc.com/en/hardware/official/M5Dial_Pendant) and [CYD Dial Pendant (right image)](http://wiki.fluidnc.com/en/hardware/official/CYD_Dial_Pendant). 6 | 7 | Both have similar functionality and similar cost, using different hardware. 8 | -------------------------------------------------------------------------------- /assets/ATTRIBUTION: -------------------------------------------------------------------------------- 1 | run by Eko Sofiantono from Noun Project (CC BY 3.0) 2 | -------------------------------------------------------------------------------- /assets/FilesBG.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 59 | 65 | 66 | 70 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /assets/JogBG.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 26 | 30 | 34 | 35 | 38 | 42 | 46 | 47 | 58 | 66 | 70 | 71 | 79 | 83 | 84 | 92 | 96 | 97 | 105 | 109 | 110 | 121 | 132 | 143 | 154 | 162 | 166 | 167 | 168 | 189 | 191 | 192 | 194 | image/svg+xml 195 | 197 | 198 | 199 | 200 | 201 | 205 | 214 | 215 | 229 | 234 | 239 | 245 | 251 | 257 | 258 | 262 | 263 | -------------------------------------------------------------------------------- /assets/Run.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 29 | 30 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 75 | 79 | 83 | 86 | 91 | 92 | Created by Eko Sofiantono 101 | from the Noun Project 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /assets/jogbg.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/assets/jogbg.xcf -------------------------------------------------------------------------------- /build_merged.py: -------------------------------------------------------------------------------- 1 | # This script adds a "build_merged" target, used like "pio run -e cyd -t build_merged", 2 | # that creates a combined image with both the firmware and the filesystem images 3 | # The two smaller images must be built first, with "pio run" and "pio run -t buildfs" 4 | Import("env") 5 | 6 | flash_size = env.BoardConfig().get("upload.flash_size", "detect") 7 | 8 | cmd = '$PYTHONEXE $UPLOADER --chip $BOARD_MCU merge_bin --output $BUILD_DIR/merged-flash.bin --flash_mode dio --flash_size ' + flash_size + " " 9 | 10 | for image in env.get("FLASH_EXTRA_IMAGES", []): 11 | cmd += image[0] + " " + env.subst(image[1]) + " " 12 | 13 | filesystem_start = env.GetProjectOption("custom_filesystem_start", "Missing_custom_filesystem_start_variable") 14 | 15 | cmd += " 0x10000 $BUILD_DIR/firmware.bin " + filesystem_start + " $BUILD_DIR/littlefs.bin" 16 | 17 | env.AddCustomTarget( 18 | name="build_merged", 19 | dependencies=["$BUILD_DIR/bootloader.bin", "$BUILD_DIR/firmware.bin"], 20 | actions=["pio run -e $PIOENV -t buildfs", cmd], 21 | title="Build Merged", 22 | description="Build combined image with program and filesystem" 23 | ) 24 | -------------------------------------------------------------------------------- /data/PCBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/PCBackground.png -------------------------------------------------------------------------------- /data/abouttp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/abouttp.png -------------------------------------------------------------------------------- /data/disabled_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/disabled_button.png -------------------------------------------------------------------------------- /data/filesbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/filesbg.png -------------------------------------------------------------------------------- /data/filestp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/filestp.png -------------------------------------------------------------------------------- /data/fluid_dial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/fluid_dial.png -------------------------------------------------------------------------------- /data/green_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/green_button.png -------------------------------------------------------------------------------- /data/hometp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/hometp.png -------------------------------------------------------------------------------- /data/jogbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/jogbg.png -------------------------------------------------------------------------------- /data/jogtp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/jogtp.png -------------------------------------------------------------------------------- /data/macrostp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/macrostp.png -------------------------------------------------------------------------------- /data/orange_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/orange_button.png -------------------------------------------------------------------------------- /data/powertp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/powertp.png -------------------------------------------------------------------------------- /data/probetp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/probetp.png -------------------------------------------------------------------------------- /data/red_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/red_button.png -------------------------------------------------------------------------------- /data/statustp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/statustp.png -------------------------------------------------------------------------------- /data/toolchangetp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/data/toolchangetp.png -------------------------------------------------------------------------------- /git-version.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import filecmp, tempfile, shutil, os 3 | 4 | # Thank you https://docs.platformio.org/en/latest/projectconf/section_env_build.html ! 5 | 6 | gitFail = False 7 | try: 8 | subprocess.check_call(["git", "status"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 9 | except: 10 | gitFail = True 11 | 12 | if gitFail: 13 | tag = "v0.0.x" 14 | rev = " (noGit)" 15 | url = " (noGit)" 16 | else: 17 | try: 18 | tag = ( 19 | subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"], stderr=subprocess.DEVNULL) 20 | .strip() 21 | .decode("utf-8") 22 | ) 23 | except: 24 | tag = "v0.0.x" 25 | 26 | # Check to see if the head commit exactly matches a tag. 27 | # If so, the revision is "release", otherwise it is BRANCH-COMMIT 28 | try: 29 | subprocess.check_call(["git", "describe", "--tags", "--exact"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 30 | rev = '' 31 | except: 32 | branchname = ( 33 | subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) 34 | .strip() 35 | .decode("utf-8") 36 | ) 37 | revision = ( 38 | subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) 39 | .strip() 40 | .decode("utf-8") 41 | ) 42 | modified = ( 43 | subprocess.check_output(["git", "status", "-uno", "-s"]) 44 | .strip() 45 | .decode("utf-8") 46 | ) 47 | if modified: 48 | dirty = "-dirty" 49 | else: 50 | dirty = "" 51 | rev = "%s-%s%s" % (branchname, revision, dirty) 52 | 53 | try: 54 | url = ( 55 | subprocess.check_output(["git", "config", "--get", "remote.origin.url"]) 56 | .strip() 57 | .decode("utf-8") 58 | ) 59 | except: 60 | url = "None" 61 | 62 | git_info = '%s%s' % (tag, rev) 63 | git_url = url 64 | 65 | provisional = "src/version.cxx" 66 | final = "src/version.cpp" 67 | with open(provisional, "w") as fp: 68 | fp.write('const char* git_info = \"' + git_info + '\";\n') 69 | fp.write('const char* git_url = \"' + git_url + '\";\n') 70 | 71 | if not os.path.exists(final): 72 | # No version.cpp so rename version.cxx to version.cpp 73 | os.rename(provisional, final) 74 | elif not filecmp.cmp(provisional, final): 75 | # version.cxx differs from version.cpp so get rid of the 76 | # old .cpp and rename .cxx to .cpp 77 | os.remove(final) 78 | os.rename(provisional, final) 79 | else: 80 | # The existing version.cpp is the same as the new version.cxx 81 | # so we can just leave the old version.cpp in place and get 82 | # rid of version.cxx 83 | os.remove(provisional) 84 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /manifest.py: -------------------------------------------------------------------------------- 1 | import subprocess, os, sys, shutil 2 | import hashlib 3 | 4 | relPath = os.path.join('release') 5 | 6 | tag = ( 7 | subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]) 8 | .strip() 9 | .decode("utf-8") 10 | ) 11 | 12 | manifestRelPath = relPath 13 | 14 | manifest = { 15 | "name": "FluidDial", 16 | "version": tag, 17 | "source_url": "https://github.com/bdring/FluidDial/tree/" + tag, 18 | "release_url": "https://github.com/bdring/FluidDial/releases/tag/" + tag, 19 | "funding_url": "https://www.paypal.com/donate/?hosted_button_id=8DYLB6ZYYDG7Y", 20 | "images": {}, 21 | "files": {}, 22 | # "upload": { 23 | # "name": "upload", 24 | # "description": "Things you can upload to the file system", 25 | # "choice-name": "Upload group", 26 | # "choices": [] 27 | # }, 28 | "installable": { 29 | "name": "installable", 30 | "description": "Things you can install", 31 | "choice-name": "Dial type", 32 | "choices": [] 33 | }, 34 | } 35 | 36 | def addImage(name, offset, srcfilename, srcpath, dstpath): 37 | dstFilename = name + '.bin' 38 | fulldstfile = os.path.join(dstpath, dstFilename) 39 | 40 | print(fulldstfile) 41 | shutil.copy(os.path.join(srcpath, srcfilename), fulldstfile) 42 | 43 | with open(fulldstfile, "rb") as f: 44 | data = f.read() 45 | image = { 46 | # "name": name, 47 | "size": os.path.getsize(fulldstfile), 48 | "offset": offset, 49 | "path": dstFilename, 50 | "signature": { 51 | "algorithm": "SHA2-256", 52 | "value": hashlib.sha256(data).hexdigest() 53 | } 54 | } 55 | if manifest['images'].get(name) != None: 56 | print("Duplicate image name", name) 57 | sys.exit(1) 58 | manifest['images'][name] = image 59 | 60 | for envName in ['m5dial', 'cyddial']: 61 | buildDir = os.path.join('.pio', 'build', envName) 62 | # shutil.copy(os.path.join(buildDir, 'merged-flash.bin'), os.path.join(relPath, envName + '.bin')) 63 | addImage(envName, '0x0000', 'merged-flash.bin', buildDir, manifestRelPath) 64 | 65 | def addSection(node, name, description, choice): 66 | section = { 67 | "name": name, 68 | "description": description, 69 | } 70 | if choice != None: 71 | section['choice-name'] = choice 72 | section['choices'] = [] 73 | node.append(section) 74 | 75 | def addDialType(name, description, choice=None): 76 | addSection(manifest['installable']['choices'], name, description, choice) 77 | 78 | def addInstallable(install_type, erase, images): 79 | for image in images: 80 | if manifest['images'].get(image) == None: 81 | print("Missing image", image) 82 | sys.exit(1) 83 | 84 | node1 = manifest['installable']['choices'] 85 | installable = { 86 | "name": install_type["name"], 87 | "description": install_type["description"], 88 | "erase": erase, 89 | "images": images 90 | } 91 | node1[len(node1)-1]['choices'].append(installable) 92 | 93 | fresh_install = { "name": "install", "description": "Complete FluidDial installation"} 94 | 95 | def makeManifest(): 96 | addDialType("FluidDial for M5Dial", "FluidDial for M5Dial", "FluidDial type") 97 | addInstallable(fresh_install, True, ["m5dial"]) 98 | 99 | addDialType("FluidDial for CYD", "FluidDial for CYD Dial", "FluidDial type") 100 | addInstallable(fresh_install, True, ["cyddial"]) 101 | 102 | 103 | makeManifest() 104 | 105 | import json 106 | def printManifest(): 107 | print(json.dumps(manifest, indent=2)) 108 | 109 | with open(os.path.join(manifestRelPath, "manifest.json"), "w") as manifest_file: 110 | json.dump(manifest, manifest_file, indent=2) 111 | 112 | sys.exit(0) 113 | -------------------------------------------------------------------------------- /part_files/CYD_Dial_Bottom.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/part_files/CYD_Dial_Bottom.stl -------------------------------------------------------------------------------- /part_files/CYD_Dial_JC2432W328C.FCStd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/part_files/CYD_Dial_JC2432W328C.FCStd -------------------------------------------------------------------------------- /part_files/CYD_Dial_JC2432W328C_bottom.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/part_files/CYD_Dial_JC2432W328C_bottom.3mf -------------------------------------------------------------------------------- /part_files/CYD_Dial_JC2432W328C_top.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/part_files/CYD_Dial_JC2432W328C_top.3mf -------------------------------------------------------------------------------- /part_files/CYD_Dial_Top.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/part_files/CYD_Dial_Top.stl -------------------------------------------------------------------------------- /part_files/FluidDialLabel.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdring/FluidDial/b021b32dbe1ada60f89908047785d5c06cd91ae7/part_files/FluidDialLabel.pdf -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | default_envs = m5dial, cyddial 13 | 14 | [common] 15 | build_flags = 16 | !python ./git-version.py 17 | -DJSP_USE_CHARP 18 | -DE4_POS_T 19 | -DVERBATIM_GCODE_MODES 20 | lib_deps = 21 | https://github.com/MitchBradley/json-streaming-parser#charp-1.0.2 22 | https://github.com/MitchBradley/GrblParser#9108f54 23 | build_src_filter = +<*.c> +<*.h> +<*.cpp> +<*.hpp> - - + - 24 | 25 | [env:m5dial] 26 | ; Pendant based on M5Dial 27 | ; http://wiki.fluidnc.com/en/hardware/official/M5Dial_Pendant 28 | platform = espressif32 29 | board = m5stack-stamps3 30 | framework = arduino 31 | platform_packages = 32 | platformio/framework-arduinoespressif32@3.20017.0 33 | monitor_filters=esp32_exception_decoder 34 | board_build.filesystem = littlefs 35 | upload_flags=--no-stub 36 | lib_deps = 37 | ${common.lib_deps} 38 | m5stack/M5Unified 39 | m5stack/M5Dial 40 | build_flags = 41 | ${common.build_flags} 42 | -DUSE_M5 43 | -DFNC_BAUD=1000000 44 | -DDEBUG_TO_USB 45 | custom_filesystem_start=0x670000 46 | extra_scripts = ./build_merged.py 47 | build_src_filter = ${common.build_src_filter} + + 48 | 49 | [env:cyd_base] 50 | ; Pendant based on a 2432S028 "Cheap Yellow Display" and a hand wheel pulse encoder 51 | ; http://wiki.fluidnc.com/en/hardware/official/CYD_Dial_Pendant 52 | platform = espressif32 53 | board = esp32dev 54 | framework = arduino 55 | platform_packages = 56 | platformio/framework-arduinoespressif32@^3.20016.0 57 | monitor_filters=esp32_exception_decoder 58 | monitor_speed=115200 59 | upload_speed=921600 60 | board_build.filesystem = littlefs 61 | upload_flags=--no-stub 62 | lib_deps = 63 | ${common.lib_deps} 64 | ; m5stack/M5Unified 65 | LovyanGFX=https://github.com/lovyan03/LovyanGFX#develop 66 | build_flags = 67 | ${common.build_flags} 68 | -DUSE_LOVYANGFX 69 | -DFNC_BAUD=1000000 70 | ;-DCORE_DEBUG_LEVEL=5 71 | -DCYD_BUTTONS 72 | custom_filesystem_start=0x290000 73 | extra_scripts = ./build_merged.py 74 | build_src_filter = ${common.build_src_filter} + + + - 75 | 76 | # This works for both resistive and capacitive CYDs, chosen at initial startup 77 | [env:cyddial] 78 | extends = env:cyd_base 79 | build_flags = 80 | ${env:cyd_nodebug.build_flags} 81 | -DCYD_BUTTONS 82 | -DRESISTIVE_CYD 83 | -DCAPACITIVE_CYD 84 | 85 | # This is the original CYD build for a resistive CYD 86 | [env:cyd_resistive] 87 | extends = env:cyd_base 88 | build_flags = 89 | ${env:cyd_base.build_flags} 90 | -DDEBUG_TO_USB 91 | -DRESISTIVE_CYD 92 | 93 | # This is for a capacitive CYD without buttons or lockout 94 | [env:cyd_nodebug] 95 | extends = env:cyd_base 96 | build_flags = 97 | ${env:cyd_base.build_flags} 98 | -DCAPACITIVE_CYD 99 | 100 | # This is for a capacitive CYD with physical buttons 101 | [env:cyd_buttons] 102 | extends = env:cyd_nodebug 103 | build_flags = 104 | ${env:cyd_nodebug.build_flags} 105 | -DCYD_BUTTONS 106 | 107 | # This is for a capacitive CYD with physical buttons and a lockout switch 108 | [env:cyd_lockable] 109 | extends = env:cyd_buttons 110 | build_flags = 111 | ${env:cyd_buttons.build_flags} 112 | -DLOCKOUT_PIN=GPIO_NUM_34 113 | 114 | [env:windows] 115 | ; Runs the code under Windows, useful for development 116 | lib_deps = 117 | ${common.lib_deps} 118 | m5stack/M5Unified@^0.1.10 119 | platform = native 120 | build_type = release 121 | build_flags = -O0 -xc++ -std=c++17 -lSDL2 122 | ${common.build_flags} 123 | -DWINDOWS 124 | -DUSE_M5 125 | -DM5GFX_BOARD=board_M5Dial 126 | -I"C:/msys64/mingw32/include/SDL2" ; for Windows SDL2 127 | -L"C:/msys64/mingw32/lib" ; for Windows SDL2 128 | build_src_filter = ${common.build_src_filter} + - 129 | -------------------------------------------------------------------------------- /src/AboutScene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Barton Dring 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Scene.h" 5 | #include "FileParser.h" 6 | #include "AboutScene.h" 7 | 8 | extern Scene menuScene; 9 | 10 | extern const char* git_info; // auto generated version.cpp 11 | 12 | static const int MIN_BRIGHTNESS = 8; 13 | 14 | void AboutScene::onEntry(void* arg) { 15 | getBrightness(); 16 | 17 | if (state != Disconnected) { 18 | send_line("$G"); 19 | send_line("$I"); 20 | } 21 | } 22 | 23 | void AboutScene::onDialButtonPress() { 24 | activate_scene(&menuScene); 25 | } 26 | void AboutScene::onGreenButtonPress() { 27 | #ifdef ARDUINO 28 | esp_restart(); 29 | #endif 30 | } 31 | void AboutScene::onRedButtonPress() { 32 | #ifdef USE_M5 33 | set_disconnected_state(); 34 | # ifdef ARDUINO 35 | centered_text("Use red button to wakeup", 118, RED, TINY); 36 | refreshDisplay(); 37 | delay_ms(2000); 38 | 39 | deep_sleep(0); 40 | # else 41 | dbg_println("Sleep"); 42 | # endif 43 | #else 44 | next_layout(1); 45 | reDisplay(); 46 | #endif 47 | } 48 | 49 | void AboutScene::onTouchClick() { 50 | fnc_realtime(StatusReport); 51 | if (state == Idle) { 52 | send_line("$G"); 53 | send_line("$I"); 54 | } 55 | } 56 | 57 | void AboutScene::onEncoder(int delta) { 58 | if (delta > 0 && _brightness < 255) { 59 | display.setBrightness(++_brightness); 60 | setPref("brightness", _brightness); 61 | } 62 | if (delta < 0 && _brightness > MIN_BRIGHTNESS) { 63 | display.setBrightness(--_brightness); 64 | setPref("brightness", _brightness); 65 | } 66 | reDisplay(); 67 | } 68 | void AboutScene::onStateChange(state_t old_state) { 69 | reDisplay(); 70 | } 71 | void AboutScene::reDisplay() { 72 | background(); 73 | drawStatus(); 74 | 75 | const int key_x = 118; 76 | const int val_x = 122; 77 | const int y_spacing = 20; 78 | int y = 80; 79 | 80 | std::string version_str = "Ver "; 81 | version_str += git_info; 82 | centered_text(version_str.c_str(), y, LIGHTGREY, TINY); 83 | refreshDisplay(); 84 | y += 10; 85 | #ifdef FNC_BAUD // FNC_BAUD might not be defined for Windows 86 | text("FNC baud:", key_x, y += y_spacing, LIGHTGREY, TINY, bottom_right); 87 | text(intToCStr(FNC_BAUD), val_x, y, GREEN, TINY, bottom_left); 88 | #endif 89 | 90 | #ifndef DEBUG_TO_USB // backlight shares a pin with this. 91 | text("Brightness:", key_x, y += y_spacing, LIGHTGREY, TINY, bottom_right); 92 | text(intToCStr(_brightness), val_x, y, GREEN, TINY, bottom_left); 93 | #endif 94 | 95 | if (wifi_ssid.length()) { 96 | std::string wifi_str = wifi_mode; 97 | if (wifi_mode == "No Wifi") { 98 | centered_text(wifi_str.c_str(), y += y_spacing, LIGHTGREY, TINY); 99 | } else { 100 | wifi_str += " "; 101 | wifi_str += wifi_ssid; 102 | centered_text(wifi_str.c_str(), y += y_spacing, LIGHTGREY, TINY); 103 | if (wifi_mode == "STA" && wifi_connected == "Not connected") { 104 | centered_text(wifi_connected.c_str(), y += y_spacing, RED, TINY); 105 | } else { 106 | wifi_str = "IP "; 107 | wifi_str += wifi_ip; 108 | centered_text(wifi_str.c_str(), y += y_spacing, LIGHTGREY, TINY); 109 | } 110 | } 111 | } 112 | 113 | #ifdef ARDUINO 114 | const char* greenLegend = "Restart"; 115 | #else 116 | const char* greenLegend = ""; 117 | #endif 118 | 119 | //drawOptionButton("Tool Menu", enable_tool_menu, 40, 135, 160); 120 | 121 | drawMenuTitle(current_scene->name()); 122 | 123 | #ifdef USE_M5 124 | drawButtonLegends("Sleep", greenLegend, "Menu"); 125 | #else 126 | drawButtonLegends("Layout", greenLegend, "Menu"); 127 | #endif 128 | drawError(); // if there is one 129 | refreshDisplay(); 130 | } 131 | 132 | int AboutScene::getBrightness() { 133 | if (initPrefs()) { 134 | getPref("brightness", &_brightness); 135 | } 136 | return _brightness; 137 | } 138 | 139 | AboutScene aboutScene; 140 | -------------------------------------------------------------------------------- /src/AboutScene.h: -------------------------------------------------------------------------------- 1 | #include "Scene.h" 2 | 3 | class AboutScene : public Scene { 4 | private: 5 | int _brightness = 255; 6 | 7 | public: 8 | AboutScene() : Scene("About", 4) {} 9 | 10 | void onEntry(void* arg); 11 | 12 | void onDialButtonPress(); 13 | void onGreenButtonPress(); 14 | void onRedButtonPress(); 15 | 16 | void onTouchClick() override; 17 | 18 | void onEncoder(int delta); 19 | void onStateChange(state_t old_state); 20 | void reDisplay(); 21 | int getBrightness(); 22 | }; 23 | -------------------------------------------------------------------------------- /src/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Instead of editing this file, consider adding -D lines in platformio.ini 4 | 5 | // System Interface 6 | 7 | // #define ECHO_FNC_TO_DEBUG 8 | 9 | // #define UART_ON_PORT_B // Not recommended, see comment in System.h 10 | 11 | // Automatically go to Jog Scene when first connected 12 | // #define AUTO_JOG_SCENE 13 | 14 | // Automatically go to Homing Scene when unhomed alarm is present 15 | // #define AUTO_HOMING_SCENE 16 | 17 | // Automatically leave Homing Scene after homing is finished 18 | // #define AUTO_HOMING_RETURN 19 | -------------------------------------------------------------------------------- /src/ConfigItem.cpp: -------------------------------------------------------------------------------- 1 | #include "ConfigItem.h" 2 | #include "Scene.h" 3 | 4 | std::vector configRequests; 5 | 6 | void parse_dollar(const char* line) { 7 | for (auto it = configRequests.begin(); it != configRequests.end(); ++it) { 8 | auto item = *it; 9 | 10 | size_t cmdlen = strlen(item->name()); 11 | 12 | if (strncmp(line, item->name(), cmdlen) == 0 && line[cmdlen] == '=') { 13 | line += cmdlen + 1; 14 | item->got(line); 15 | 16 | current_scene->reDisplay(); 17 | configRequests.erase(it); 18 | break; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ConfigItem.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "FluidNCModel.h" 5 | 6 | class ConfigItem; 7 | extern std::vector configRequests; 8 | 9 | class ConfigItem { 10 | private: 11 | const char* _name; 12 | bool _known; 13 | 14 | public: 15 | ConfigItem(const char* name) : _name(name), _known(false) {} 16 | 17 | virtual void set(const char* s) = 0; 18 | const char* name() { return _name; } 19 | bool known() { return _known; } 20 | void init() { 21 | _known = false; 22 | configRequests.push_back(this); 23 | send_line(_name); 24 | } 25 | void got(const char* s) { 26 | _known = true; 27 | set(s); 28 | } 29 | }; 30 | 31 | class IntConfigItem : public ConfigItem { 32 | private: 33 | int _value; 34 | 35 | public: 36 | IntConfigItem(const char* name) : ConfigItem(name) {} 37 | int get() { return _value; } 38 | void set(const char* s) { _value = atoi(s); } 39 | }; 40 | 41 | class PosConfigItem : public ConfigItem { 42 | private: 43 | pos_t _value; 44 | 45 | public: 46 | PosConfigItem(const char* name) : ConfigItem(name) {} 47 | pos_t get() { return _value; } 48 | void set(const char* s) { _value = atopos(s); } 49 | }; 50 | 51 | class StringConfigItem : public ConfigItem { 52 | private: 53 | std::string _value; 54 | 55 | public: 56 | StringConfigItem(const char* name) : ConfigItem(name) {} 57 | std::string get() { return _value; } 58 | void set(const char* s) { _value = s; } 59 | }; 60 | 61 | class BoolConfigItem : public ConfigItem { 62 | private: 63 | bool _value; 64 | 65 | public: 66 | BoolConfigItem(const char* name) : ConfigItem(name) {} 67 | bool get() { return _value; } 68 | void set(const char* s) { _value = strcmp(s, "true") == 0; } 69 | }; 70 | 71 | void parse_dollar(const char* line); 72 | -------------------------------------------------------------------------------- /src/ConfirmScene.cpp: -------------------------------------------------------------------------------- 1 | #include "Menu.h" 2 | #include 3 | 4 | // Confirm scene needs a string to display. It displays the string on 5 | // a background depicting the red and green buttons. 6 | // It pops when one of those buttons is pressed, or on back and flick back, 7 | // setting a variable to true iff the green button was pressed 8 | 9 | class ConfirmScene : public Scene { 10 | std::string _msg; 11 | 12 | public: 13 | ConfirmScene() : Scene("Confirm") {} 14 | void onEntry(void* arg) { _msg = (const char*)arg; } 15 | void reDisplay() { 16 | background(); 17 | canvas.fillRoundRect(10, 90, 220, 60, 15, YELLOW); 18 | centered_text(_msg.c_str(), 120, BLACK, MEDIUM); 19 | 20 | drawButtonLegends("No", "Yes", "Back"); 21 | 22 | refreshDisplay(); 23 | } 24 | void onRedButtonPress() { pop_scene(nullptr); } 25 | void onGreenButtonPress() { pop_scene((void*)"Confirmed"); } 26 | void onDialButtonPress() { pop_scene(nullptr); } 27 | }; 28 | ConfirmScene confirmScene; 29 | -------------------------------------------------------------------------------- /src/ConfirmScene.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Menu.h" 4 | 5 | extern Scene confirmScene; 6 | -------------------------------------------------------------------------------- /src/Drawing.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "System.h" 5 | #include "Drawing.h" 6 | #include "alarm.h" 7 | #include 8 | 9 | void drawBackground(int color) { 10 | canvas.fillSprite(color); 11 | } 12 | 13 | void drawFilledCircle(int x, int y, int radius, int fillcolor) { 14 | canvas.fillCircle(x, y, radius, fillcolor); 15 | } 16 | void drawFilledCircle(Point xy, int radius, int fillcolor) { 17 | Point dispxy = xy.to_display(); 18 | drawFilledCircle(dispxy.x, dispxy.y, radius, fillcolor); 19 | } 20 | 21 | void drawCircle(int x, int y, int radius, int thickness, int outlinecolor) { 22 | for (int i = 0; i < thickness; i++) { 23 | canvas.drawCircle(x, y, radius - i, outlinecolor); 24 | } 25 | } 26 | void drawCircle(Point xy, int radius, int thickness, int outlinecolor) { 27 | Point dispxy = xy.to_display(); 28 | drawCircle(dispxy.x, dispxy.y, radius, thickness, outlinecolor); 29 | } 30 | 31 | void drawOutlinedCircle(int x, int y, int radius, int fillcolor, int outlinecolor) { 32 | canvas.fillCircle(x, y, radius, fillcolor); 33 | canvas.drawCircle(x, y, radius, outlinecolor); 34 | } 35 | void drawOutlinedCircle(Point xy, int radius, int fillcolor, int outlinecolor) { 36 | Point dispxy = xy.to_display(); 37 | drawOutlinedCircle(dispxy.x, dispxy.y, radius, fillcolor, outlinecolor); 38 | } 39 | 40 | void drawRect(int x, int y, int width, int height, int radius, int bgcolor) { 41 | canvas.fillRoundRect(x, y, width, height, radius, bgcolor); 42 | } 43 | void drawRect(Point xy, int width, int height, int radius, int bgcolor) { 44 | Point offsetxy = { width / 2, -height / 2 }; // { 30, -30} 45 | Point dispxy = (xy - offsetxy).to_display(); // {i 46 | drawRect(dispxy.x, dispxy.y, width, height, radius, bgcolor); 47 | } 48 | void drawRect(Point xy, Point wh, int radius, int bgcolor) { 49 | drawRect(xy, wh.x, wh.y, radius, bgcolor); 50 | } 51 | 52 | void drawOutlinedRect(int x, int y, int width, int height, int bgcolor, int outlinecolor) { 53 | canvas.fillRoundRect(x, y, width, height, 5, bgcolor); 54 | canvas.drawRoundRect(x, y, width, height, 5, outlinecolor); 55 | } 56 | void drawOutlinedRect(Point xy, int width, int height, int bgcolor, int outlinecolor) { 57 | Point dispxy = xy.to_display(); 58 | drawOutlinedRect(dispxy.x, dispxy.y, width, height, bgcolor, outlinecolor); 59 | } 60 | void drawPngFile(const char* filename, Point xy) { 61 | // drawPngFile(filename, xo(xy.x), yo(xy.y)); 62 | // drawPngFile(filename, xy.x - 40, xy.y); 63 | drawPngFile(filename, xy.x, xy.y); 64 | } 65 | void drawPngBackground(const char* filename) { 66 | drawPngFile(filename, 0, 0); 67 | } 68 | void drawBackground(LGFX_Sprite* sprite) { 69 | sprite->pushSprite(0, 0); 70 | } 71 | LGFX_Sprite* createPngBackground(const char* filename) { 72 | LGFX_Sprite* sprite = new LGFX_Sprite(&canvas); 73 | sprite->setColorDepth(canvas.getColorDepth()); 74 | sprite->createSprite(canvas.width(), canvas.height()); 75 | drawPngFile(sprite, filename, 0, 0); 76 | return sprite; 77 | } 78 | 79 | // We use 1 to mean no background 80 | // 1 is visually indistinguishable from black so losing that value is unimportant 81 | #define NO_BG 1 82 | // clang-format off 83 | std::map stateBGColors = { 84 | { Idle, NO_BG }, 85 | { Alarm, RED }, 86 | { CheckMode, WHITE }, 87 | { Homing, NO_BG }, 88 | { Cycle, NO_BG }, 89 | { Hold, YELLOW }, 90 | { Jog, NO_BG }, 91 | { DoorOpen, RED }, 92 | { DoorClosed, YELLOW }, 93 | { GrblSleep, WHITE }, 94 | { ConfigAlarm, WHITE }, 95 | { Critical, WHITE }, 96 | { Disconnected, RED }, 97 | }; 98 | std::map stateFGColors = { 99 | { Idle, LIGHTGREY }, 100 | { Alarm, BLACK }, 101 | { CheckMode, BLACK }, 102 | { Homing, CYAN }, 103 | { Cycle, GREEN }, 104 | { Hold, BLACK }, 105 | { Jog, CYAN }, 106 | { DoorOpen, BLACK }, 107 | { DoorClosed, BLACK }, 108 | { GrblSleep, BLACK }, 109 | { ConfigAlarm, BLACK }, 110 | { Critical, BLACK }, 111 | { Disconnected, BLACK }, 112 | }; 113 | // clang-format on 114 | 115 | void drawStatus() { 116 | static constexpr int x = 100; 117 | static constexpr int y = 24; 118 | static constexpr int width = 140; 119 | static constexpr int height = 36; 120 | 121 | int bgColor = stateBGColors[state]; 122 | if (bgColor != 1) { 123 | canvas.fillRoundRect((display_short_side() - width) / 2, y, width, height, 5, bgColor); 124 | } 125 | int fgColor = stateFGColors[state]; 126 | if (state == Alarm) { 127 | centered_text(my_state_string, y + height / 2 - 4, fgColor, SMALL); 128 | centered_text(alarm_name_short[lastAlarm], y + height / 2 + 12, fgColor); 129 | } else { 130 | centered_text(my_state_string, y + height / 2 + 3, fgColor, MEDIUM); 131 | } 132 | } 133 | 134 | void drawStatusTiny(int y) { 135 | static constexpr int width = 90; 136 | static constexpr int height = 20; 137 | 138 | int bgColor = stateBGColors[state]; 139 | if (bgColor != 1) { 140 | canvas.fillRoundRect((display_short_side() - width) / 2, y, width, height, 5, bgColor); 141 | } 142 | centered_text(my_state_string, y + height / 2 + 3, stateFGColors[state], TINY); 143 | } 144 | 145 | void drawStatusSmall(int y) { 146 | static constexpr int width = 90; 147 | static constexpr int height = 25; 148 | 149 | int bgColor = stateBGColors[state]; 150 | if (bgColor != 1) { 151 | canvas.fillRoundRect((display_short_side() - width) / 2, y, width, height, 5, bgColor); 152 | } 153 | centered_text(my_state_string, y + height / 2 + 3, stateFGColors[state], SMALL); 154 | } 155 | 156 | Stripe::Stripe(int x, int y, int width, int height, fontnum_t font) : _x(x), _y(y), _width(width), _height(height), _font(font) {} 157 | 158 | void Stripe::draw(char left, const char* right, bool highlighted, int left_color) { 159 | char t[2] = { left, '\0' }; 160 | draw(t, right, highlighted, left_color); 161 | } 162 | void Stripe::draw(const char* left, const char* right, bool highlighted, int left_color) { 163 | drawOutlinedRect(_x, _y, _width, _height, highlighted ? BLUE : NAVY, WHITE); 164 | if (*left) { 165 | text(left, text_left_x(), text_middle_y(), left_color, _font, middle_left); 166 | } 167 | if (*right) { 168 | text(right, text_right_x(), text_middle_y(), WHITE, _font, middle_right); 169 | } 170 | advance(); 171 | } 172 | void Stripe::draw(const char* center, bool highlighted) { 173 | drawOutlinedRect(_x, _y, _width, _height, highlighted ? BLUE : NAVY, WHITE); 174 | text(center, text_center_x(), text_middle_y(), WHITE, _font, middle_center); 175 | advance(); 176 | } 177 | 178 | #define PUSH_BUTTON_LINE 212 179 | #define DIAL_BUTTON_LINE 228 180 | 181 | static int side_button_line() { 182 | return round_display ? PUSH_BUTTON_LINE : DIAL_BUTTON_LINE; 183 | } 184 | 185 | // This shows on the display what the button currently do. 186 | void drawButtonLegends(const char* red, const char* green, const char* orange) { 187 | text(red, round_display ? 50 : 10, side_button_line(), RED, TINY, middle_left); 188 | text(green, display_short_side() - (round_display ? 50 : 10), side_button_line(), GREEN, TINY, middle_right); 189 | centered_text(orange, DIAL_BUTTON_LINE, ORANGE); 190 | } 191 | 192 | void putDigit(int& n, int x, int y, int color) { 193 | char txt[2] = { '\0', '\0' }; 194 | txt[0] = "0123456789"[n % 10]; 195 | n /= 10; 196 | text(txt, x, y, color, MEDIUM, middle_right); 197 | } 198 | void fancyNumber(pos_t n, int n_decimals, int hl_digit, int x, int y, int text_color, int hl_text_color) { 199 | fontnum_t font = SMALL; 200 | int n_digits = n_decimals + 1; 201 | int i; 202 | bool isneg = n < 0; 203 | if (isneg) { 204 | n = -n; 205 | } 206 | #ifdef E4_POS_T 207 | // in e4 format, the number always has 4 postdecimal digits, 208 | // so if n_decimals is less than 4, we discard digits from 209 | // the right. We could do this by computing a divisor 210 | // based on e4_power10(4 - n_decimals), but the expected 211 | // number of iterations of this loop is max 4, typically 2, 212 | // so that is hardly worthwhile. 213 | for (i = 4; i > n_decimals; --i) { 214 | if (i == (n_decimals + 1)) { // Round 215 | n += 5; 216 | } 217 | n /= 10; 218 | } 219 | #else 220 | for (i = 0; i < n_decimals; i++) { 221 | n *= 10; 222 | } 223 | #endif 224 | const int char_width = 20; 225 | 226 | int ni = (int)n; 227 | for (i = 0; i < n_decimals; i++) { 228 | putDigit(ni, x, y, i == hl_digit ? hl_text_color : text_color); 229 | x -= char_width; 230 | } 231 | if (n_decimals) { 232 | text(".", x - 10, y, text_color, MEDIUM, middle_center); 233 | x -= char_width; 234 | } 235 | do { 236 | putDigit(ni, x, y, i++ == hl_digit ? hl_text_color : text_color); 237 | x -= char_width; 238 | } while (ni || i <= hl_digit); 239 | if (isneg) { 240 | text("-", x, y, text_color, MEDIUM, middle_right); 241 | } 242 | } 243 | 244 | void DRO::drawHoming(int axis, bool highlight, bool homed) { 245 | text(axisNumToCStr(axis), text_left_x(), text_middle_y(), myLimitSwitches[axis] ? GREEN : YELLOW, MEDIUM, middle_left); 246 | fancyNumber(myAxes[axis], num_digits(), -1, text_right_x(), text_middle_y(), highlight ? (homed ? GREEN : RED) : DARKGREY, RED); 247 | advance(); 248 | } 249 | 250 | void DRO::draw(int axis, int hl_digit, bool highlight) { 251 | text(axisNumToCStr(axis), text_left_x(), text_middle_y(), highlight ? GREEN : DARKGREY, MEDIUM, middle_left); 252 | fancyNumber( 253 | myAxes[axis], num_digits(), hl_digit, text_right_x(), text_middle_y(), highlight ? WHITE : DARKGREY, highlight ? RED : DARKGREY); 254 | advance(); 255 | } 256 | 257 | void DRO::draw(int axis, bool highlight) { 258 | Stripe::draw(axisNumToChar(axis), pos_to_cstr(myAxes[axis], num_digits()), highlight, myLimitSwitches[axis] ? GREEN : WHITE); 259 | } 260 | 261 | void LED::draw(bool highlighted) { 262 | drawOutlinedCircle(_x, _y, _radius, (highlighted) ? GREEN : DARKGREY, WHITE); 263 | _y += _gap; 264 | } 265 | 266 | void drawMenuTitle(const char* name) { 267 | centered_text(name, 12); 268 | } 269 | 270 | void refreshDisplay() { 271 | display.startWrite(); 272 | canvas.pushSprite(sprite_offset.x, sprite_offset.y); 273 | display.endWrite(); 274 | } 275 | 276 | void drawError() { 277 | if (lastError) { 278 | if ((milliseconds() - errorExpire) < 0) { 279 | canvas.fillCircle(120, 120, 95, RED); 280 | drawCircle(120, 120, 95, 5, WHITE); 281 | centered_text("Error", 95, WHITE, MEDIUM); 282 | centered_text(decode_error_number(lastError), 140, WHITE, TINY); 283 | } else { 284 | lastError = 0; 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/Drawing.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | // Factors for drawing parts of the pendant display 5 | 6 | #pragma once 7 | #include "FluidNCModel.h" 8 | #include "Text.h" 9 | 10 | class Stripe { 11 | private: 12 | int _x; 13 | int _width; 14 | int _height; 15 | fontnum_t _font; 16 | const int _text_inset = 5; 17 | 18 | protected: 19 | int _y; 20 | 21 | int text_left_x() { return _x + _text_inset; } 22 | int text_center_x() { return _x + _width / 2; } 23 | int text_right_x() { return _x + _width - _text_inset; } 24 | int text_middle_y() { return _y + _height / 2 + 2; } 25 | int widget_left_x() { return _x; } 26 | 27 | public: 28 | Stripe(int x, int y, int width, int height, fontnum_t font); 29 | void draw(const char* left, const char* right, bool highlighted, int left_color = WHITE); 30 | void draw(char left, const char* right, bool highlighted, int left_color = WHITE); 31 | void draw(const char* center, bool highlighted); 32 | int y() { return _y; } 33 | int gap() { return _height + 1; } 34 | void advance() { _y += gap(); } 35 | }; 36 | class LED { 37 | private: 38 | int _x; 39 | int _y; 40 | int _radius; 41 | int _gap; 42 | 43 | public: 44 | LED(int x, int y, int radius, int gap) : _x(x), _y(y), _radius(radius), _gap(gap) {} 45 | void draw(bool highlighted); 46 | }; 47 | 48 | class DRO : public Stripe { 49 | public: 50 | DRO(int x, int y, int width, int height) : Stripe(x, y, width, height, MEDIUM_MONO) {} 51 | void draw(int axis, bool highlight); 52 | void draw(int axis, int hl_digit, bool highlight); 53 | void drawHoming(int axis, bool highlight, bool homed); 54 | }; 55 | 56 | // draw stuff 57 | // Routines that take Point as an argument work in a coordinate 58 | // space where 0,0 is at the center of the display and +Y is up 59 | 60 | LGFX_Sprite* createPngBackground(const char* filename); 61 | 62 | void drawBackground(LGFX_Sprite* sprite); 63 | void drawBackground(int color); 64 | void drawStatus(); 65 | void drawStatusTiny(int y); 66 | void drawStatusSmall(int y); 67 | 68 | void drawFilledCircle(int x, int y, int radius, int fillcolor); 69 | void drawFilledCircle(Point xy, int radius, int fillcolor); 70 | 71 | void drawCircle(int x, int y, int radius, int thickness, int outlinecolor); 72 | void drawCircle(Point xy, int radius, int thickness, int outlinecolor); 73 | 74 | void drawOutlinedCircle(int x, int y, int radius, int fillcolor, int outlinecolor); 75 | void drawOutlinedCircle(Point xy, int radius, int fillcolor, int outlinecolor); 76 | 77 | void drawRect(int x, int y, int width, int height, int radius, int bgcolor); 78 | void drawRect(Point xy, int width, int height, int radius, int bgcolor); 79 | void drawRect(Point xy, Point wh, int radius, int bgcolor); 80 | 81 | void drawOutlinedRect(int x, int y, int width, int height, int bgcolor, int outlinecolor); 82 | void drawOutlinedRect(Point xy, int width, int height, int bgcolor, int outlinecolor); 83 | 84 | void drawButtonLegends(const char* red, const char* green, const char* orange); 85 | void drawMenuTitle(const char* name); 86 | 87 | void drawPngFile(const char* filename, Point xy); 88 | void drawPngBackground(const char* filename); 89 | 90 | void refreshDisplay(); 91 | 92 | void drawError(); 93 | 94 | extern Point sprite_offset; 95 | -------------------------------------------------------------------------------- /src/Encoder.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "Encoder.h" 3 | #include "sdkconfig.h" 4 | #include "driver/pcnt.h" 5 | #include "driver/gpio.h" 6 | 7 | /* clang-format: off */ 8 | void init_encoder(int a_pin, int b_pin) { 9 | pcnt_config_t enc_config = { 10 | .pulse_gpio_num = a_pin, //Rotary Encoder Chan A 11 | .ctrl_gpio_num = b_pin, //Rotary Encoder Chan B 12 | 13 | .lctrl_mode = PCNT_MODE_KEEP, // Rising A on HIGH B = CW Step 14 | .hctrl_mode = PCNT_MODE_REVERSE, // Rising A on LOW B = CCW Step 15 | .pos_mode = PCNT_COUNT_INC, // Count Only On Rising-Edges 16 | .neg_mode = PCNT_COUNT_DEC, // Discard Falling-Edge 17 | 18 | .counter_h_lim = INT16_MAX, 19 | .counter_l_lim = INT16_MIN, 20 | 21 | .unit = PCNT_UNIT_0, 22 | .channel = PCNT_CHANNEL_0, 23 | }; 24 | pcnt_unit_config(&enc_config); 25 | 26 | enc_config.pulse_gpio_num = b_pin; 27 | enc_config.ctrl_gpio_num = a_pin; 28 | enc_config.channel = PCNT_CHANNEL_1; 29 | enc_config.pos_mode = PCNT_COUNT_DEC; //Count Only On Falling-Edges 30 | enc_config.neg_mode = PCNT_COUNT_INC; // Discard Rising-Edge 31 | pcnt_unit_config(&enc_config); 32 | 33 | pcnt_set_filter_value(PCNT_UNIT_0, 250); // Filter Runt Pulses 34 | 35 | pcnt_filter_enable(PCNT_UNIT_0); 36 | 37 | gpio_pullup_en((gpio_num_t)a_pin); 38 | gpio_pullup_en((gpio_num_t)b_pin); 39 | 40 | pcnt_counter_pause(PCNT_UNIT_0); // Initial PCNT init 41 | pcnt_counter_clear(PCNT_UNIT_0); 42 | pcnt_counter_resume(PCNT_UNIT_0); 43 | } 44 | 45 | int16_t get_encoder() { 46 | int16_t count; 47 | pcnt_get_counter_value(PCNT_UNIT_0, &count); 48 | return count; 49 | } 50 | -------------------------------------------------------------------------------- /src/Encoder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | int16_t get_encoder(); 6 | void init_encoder(int a_pin, int b_pin); 7 | -------------------------------------------------------------------------------- /src/FileLinesParser.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | typedef void (*callback_t)(void*); 5 | 6 | #include 7 | #include 8 | 9 | extern std::string dirName; 10 | extern int dirLevel; 11 | 12 | extern void request_file_list(); 13 | extern void request_macro_list(); 14 | 15 | extern std::vector fileLines; 16 | 17 | extern void request_file_preview(const char* name); 18 | 19 | extern std::string current_filename; 20 | 21 | void init_listener(); 22 | void init_file_list(); 23 | 24 | void enter_directory(const char* dirname); 25 | inline void enter_directory(const std::string& dirname) { 26 | enter_directory(dirname.c_str()); 27 | } 28 | void exit_directory(); 29 | -------------------------------------------------------------------------------- /src/FileListParser,h: -------------------------------------------------------------------------------- 1 | enum FileType { 2 | ORDINARY, 3 | DIRECTORY, 4 | }; 5 | struct fileinfo { 6 | std::string fileName; 7 | FileType fileType; 8 | int fileSize; 9 | }; 10 | extern fileinfo fileInfo; 11 | extern std::vector fileVector; 12 | -------------------------------------------------------------------------------- /src/FileListParser2.h: -------------------------------------------------------------------------------- 1 | extern std::string current_filename; 2 | -------------------------------------------------------------------------------- /src/FileMenu.cpp: -------------------------------------------------------------------------------- 1 | #if 0 2 | // Copyright (c) 2023 - Mitch Bradley 3 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 4 | 5 | # include "FileMenu.h" 6 | # include 7 | # include 8 | # include "FileParser.h" 9 | # include "polar.h" 10 | 11 | extern Scene filePreviewScene; 12 | 13 | static bool isDirectory(const std::string& name) { 14 | return name.length() && name[name.length() - 1] == '/'; 15 | } 16 | static std::string dirNameOnly(const std::string& name) { 17 | if (isDirectory(name)) { 18 | return name.substr(0, name.length() - 1); 19 | } 20 | return name; 21 | } 22 | 23 | static std::string baseName(const std::string& name) { 24 | if (isDirectory(name)) { 25 | return name.substr(0, name.length() - 1); 26 | } 27 | auto dotpos = name.rfind('.'); 28 | if (dotpos != std::string::npos) { 29 | return name.substr(0, dotpos); 30 | } 31 | return name; 32 | } 33 | 34 | void FileItem::show(const Point& where) { 35 | dbg_printf("Show %s\n", name().c_str()); 36 | int color = WHITE; 37 | std::string s = baseName(name()); 38 | if (isDirectory(name())) { 39 | color = YELLOW; 40 | s += " >"; 41 | } 42 | 43 | if (_highlighted) { 44 | Point wh { 200, 45 }; 45 | drawRect(where, wh, 20, color); 46 | text(s.c_str(), where, BLACK, TINY, middle_center); 47 | } else { 48 | text(s.c_str(), where, WHITE, TINY, middle_center); 49 | } 50 | } 51 | 52 | const std::string& FileMenu::selected_name() { 53 | return _items[_selected]->name(); 54 | } 55 | 56 | void FileMenu::onEntry(void* arg) { 57 | dbg_println("Entering fss"); 58 | } 59 | 60 | void FileMenu::onRedButtonPress() { 61 | if (state != Idle) { 62 | return; 63 | } 64 | if (dirLevel) { 65 | exit_directory(); 66 | } else { 67 | init_file_list(); 68 | } 69 | ackBeep(); 70 | } 71 | void FileMenu::onFilesList() { 72 | _selected = 0; 73 | reDisplay(); 74 | } 75 | 76 | void FileMenu::onDialButtonPress() { 77 | pop_scene(); 78 | } 79 | 80 | void FileMenu::onGreenButtonPress() { 81 | if (state != Idle) { 82 | return; 83 | } 84 | if (num_items()) { 85 | std::string dName; 86 | if (isDirectory(selected_name())) { 87 | enter_directory(dirNameOnly(selected_name()).c_str()); 88 | } else { 89 | push_scene(&filePreviewScene, (void*)(selected_name().c_str())); 90 | } 91 | } 92 | ackBeep(); 93 | } 94 | 95 | void FileMenu::reDisplay() { 96 | dbg_printf("FM redisp sel %d ni %d\n", _selected, num_items()); 97 | menuBackground(); 98 | if (num_items() == 0) { 99 | Point where { 0, 0 }; 100 | Point wh { 200, 45 }; 101 | drawRect(where, wh, 20, YELLOW); 102 | text("No Files", where, BLACK, MEDIUM, middle_center); 103 | } else { 104 | if (_selected > 1) { 105 | _items[_selected - 2]->show({ 0, 70 }); 106 | } 107 | if (_selected > 0) { 108 | _items[_selected - 1]->show({ 0, 40 }); 109 | } 110 | dbg_println(selected_name()); 111 | _items[_selected]->show({ 0, 0 }); 112 | if (_selected < num_items() - 1) { 113 | _items[_selected + 1]->show({ 0, -40 }); 114 | } 115 | if (_selected < num_items() - 2) { 116 | _items[_selected + 2]->show({ 0, -70 }); 117 | } 118 | } 119 | buttonLegends(); 120 | refreshDisplay(); 121 | } 122 | void FileMenu::buttonLegends() { 123 | const char* grnLabel = ""; 124 | const char* redLabel = ""; 125 | 126 | if (state == Idle) { 127 | redLabel = dirLevel ? "Up.." : "Refresh"; 128 | 129 | if (num_items()) { 130 | grnLabel = isDirectory(selected_name()) ? "Down.." : "Load"; 131 | } 132 | } 133 | 134 | drawButtonLegends(redLabel, grnLabel, "Back"); 135 | } 136 | 137 | void FileMenu::rotate(int delta) { 138 | if (_selected == 0 && delta <= 0) { 139 | return; 140 | } 141 | if (_selected == num_items() && delta >= 0) { 142 | return; 143 | } 144 | if (_selected != -1) { 145 | _items[_selected]->unhighlight(); 146 | } 147 | _selected += delta; 148 | if (_selected < 0) { 149 | _selected = 0; 150 | } 151 | if (_selected >= num_items()) { 152 | _selected = num_items() - 1; 153 | } 154 | _items[_selected]->highlight(); 155 | reDisplay(); 156 | } 157 | 158 | int FileMenu::touchedItem(int x, int y) { 159 | return -1; 160 | } 161 | 162 | void FileMenu::menuBackground() { 163 | background(); 164 | drawPngBackground("/filesbg.png"); 165 | 166 | text(dirName, { 0, 100 }, YELLOW, MEDIUM); 167 | 168 | // Draw dot showing the selected file 169 | if (num_items() > 1) { 170 | int span = 100; // degrees 171 | int dtheta = span * _selected / (num_items() - 1); 172 | int theta = (span / 2) - dtheta; 173 | int dx, dy; 174 | r_degrees_to_xy(110, theta, &dx, &dy); 175 | 176 | drawFilledCircle({ dx, dy }, 8, WHITE); 177 | } 178 | } 179 | 180 | FileMenu wmbFileSelectScene; 181 | #endif 182 | -------------------------------------------------------------------------------- /src/FileMenu.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Menu.h" 5 | 6 | class FileItem : public Item { 7 | private: 8 | public: 9 | FileItem(const char* name) : Item(name) {} 10 | void invoke(void* arg) override { 11 | // doFileScreen(_name); 12 | } 13 | void show(const Point& where) override; 14 | }; 15 | 16 | class FileMenu : public Menu { 17 | private: 18 | int _selected_file = 0; 19 | std::string _dirname = "/"; 20 | 21 | public: 22 | FileMenu() : Menu("Files") {} 23 | 24 | const std::string& selected_name(); 25 | void onEntry(void* arg) override; 26 | 27 | void onRedButtonPress() override; 28 | void onFilesList() override; 29 | 30 | void onDialButtonPress() override; 31 | 32 | void onGreenButtonPress() override; 33 | void reDisplay() override; 34 | 35 | void buttonLegends(); 36 | void rotate(int delta) override; 37 | int touchedItem(int x, int y) override; 38 | 39 | void menuBackground() override; 40 | }; 41 | -------------------------------------------------------------------------------- /src/FileParser.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include 5 | #include 6 | 7 | typedef void (*callback_t)(void*); 8 | 9 | struct fileinfo { 10 | std::string fileName; 11 | int fileSize; 12 | bool isDir() const { return fileSize < 0; } 13 | }; 14 | 15 | extern fileinfo fileInfo; 16 | extern std::vector fileVector; 17 | 18 | extern void request_file_list(const char* dirname); 19 | 20 | struct Macro { 21 | std::string name; 22 | std::string filename; 23 | std::string target; 24 | }; 25 | 26 | extern std::vector macros; 27 | 28 | extern void request_macros(); 29 | 30 | extern void request_file_preview(const char* name, int firstline, int lastline); 31 | 32 | extern std::string current_filename; 33 | extern std::string wifi_mode, wifi_ip, wifi_connected, wifi_ssid; 34 | 35 | void init_listener(); 36 | void init_file_list(); 37 | -------------------------------------------------------------------------------- /src/FileParser2.cpp: -------------------------------------------------------------------------------- 1 | #if 0 2 | // Copyright (c) 2023 - Mitch Bradley 3 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 4 | 5 | # include "FileParser2.h" 6 | 7 | # include "FileMenu.h" 8 | # include "GrblParserC.h" // send_line() 9 | 10 | # include 11 | # include 12 | 13 | extern FileMenu wmbFileSelectScene; 14 | 15 | JsonStreamingParser parser; 16 | 17 | // This is necessary because of an annoying "feature" of JsonStreamingParser. 18 | // After it issues an endDocument, it sets its internal state to STATE_DONE, 19 | // in which it ignores everything. You cannot reset the parser in the endDocument 20 | // handler because it sets that state afterwards. So we have to record the fact 21 | // that an endDocument has happened and do the reset later, when new data comes in. 22 | bool parser_needs_reset = true; 23 | 24 | int dirLevel = 0; 25 | std::string dirName("/sd"); 26 | 27 | std::string current_filename; 28 | 29 | void enter_directory(const char* name) { 30 | dirName += "/"; 31 | dirName += name; 32 | 33 | ++dirLevel; 34 | request_file_list(); 35 | } 36 | void exit_directory() { 37 | if (dirLevel) { 38 | auto pos = dirName.rfind('/'); 39 | dirName = dirName.substr(0, pos); 40 | --dirLevel; 41 | request_file_list(); 42 | } 43 | } 44 | 45 | static bool fileinfoCompare(Item& f1, Item& f2) { 46 | // sort into filename order, with files first and folders second (same as on webUI) 47 | return f1.name().compare(f2.name()) < 0; 48 | } 49 | 50 | std::vector fileLines; 51 | 52 | class FilesListListener : public JsonListener { 53 | private: 54 | bool keyIsName; 55 | bool keyIsSize; 56 | std::string fileName; 57 | 58 | public: 59 | void whitespace(char c) override {} 60 | 61 | void startDocument() override {} 62 | void startArray() override { wmbFileSelectScene.removeAllItems(); } 63 | void startObject() override { 64 | keyIsName = keyIsSize = false; 65 | fileName = ""; 66 | } 67 | 68 | void key(const char* key) override { 69 | if (strcmp(key, "name") == 0) { 70 | keyIsName = true; // gets reset in endObject() 71 | return; 72 | } 73 | if (strcmp(key, "size") == 0) { 74 | keyIsSize = true; // gets reset in endObject() 75 | return; 76 | } 77 | } 78 | 79 | void value(const char* value) override { 80 | if (keyIsSize) { 81 | int size = atoi(value); 82 | dbg_printf("size %d\n", size); 83 | if (size < 0) { 84 | fileName += "/"; 85 | } 86 | keyIsSize = false; 87 | return; 88 | } 89 | if (keyIsName) { 90 | fileName = value; 91 | keyIsName = false; 92 | return; 93 | } 94 | } 95 | 96 | void endArray() override { 97 | // std::sort(fileVector.begin(), fileVector.end(), fileinfoCompare); 98 | } 99 | 100 | void endObject() override { 101 | if (fileName.length()) { 102 | wmbFileSelectScene.addItem(new FileItem(fileName.c_str())); 103 | } 104 | } 105 | 106 | //#define DEBUG_FILE_LIST 107 | void endDocument() override { 108 | init_listener(); 109 | current_scene->onFilesList(); 110 | } 111 | } filesListListener; 112 | #endif 113 | -------------------------------------------------------------------------------- /src/FileParser2.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include 5 | #include 6 | 7 | typedef void (*callback_t)(void*); 8 | 9 | extern std::string dirName; 10 | extern int dirLevel; 11 | 12 | extern void request_file_list(); 13 | 14 | extern std::vector fileLines; 15 | 16 | extern void request_file_preview(const char* name); 17 | 18 | extern std::string current_filename; 19 | 20 | void init_listener(); 21 | void init_file_list(); 22 | 23 | void enter_directory(const char* dirname); 24 | inline void enter_directory(const std::string& dirname) { 25 | enter_directory(dirname.c_str()); 26 | } 27 | void exit_directory(); 28 | -------------------------------------------------------------------------------- /src/FilePreviewScene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Barton Dringstarting 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include 5 | #include "Scene.h" 6 | #include "FileParser.h" 7 | #include 8 | 9 | extern Scene menuScene; 10 | extern Scene statusScene; 11 | 12 | class FilePreviewScene : public Scene { 13 | std::string _error_string; 14 | std::string _filename; 15 | bool _needlines; 16 | int _firstline = 0; 17 | 18 | std::map _lines; 19 | 20 | static const int _nlines = 7; 21 | 22 | public: 23 | FilePreviewScene() : Scene("Preview", 4) {} 24 | void get_lines() { 25 | _needlines = true; 26 | request_file_preview(_filename.c_str(), _firstline, _nlines); 27 | } 28 | 29 | void onEntry(void* arg) { 30 | if (arg) { 31 | char* fname = (char*)arg; 32 | _filename = fname; 33 | get_lines(); 34 | } else { 35 | _needlines = false; 36 | } 37 | } 38 | void onFileLines(int firstline, const std::vector& lines) { 39 | _error_string.clear(); 40 | _needlines = false; 41 | _lines.clear(); 42 | for (auto const& line : lines) { 43 | _lines[firstline++] = line; 44 | } 45 | reDisplay(); 46 | } 47 | void onError(const char* errstr) { 48 | _error_string = errstr; 49 | reDisplay(); 50 | } 51 | void scroll(int updown) { 52 | if (updown == 0) { 53 | return; 54 | } 55 | int fl = _firstline; 56 | fl += updown; 57 | if (fl >= 0) { 58 | _firstline = fl; 59 | get_lines(); 60 | } 61 | } 62 | 63 | void onDialButtonPress() { pop_scene(); } 64 | 65 | void onEncoder(int delta) override { scroll(delta); } 66 | 67 | void onRedButtonPress() { 68 | if (state == Idle) { 69 | pop_scene(); 70 | ackBeep(); 71 | } 72 | } 73 | 74 | void onDROChange() { reDisplay(); } 75 | 76 | void onGreenButtonPress() { 77 | if (state == Idle) { 78 | send_linef("$SD/Run=%s", _filename.c_str()); 79 | ackBeep(); 80 | } 81 | } 82 | 83 | void onStateChange(state_t old_state) { 84 | if (state == Cycle) { 85 | push_scene(&statusScene); 86 | } 87 | } 88 | 89 | void reDisplay() { 90 | background(); 91 | drawMenuTitle(name()); 92 | 93 | const char* grnLabel = ""; 94 | const char* redLabel = ""; 95 | 96 | if (state == Idle) { 97 | if (_needlines == false) { 98 | int y = 48; 99 | int tl = 0; 100 | if (_lines.size()) { 101 | for (auto const& entry : _lines) { 102 | text(entry.second.c_str(), 25, y + tl * 22, WHITE, TINY, top_left); 103 | ++tl; 104 | } 105 | } else { 106 | text("Empty File", 120, 120, WHITE, SMALL, middle_center); 107 | } 108 | } else if (_error_string.length()) { 109 | text(_error_string, 120, 120, WHITE, SMALL, middle_center); 110 | } else { 111 | text("Reading File", 120, 120, WHITE, TINY, middle_center); 112 | } 113 | grnLabel = "Run"; 114 | redLabel = "Back"; 115 | } else { 116 | centered_text("Invalid State", 105, WHITE, SMALL); 117 | centered_text("File Preview", 145, WHITE, SMALL); 118 | } 119 | 120 | drawButtonLegends(redLabel, grnLabel, "Back"); 121 | drawStatusSmall(21); 122 | refreshDisplay(); 123 | } 124 | }; 125 | FilePreviewScene filePreviewScene; 126 | -------------------------------------------------------------------------------- /src/FilesLinesParser.cx: -------------------------------------------------------------------------------- 1 | class FileLinesListener : public JsonListener { 2 | private: 3 | bool _in_array; 4 | 5 | public: 6 | void whitespace(char c) override {} 7 | 8 | void startDocument() override {} 9 | void startArray() override { 10 | fileLines.clear(); 11 | _in_array = true; 12 | } 13 | void startObject() override {} 14 | 15 | void key(const char* key) override { 16 | // if (strcmp(key, "path") == 0) {} 17 | } 18 | 19 | void value(const char* value) override { 20 | if (_in_array) { 21 | fileLines.push_back(value); 22 | } 23 | } 24 | 25 | void endArray() override { _in_array = false; } 26 | void endObject() override {} 27 | void endDocument() override { 28 | init_listener(); 29 | current_scene->onFileLines(); 30 | } 31 | } fileLinesListener; 32 | 33 | class InitialListener : public JsonListener { 34 | private: 35 | public: 36 | void whitespace(char c) override {} 37 | void startDocument() override {} 38 | void value(const char* value) override {} 39 | void endArray() override {} 40 | void endObject() override {} 41 | void endDocument() override {} 42 | void startArray() override {} 43 | void startObject() override {} 44 | 45 | void key(const char* key) override { 46 | if (strcmp(key, "files") == 0) { 47 | parser.setListener(&filesListListener); 48 | return; 49 | } 50 | if (strcmp(key, "file_lines") == 0) { 51 | parser.setListener(&fileLinesListener); 52 | return; 53 | } 54 | } 55 | } initialListener; 56 | 57 | void init_listener() { 58 | parser.setListener(&initialListener); 59 | parser_needs_reset = true; 60 | } 61 | 62 | void request_file_list() { 63 | send_linef("$Files/ListGCode=%s", dirName.c_str()); 64 | parser_needs_reset = true; 65 | } 66 | 67 | void init_file_list() { 68 | init_listener(); 69 | dirLevel = 0; 70 | dirName = "/sd"; 71 | request_file_list(); 72 | } 73 | 74 | void request_file_preview(const char* name) { 75 | send_linef("$File/ShowSome=7,%s/%s", dirName.c_str(), name); 76 | } 77 | extern "C" void handle_msg(char* command, char* arguments) { 78 | if (strcmp(command, "RST") == 0) { 79 | dbg_println("FluidNC Reset"); 80 | } 81 | if (strcmp(command, "Files changed") == 0) { 82 | dbg_println("Files changed"); 83 | init_file_list(); 84 | } 85 | if (strcmp(command, "JSON") == 0) { 86 | if (parser_needs_reset) { 87 | parser.reset(); 88 | parser_needs_reset = false; 89 | } 90 | while (*arguments) { 91 | parser.parse(*arguments++); 92 | } 93 | #define Ack 0xB2 94 | fnc_realtime((realtime_cmd_t)Ack); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/FluidNCModel.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "FluidNCModel.h" 5 | #include "ConfigItem.h" 6 | #include "FileParser.h" // init_file_list() 7 | #include 8 | #include "System.h" 9 | #include "Scene.h" 10 | #include "e4math.h" 11 | #include "HomingScene.h" 12 | 13 | extern Scene statusScene; 14 | 15 | // local copies of status items 16 | const char* my_state_string = "N/C"; 17 | state_t state = Idle; 18 | int n_axes = 3; 19 | pos_t myAxes[6] = { 0 }; 20 | bool myLimitSwitches[6] = { false }; 21 | bool myProbeSwitch = false; 22 | const char* myFile = ""; // running SD filename 23 | const char* myCtrlPins = ""; 24 | file_percent_t myPercent = 0.0; // percent conplete of SD file 25 | override_percent_t myFro = 100; // Feed rate override 26 | override_percent_t mySro = 100; // Spindle Override 27 | uint32_t myFeed = 0; 28 | uint32_t mySpeed = 0; 29 | uint32_t mySelectedTool = 0; 30 | 31 | std::string myModes = "no data"; 32 | 33 | int lastAlarm = 0; 34 | int lastError = 0; 35 | bool inInches = false; 36 | uint32_t errorExpire; 37 | 38 | int num_digits() { 39 | return inInches ? 3 : 2; 40 | } 41 | 42 | // clang-format off 43 | // Maps the state strings in status reports to internal state enum values 44 | struct cmp_str { 45 | bool operator()(char const *a, char const *b) const { 46 | return strcmp(a, b) < 0; 47 | } 48 | }; 49 | 50 | std::map state_map = { 51 | { "Idle", Idle }, 52 | { "Alarm", Alarm }, 53 | { "Hold:0", Hold }, 54 | { "Hold:1", Hold }, 55 | { "Run", Cycle }, 56 | { "Jog", Jog }, 57 | { "Home", Homing }, 58 | { "Door:0", DoorClosed }, 59 | { "Door:1", DoorOpen }, 60 | { "Check", CheckMode }, 61 | { "Sleep", GrblSleep }, 62 | }; 63 | // clang-format on 64 | 65 | bool decode_state_string(const char* state_string, state_t& state) { 66 | if (strcmp(my_state_string, state_string) != 0) { 67 | auto found = state_map.find(state_string); 68 | if (found != state_map.end()) { 69 | my_state_string = found->first; 70 | state = found->second; 71 | return true; 72 | } 73 | } 74 | return false; 75 | } 76 | 77 | void set_disconnected_state() { 78 | state = Disconnected; 79 | my_state_string = "N/C"; 80 | } 81 | 82 | // clang-format off 83 | std::map error_map = { // Do here so abreviations are right for the dial 84 | { 0, "None"}, 85 | { 1, "GCode letter"}, 86 | { 2, "GCode format"}, 87 | { 3, "Bad $ command"}, 88 | { 4, "Negative value"}, 89 | { 5, "Setting Diabled"}, 90 | { 10, "Soft limit error"}, 91 | { 13, "Check door"}, 92 | { 18, "No Homing Cycles"}, 93 | { 20, "Unsupported GCode"}, 94 | { 22, "Undefined feedrate"}, 95 | { 19, "No single axis"}, 96 | { 34, "Arc radius error"}, 97 | { 39, "P Param Exceeded"}, 98 | }; 99 | // clang-format on 100 | 101 | const char* decode_error_number(int error_num) { 102 | if (error_map.find(error_num) != error_map.end()) { 103 | return error_map[error_num]; 104 | } 105 | static char retval[33]; 106 | sprintf(retval, "%d", error_num); 107 | return retval; 108 | } 109 | 110 | extern "C" void begin_status_report() { 111 | myPercent = 0; 112 | } 113 | 114 | extern "C" void show_file(const char* filename, file_percent_t percent) { 115 | myPercent = percent; 116 | } 117 | 118 | extern "C" void show_overrides(override_percent_t feed_ovr, override_percent_t rapid_ovr, override_percent_t spindle_ovr) { 119 | myFro = feed_ovr; 120 | mySro = spindle_ovr; 121 | } 122 | 123 | extern "C" void show_feed_spindle(uint32_t feedrate, uint32_t spindle_speed) { 124 | myFeed = feedrate; 125 | mySpeed = spindle_speed; 126 | }; 127 | 128 | extern "C" void show_limits(bool probe, const bool* limits, size_t n_axis) { 129 | myProbeSwitch = probe; 130 | memcpy(myLimitSwitches, limits, n_axis * sizeof(*limits)); 131 | } 132 | 133 | extern "C" void show_control_pins(const char* pins) { 134 | //dbg_printf("show_control_pins:%s\r\n", pins); 135 | myCtrlPins = pins; 136 | } 137 | 138 | #ifdef E4_POS_T 139 | extern "C" void show_dro(const pos_t* axes, const pos_t* wco, bool isMpos, bool* limits, size_t n_axis) { 140 | n_axes = (int)n_axis; 141 | for (int axis = 0; axis < n_axis; axis++) { 142 | e4_t axis_val = axes[axis]; 143 | if (isMpos) { 144 | axis_val -= wco[axis]; 145 | } 146 | myAxes[axis] = inInches ? e4_mm_to_inch(axis_val) : axis_val; 147 | } 148 | } 149 | #else 150 | pos_t fromMm(pos_t position) { 151 | return inInches ? position / 25.4 : position; 152 | } 153 | pos_t toMm(pos_t position) { 154 | return inInches ? position * 25.4 : position; 155 | } 156 | 157 | extern "C" void show_dro(const pos_t* axes, const pos_t* wco, bool isMpos, bool* limits, size_t n_axis) { 158 | for (int axis = 0; axis < n_axis; axis++) { 159 | myAxes[axis] = fromMm(axes[axis]); 160 | if (isMpos) { 161 | myAxes[axis] -= fromMm(wco[axis]); 162 | } 163 | } 164 | } 165 | #endif 166 | 167 | void send_line(const char* s, int timeout) { 168 | fnc_send_line(s, timeout); 169 | dbg_println(s); 170 | } 171 | static void vsend_linef(const char* fmt, va_list va) { 172 | static char buf[128]; 173 | vsnprintf(buf, 128, fmt, va); 174 | send_line(buf); 175 | } 176 | void send_linef(const char* fmt, ...) { 177 | va_list args; 178 | va_start(args, fmt); 179 | vsend_linef(fmt, args); 180 | va_end(args); 181 | } 182 | 183 | char axisNumToChar(int axis) { 184 | return "XYZABC"[axis]; 185 | } 186 | 187 | const char* axisNumToCStr(int axis) { 188 | static char ret[2] = { '\0', '\0' }; 189 | ret[0] = axisNumToChar(axis); 190 | return ret; 191 | } 192 | 193 | const char* intToCStr(int val) { 194 | static char buffer[20]; 195 | sprintf(buffer, "%d", val); 196 | return buffer; 197 | } 198 | 199 | const char* mode_string() { 200 | return myModes.c_str(); 201 | } 202 | 203 | state_t previous_state; 204 | bool awaiting_alarm = false; 205 | 206 | extern "C" void show_state(const char* state_string) { 207 | previous_state = state; 208 | state_t new_state; 209 | if (decode_state_string(state_string, new_state) && state != new_state) { 210 | if (state == Disconnected) { 211 | fnc_realtime((realtime_cmd_t)0x0c); // Ctrl-L - echo off 212 | send_line("$G"); // Refresh GCode modes 213 | send_line("$G"); // Refresh GCode modes 214 | send_line("$RI=200"); 215 | init_file_list(); 216 | detect_homing_info(); 217 | } 218 | state = new_state; 219 | if (state == Alarm && lastAlarm == 0) { // Unknown 220 | send_line("$A"); // Get last alarm 221 | awaiting_alarm = true; 222 | return; 223 | } 224 | act_on_state_change(); 225 | } 226 | } 227 | 228 | extern "C" void handle_other(char* line) { 229 | if (*line == '$') { 230 | parse_dollar(line); 231 | return; 232 | } 233 | int alarmlen = strlen("Active alarm: "); 234 | if (strncmp(line, "Active alarm: ", alarmlen) == 0) { 235 | lastAlarm = atoi(line + alarmlen); 236 | if (awaiting_alarm) { 237 | dbg_printf("Got alarm %d\n", lastAlarm); 238 | awaiting_alarm = false; 239 | act_on_state_change(); 240 | } 241 | } 242 | } 243 | 244 | extern "C" void show_error(int error) { 245 | errorExpire = milliseconds() + 1000; 246 | lastError = error; 247 | current_scene->reDisplay(); 248 | } 249 | 250 | extern "C" void show_timeout() { 251 | dbg_println("Timeout"); 252 | } 253 | extern "C" void show_ok() {} 254 | 255 | extern "C" void end_status_report() { 256 | current_scene->onDROChange(); 257 | } 258 | 259 | extern "C" void show_alarm(int alarm) { 260 | lastAlarm = alarm; 261 | current_scene->reDisplay(); 262 | } 263 | 264 | extern "C" void show_gcode_modes(struct gcode_modes* modes) { 265 | inInches = strcmp(modes->units, "In") == 0 || strcmp(modes->units, "G20") == 0; 266 | 267 | myModes = modes->wcs; 268 | myModes += " "; 269 | myModes += modes->units; 270 | myModes += " "; 271 | myModes += modes->distance; 272 | myModes += " "; 273 | myModes += modes->spindle; 274 | if (strcmp(modes->mist, "On") == 0) { 275 | myModes += " Mist"; 276 | } 277 | if (strcmp(modes->flood, "On") == 0) { 278 | myModes += " Flood"; 279 | } 280 | 281 | mySelectedTool = modes->tool; 282 | current_scene->reDisplay(); 283 | } 284 | 285 | int disconnect_ms = 0; 286 | int next_ping_ms = 0; 287 | 288 | // If we haven't heard from FluidNC in 4 seconds for some other reason, 289 | // send a status report request. 290 | const int ping_interval_ms = 4000; 291 | 292 | // If we haven't heard from FluidNC in 6 seconds for any reason, declare 293 | // FluidNC unresponsive. After a ping, FluidNC has 2 seconds to respond. 294 | const int disconnect_interval_ms = 6000; 295 | 296 | bool starting = true; 297 | 298 | void request_status_report() { 299 | fnc_putchar(0x11); // XON; request software flow control 300 | fnc_realtime(StatusReport); // Request fresh status 301 | next_ping_ms = milliseconds() + ping_interval_ms; 302 | } 303 | 304 | bool fnc_is_connected() { 305 | int now = milliseconds(); 306 | if (starting) { 307 | starting = false; 308 | disconnect_ms = now + (disconnect_interval_ms - ping_interval_ms); 309 | request_status_report(); // sets next_ping_ms 310 | return false; // Do we need a value for "unknown"? 311 | } 312 | if ((now - disconnect_ms) >= 0) { 313 | next_ping_ms = now + ping_interval_ms; 314 | disconnect_ms = now + disconnect_interval_ms; 315 | return false; 316 | } 317 | 318 | if ((now - next_ping_ms) >= 0) { 319 | request_status_report(); 320 | } 321 | return true; 322 | } 323 | 324 | void update_rx_time() { 325 | int now = milliseconds(); 326 | next_ping_ms = now + ping_interval_ms; 327 | disconnect_ms = now + disconnect_interval_ms; 328 | } 329 | -------------------------------------------------------------------------------- /src/FluidNCModel.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #pragma once 5 | #include "GrblParserC.h" 6 | 7 | // Same states as FluidNC except for the last one 8 | enum state_t { 9 | Idle = 0, // Must be zero. 10 | Alarm, // In alarm state. Locks out all g-code processes. Allows settings access. 11 | CheckMode, // G-code check mode. Locks out planner and motion only. 12 | Homing, // Performing homing cycle 13 | Cycle, // Cycle is running or motions are being executed. 14 | Hold, // Active feed hold 15 | Jog, // Jogging mode. 16 | DoorOpen, 17 | DoorClosed, 18 | GrblSleep, // Sleep state. 19 | ConfigAlarm, // You can't do anything but fix your config file. 20 | Critical, // You can't do anything but reset with CTRL-x or the reset button 21 | Disconnected, // We can't talk to FluidNC 22 | }; 23 | 24 | // Variables and functions to model the state of the FluidNC controller 25 | 26 | extern state_t state; 27 | extern state_t previous_state; 28 | extern const char* my_state_string; 29 | 30 | extern int n_axes; 31 | extern pos_t myAxes[6]; 32 | extern bool myLimitSwitches[6]; 33 | extern bool myProbeSwitch; 34 | extern const char* myCtrlPins; 35 | extern const char* myFile; 36 | extern file_percent_t myPercent; 37 | extern override_percent_t myFro; 38 | extern override_percent_t mySro; 39 | extern uint32_t myFeed; 40 | extern uint32_t mySpeed; 41 | extern int lastAlarm; 42 | extern int lastError; 43 | extern uint32_t errorExpire; 44 | extern bool inInches; 45 | extern uint32_t mySelectedTool; 46 | 47 | int num_digits(); 48 | 49 | void send_line(const char* s, int timeout = 2000); 50 | void send_linef(const char* fmt, ...); 51 | 52 | const char* intToCStr(int val); 53 | const char* axisNumToCStr(int axis); 54 | char axisNumToChar(int axis); 55 | 56 | state_t decode_state_string(const char* state_string); 57 | const char* decode_error_number(int error_num); 58 | const char* mode_string(); 59 | 60 | bool fnc_is_connected(); 61 | void set_disconnected_state(); 62 | 63 | void update_rx_time(); 64 | 65 | extern pos_t toMm(pos_t position); 66 | extern pos_t fromMm(pos_t position); 67 | -------------------------------------------------------------------------------- /src/Hardware2432.hpp: -------------------------------------------------------------------------------- 1 | #ifdef DEBUG_TO_USB 2 | 3 | // This variant lets you have debugging output on the 4 | // USB serial port, at the expense of not being able 5 | // to control the backlight brightness due to the use 6 | // of GPIO 21 as backlight control on some boards. 7 | 8 | constexpr static const int PND_RX_FNC_TX_PIN = GPIO_NUM_35; 9 | constexpr static const int PND_TX_FNC_RX_PIN = GPIO_NUM_21; 10 | constexpr static const int FNC_UART_NUM = 1; 11 | #else 12 | 13 | // This variant lets you control the backlight via GPIO 21, 14 | // and opens up GPIO pins that could be used for other 15 | // purposes, but you do not have debug output. 16 | 17 | constexpr static const int PND_RX_FNC_TX_PIN = GPIO_NUM_1; 18 | constexpr static const int PND_TX_FNC_RX_PIN = GPIO_NUM_3; 19 | constexpr static const int FNC_UART_NUM = 0; 20 | #endif 21 | 22 | // GPIO assignments for the encoder are set by 23 | // init_hardware() in Hardware2432.cpp, based 24 | // on which board variant is in use. 25 | -------------------------------------------------------------------------------- /src/HardwareM5Dial.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | // System interface routines for the Arduino framework 5 | 6 | #include "System.h" 7 | #include "M5GFX.h" 8 | #include "Drawing.h" 9 | #include "HardwareM5Dial.hpp" 10 | 11 | LGFX_Device& display = M5Dial.Display; 12 | LGFX_Sprite canvas(&M5Dial.Display); 13 | m5::Speaker_Class& speaker = M5Dial.Speaker; 14 | m5::Touch_Class& touch = M5Dial.Touch; 15 | Stream& debugPort = USBSerial; 16 | 17 | m5::Button_Class& dialButton = M5Dial.BtnA; 18 | m5::Button_Class greenButton; 19 | m5::Button_Class redButton; 20 | 21 | bool round_display = true; 22 | 23 | void init_hardware() { 24 | auto cfg = M5.config(); 25 | 26 | // Don't enable the encoder because M5's encoder driver is flaky 27 | M5Dial.begin(cfg, false, false); 28 | 29 | // Turn on the power hold pin 30 | lgfx::gpio::command(lgfx::gpio::command_mode_output, GPIO_NUM_46); 31 | lgfx::gpio::command(lgfx::gpio::command_write_high, GPIO_NUM_46); 32 | 33 | // This must be done after M5Dial.begin which sets the PortA pins 34 | // to I2C mode. We need to override that to use them for serial. 35 | // The baud rate is irrelevant because USBSerial emulates a UART 36 | // API but the data never travels over an actual physical UART 37 | // link with a defined baud rate. The data instead travels over 38 | // a USB link at the USB data rate. You can set the baud rate 39 | // at the other end to anything you want and it will still work. 40 | USBSerial.begin(); 41 | 42 | init_fnc_uart(FNC_UART_NUM, PND_TX_FNC_RX_PIN, PND_RX_FNC_TX_PIN); 43 | 44 | // Setup external GPIOs as buttons 45 | lgfx::gpio::command(lgfx::gpio::command_mode_input_pullup, RED_BUTTON_PIN); 46 | lgfx::gpio::command(lgfx::gpio::command_mode_input_pullup, GREEN_BUTTON_PIN); 47 | 48 | greenButton.setDebounceThresh(5); 49 | redButton.setDebounceThresh(5); 50 | 51 | init_encoder(ENC_A, ENC_B); 52 | 53 | speaker.setVolume(255); 54 | 55 | touch.setFlickThresh(30); 56 | } 57 | 58 | Point sprite_offset { 0, 0 }; 59 | 60 | void show_logo() { 61 | display.drawPngFile(LittleFS, "/fluid_dial.png", 0, 0, display.width(), display.height(), 0, 0, 0.0f, 0.0f, datum_t::middle_center); 62 | } 63 | 64 | void base_display() { 65 | display.clear(); 66 | } 67 | 68 | void next_layout(int delta) {} 69 | 70 | void system_background() { 71 | canvas.fillSprite(TFT_BLACK); 72 | } 73 | 74 | bool switch_button_touched(bool& pressed, int& button) { 75 | if (redButton.wasPressed()) { 76 | button = 0; 77 | pressed = true; 78 | return true; 79 | } 80 | if (redButton.wasReleased()) { 81 | button = 0; 82 | pressed = false; 83 | return true; 84 | } 85 | if (dialButton.wasPressed()) { 86 | button = 1; 87 | pressed = true; 88 | return true; 89 | } 90 | if (dialButton.wasReleased()) { 91 | button = 1; 92 | pressed = false; 93 | return true; 94 | } 95 | if (greenButton.wasPressed()) { 96 | button = 2; 97 | pressed = true; 98 | return true; 99 | } 100 | if (greenButton.wasReleased()) { 101 | button = 2; 102 | pressed = false; 103 | return true; 104 | } 105 | return false; 106 | } 107 | 108 | bool screen_encoder(int x, int y, int& delta) { 109 | return false; 110 | } 111 | bool screen_button_touched(bool pressed, int x, int y, int& button) { 112 | return false; 113 | } 114 | 115 | void update_events() { 116 | M5Dial.update(); 117 | 118 | auto ms = m5gfx::millis(); 119 | 120 | // The red and green buttons are active low 121 | redButton.setRawState(ms, !m5gfx::gpio_in(RED_BUTTON_PIN)); 122 | greenButton.setRawState(ms, !m5gfx::gpio_in(GREEN_BUTTON_PIN)); 123 | } 124 | 125 | void ackBeep() { 126 | speaker.tone(1800, 50); 127 | } 128 | 129 | bool ui_locked() { 130 | return false; 131 | } 132 | 133 | #include 134 | // The M5 Library is broken with respect to deep sleep on M5 Dial 135 | // so we have to do it ourselves. The problem is that the WAKE 136 | // button is supposed to be the dial button that connects to GPIO42, 137 | // but that can't work because GPIO42 is not an RTC GPIO and thus 138 | // cannot be used as an ext0 wakeup source. 139 | void deep_sleep(int us) { 140 | display.sleep(); 141 | 142 | rtc_gpio_pullup_en((gpio_num_t)WAKEUP_GPIO); 143 | 144 | esp_sleep_enable_ext0_wakeup((gpio_num_t)WAKEUP_GPIO, false); 145 | while (digitalRead(WAKEUP_GPIO) == false) { 146 | delay_ms(10); 147 | } 148 | if (us > 0) { 149 | esp_sleep_enable_timer_wakeup(us); 150 | } else { 151 | // esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_TIMER); 152 | } 153 | esp_deep_sleep_start(); 154 | } 155 | -------------------------------------------------------------------------------- /src/HardwareM5Dial.hpp: -------------------------------------------------------------------------------- 1 | constexpr static const int ENC_A = GPIO_NUM_40; 2 | constexpr static const int ENC_B = GPIO_NUM_41; 3 | constexpr static const int DIAL_BUTTON_PIN = GPIO_NUM_42; 4 | 5 | #include "M5Dial.h" 6 | 7 | constexpr static const int FNC_UART_NUM = 1; 8 | #ifdef UART_ON_PORT_B 9 | constexpr static const int RED_BUTTON_PIN = GPIO_NUM_13; 10 | constexpr static const int GREEN_BUTTON_PIN = GPIO_NUM_15; 11 | constexpr static const int PND_RX_FNC_TX_PIN = GPIO_NUM_1; 12 | constexpr static const int PND_TX_FNC_RX_PIN = GPIO_NUM_2; 13 | 14 | #else // UART is on PORT A 15 | // This pin assignment avoids a problem whereby touch will not work 16 | // if the pendant is powered independently of the FluidNC controller 17 | // and the pendant is power-cycled while the FluidNC controller is on. 18 | // The problem is caused by back-powering of the 3V3 rail through the 19 | // M5Stamp's Rx pin. When RX is on GPIO1, 3V3 from the FluidNC Tx line 20 | // causes the M5Stamp 3V3 rail to float at 1.35V, which in turn prevents 21 | // the touch chip from starting properly after full power is applied. 22 | // The touch function then does not work. 23 | // When RX is on GPIO15, the back-powering drives the 3V3 rail only to 24 | // 0.3V and everything starts correctly. 25 | constexpr static const int RED_BUTTON_PIN = GPIO_NUM_1; 26 | constexpr static const int GREEN_BUTTON_PIN = GPIO_NUM_2; 27 | constexpr static const int PND_RX_FNC_TX_PIN = GPIO_NUM_15; 28 | constexpr static const int PND_TX_FNC_RX_PIN = GPIO_NUM_13; 29 | #endif 30 | 31 | #define WAKEUP_GPIO RED_BUTTON_PIN 32 | -------------------------------------------------------------------------------- /src/HelpScene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Scene.h" 5 | 6 | static const char* null_help[] = { "", "HelpScene:", "Null pointer", NULL }; 7 | class HelpScene : public Scene { 8 | public: 9 | HelpScene() : Scene("Help") {} 10 | void onEntry(void* arg) { 11 | const char** msg = arg ? static_cast(arg) : null_help; 12 | const char* line; 13 | drawBackground(BROWN); 14 | int pos = 20; 15 | for (; line = *msg, line; ++msg) { 16 | centered_text(line, pos, WHITE, TINY); 17 | pos += 28; 18 | } 19 | drawButtonLegends("", "", "Back"); 20 | refreshDisplay(); 21 | } 22 | void onDialButtonPress() { pop_scene(); } 23 | void onTouchClick() { 24 | if (touchIsCenter()) { 25 | pop_scene(); 26 | } 27 | } 28 | } helpScene; 29 | -------------------------------------------------------------------------------- /src/HomingScene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Barton Dring 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Scene.h" 5 | #include "ConfigItem.h" 6 | 7 | extern Scene statusScene; 8 | 9 | #define HOMING_N_AXIS 3 10 | 11 | IntConfigItem homing_cycles[HOMING_N_AXIS] = { 12 | { "$/axes/x/homing/cycle" }, 13 | { "$/axes/y/homing/cycle" }, 14 | { "$/axes/z/homing/cycle" }, 15 | }; 16 | BoolConfigItem homing_allows[HOMING_N_AXIS] = { 17 | { "$/axes/x/homing/allow_single_axis" }, 18 | { "$/axes/y/homing/allow_single_axis" }, 19 | { "$/axes/z/homing/allow_single_axis" }, 20 | }; 21 | 22 | int homed_axes = 0; 23 | bool is_homed(int axis) { 24 | return homed_axes & (1 << axis); 25 | } 26 | void set_axis_homed(int axis) { 27 | homed_axes |= 1 << axis; 28 | current_scene->reDisplay(); 29 | } 30 | 31 | void detect_homing_info() { 32 | for (int i = 0; i < HOMING_N_AXIS; i++) { 33 | homing_cycles[i].init(); 34 | homing_allows[i].init(); 35 | } 36 | homed_axes = 0; 37 | } 38 | bool can_home(int i) { 39 | // Cannot home if cycle == 0 and !allow_single_axis 40 | return homing_cycles[i].get() != 0 || homing_allows[i].get(); 41 | } 42 | 43 | bool have_homing_info() { 44 | return homing_allows[HOMING_N_AXIS - 1].known(); 45 | } 46 | 47 | class HomingScene : public Scene { 48 | private: 49 | int _axis_to_home = -1; 50 | int _auto = false; 51 | 52 | bool _allows[HOMING_N_AXIS]; 53 | 54 | public: 55 | HomingScene() : Scene("Home", 4) {} 56 | 57 | bool is_homing(int axis) { return can_home(axis) && (_axis_to_home == -1 || _axis_to_home == axis); } 58 | void onEntry(void* arg) override { 59 | if (state == Idle && _auto) { 60 | pop_scene(); 61 | } 62 | const char* s = static_cast(arg); 63 | _auto = s && strcmp(s, "auto") == 0; 64 | } 65 | 66 | void onStateChange(state_t old_state) override { 67 | #ifdef AUTO_HOMING_RETURN 68 | if (old_state == Homing && state == Idle && _auto) { 69 | pop_scene(); 70 | } 71 | #endif 72 | } 73 | void onDialButtonPress() override { pop_scene(); } 74 | void onGreenButtonPress() override { 75 | if (state == Idle || state == Alarm) { 76 | if (_axis_to_home != -1) { 77 | send_linef("$H%c", axisNumToChar(_axis_to_home)); 78 | } else { 79 | send_line("$H"); 80 | } 81 | } else if (state == Cycle) { 82 | fnc_realtime(FeedHold); 83 | } else if (state == Hold || state == DoorClosed) { 84 | fnc_realtime(CycleStart); 85 | } 86 | } 87 | void onRedButtonPress() override { 88 | if (state == Homing || state == Alarm) { 89 | fnc_realtime(Reset); 90 | } 91 | } 92 | 93 | void increment_axis_to_home() { 94 | do { 95 | ++_axis_to_home; 96 | if (_axis_to_home > HOMING_N_AXIS) { 97 | _axis_to_home = -1; 98 | return; 99 | } 100 | } while (!can_home(_axis_to_home)); 101 | } 102 | void onTouchClick() { 103 | if (state == Idle || state == Homing || state == Alarm) { 104 | increment_axis_to_home(); 105 | reDisplay(); 106 | ackBeep(); 107 | } 108 | } 109 | 110 | void onEncoder(int delta) override { 111 | increment_axis_to_home(); 112 | reDisplay(); 113 | } 114 | void onDROChange() { reDisplay(); } // also covers any status change 115 | 116 | void reDisplay() { 117 | background(); 118 | drawMenuTitle(current_scene->name()); 119 | drawStatus(); 120 | 121 | const char* redLabel = ""; 122 | std::string grnLabel = ""; 123 | const char* orangeLabel = ""; 124 | std::string green = "Home "; 125 | 126 | if (false && state == Homing) { 127 | DRO dro(16, 68, 210, 32); 128 | for (size_t axis = 0; axis < HOMING_N_AXIS; axis++) { 129 | dro.draw(axis, -1, true); 130 | } 131 | 132 | } else if (state == Idle || state == Homing || state == Alarm) { 133 | DRO dro(16, 68, 210, 32); 134 | for (int axis = 0; axis < HOMING_N_AXIS; ++axis) { 135 | dro.drawHoming(axis, is_homing(axis), is_homed(axis)); 136 | } 137 | 138 | #if 0 139 | int x = 50; 140 | int y = 65; 141 | int width = display.width() - (x * 2); 142 | int height = 32; 143 | 144 | Stripe button(x, y, width, height, SMALL); 145 | button.draw("Home All", _axis_to_home == -1); 146 | y = button.y(); // LEDs start with the Home X button 147 | button.draw("Home X", _axis_to_home == 0); 148 | button.draw("Home Y", _axis_to_home == 1); 149 | button.draw("Home Z", _axis_to_home == 2); 150 | LED led(x - 16, y + height / 2, 10, button.gap()); 151 | led.draw(myLimitSwitches[0]); 152 | led.draw(myLimitSwitches[1]); 153 | led.draw(myLimitSwitches[2]); 154 | #endif 155 | 156 | if (state == Homing) { 157 | redLabel = "E-Stop"; 158 | } else { 159 | if (state == Alarm && (strchr(myCtrlPins, 'D') == NULL)) { // You can reset alarms if door is not active 160 | redLabel = "Reset"; 161 | } 162 | if (_axis_to_home == -1) { 163 | for (int axis = 0; axis < HOMING_N_AXIS; ++axis) { 164 | if (can_home(axis)) { 165 | if (!grnLabel.length()) { 166 | grnLabel = "Home"; 167 | } 168 | 169 | grnLabel += axisNumToChar(axis); 170 | } 171 | } 172 | } else { 173 | grnLabel = "Home"; 174 | grnLabel += axisNumToChar(_axis_to_home); 175 | } 176 | } 177 | } else { 178 | centered_text("Invalid State", 105, WHITE, MEDIUM); 179 | centered_text("For Homing", 145, WHITE, MEDIUM); 180 | redLabel = "E-Stop"; 181 | if (state == Cycle) { 182 | grnLabel = "Hold"; 183 | } else if (state == Hold || state == DoorClosed) { 184 | grnLabel = "Resume"; 185 | } 186 | } 187 | drawButtonLegends(redLabel, grnLabel.c_str(), "Back"); 188 | 189 | refreshDisplay(); 190 | } 191 | }; 192 | HomingScene homingScene; 193 | -------------------------------------------------------------------------------- /src/HomingScene.h: -------------------------------------------------------------------------------- 1 | extern void detect_homing_info(); 2 | extern void set_axis_homed(int axis); 3 | -------------------------------------------------------------------------------- /src/LGFX_SDL.hppx: -------------------------------------------------------------------------------- 1 | namespace lgfx { 2 | inline namespace v1 { 3 | //---------------------------------------------------------------------------- 4 | 5 | class LGFX : public LGFX_Device { 6 | lgfx::Panel_sdl _panel_instance; 7 | 8 | bool init_impl(bool use_reset, bool use_clear) { return LGFX_Device::init_impl(false, use_clear); } 9 | 10 | public: 11 | LGFX(int width = 320, int height = 240, uint_fast8_t scaling_x = 0, uint_fast8_t scaling_y = 0) { 12 | auto cfg = _panel_instance.config(); 13 | cfg.memory_width = width; 14 | cfg.panel_width = width; 15 | cfg.memory_height = height; 16 | cfg.panel_height = height; 17 | _panel_instance.config(cfg); 18 | if (scaling_x == 0) { 19 | scaling_x = 1; 20 | } 21 | if (scaling_y == 0) { 22 | scaling_y = scaling_x; 23 | } 24 | _panel_instance.setScaling(scaling_x, scaling_y); 25 | setPanel(&_panel_instance); 26 | // _board = board_t::board_unknown; 27 | } 28 | }; 29 | 30 | //---------------------------------------------------------------------------- 31 | } 32 | } 33 | 34 | using LGFX = lgfx::LGFX; 35 | -------------------------------------------------------------------------------- /src/MacroItem.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Menu.h" 5 | 6 | class MacroItem : public Item { 7 | private: 8 | std::string _filename; 9 | 10 | public: 11 | MacroItem(const char* name, std::string filename) : Item(name), _filename(filename) {} 12 | void invoke(void* arg) override; 13 | void show(const Point& where) override; 14 | }; 15 | -------------------------------------------------------------------------------- /src/MacroMenu.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Menu.h" 5 | #include "MacroItem.h" 6 | #include "polar.h" 7 | #include "FileParser.h" 8 | 9 | extern Scene statusScene; 10 | extern Scene filePreviewScene; 11 | 12 | void MacroItem::invoke(void* arg) { 13 | if (arg && strcmp((char*)arg, "Run") == 0) { 14 | send_linef("$Localfs/Run=%s", _filename.c_str()); 15 | } else { 16 | push_scene(&filePreviewScene, (void*)_filename.c_str()); 17 | // doFileScreen(_name); 18 | } 19 | } 20 | void MacroItem::show(const Point& where) { 21 | int color = WHITE; 22 | 23 | std::string extra(_filename); 24 | if (extra.rfind("/localfs", 0) == 0) { 25 | extra.erase(0, strlen("/localfs")); 26 | } else if (extra.rfind("cmd:", 0) == 0) { 27 | extra.erase(0, strlen("cmd:")); 28 | } 29 | 30 | if (_highlighted) { 31 | drawRect(where, Point { 200, 50 }, 15, color); 32 | text(name(), where + Point { 0, 6 }, BLACK, MEDIUM, middle_center); 33 | text(extra, where - Point { 0, 16 }, BLACK, TINY, middle_center); 34 | } else { 35 | text(name(), where, WHITE, SMALL, middle_center); 36 | } 37 | } 38 | 39 | class MacroMenu : public Menu { 40 | private: 41 | bool _reading = true; 42 | std::string _error_string; 43 | 44 | public: 45 | MacroMenu() : Menu("Macros") {} 46 | 47 | const std::string& selected_name() { return _items[_selected]->name(); } 48 | 49 | void refreshMacros() { 50 | removeAllItems(); 51 | _reading = true; 52 | request_macros(); 53 | } 54 | 55 | void onRedButtonPress() { refreshMacros(); } 56 | void onFilesList() { 57 | _error_string.clear(); 58 | _reading = false; 59 | if (num_items()) { 60 | _selected = 0; 61 | _items[_selected]->highlight(); 62 | } 63 | reDisplay(); 64 | } 65 | 66 | void onError(const char* errstr) { 67 | _error_string = errstr; 68 | _reading = false; 69 | reDisplay(); 70 | } 71 | 72 | void onEntry(void* arg) override { 73 | if (num_items() == 0) { 74 | refreshMacros(); 75 | } 76 | } 77 | 78 | void onDialButtonPress() { 79 | if (num_items()) { 80 | invoke((void*)"Run"); 81 | } 82 | } 83 | 84 | void onGreenButtonPress() { 85 | if (state != Idle) { 86 | return; 87 | } 88 | if (num_items()) { 89 | invoke(); 90 | } 91 | } 92 | 93 | void onTouchClick() { onGreenButtonPress(); } 94 | 95 | void reDisplay() override { 96 | menuBackground(); 97 | if (num_items() == 0) { 98 | // Point where { 0, 0 }; 99 | // Point wh { 200, 45 }; 100 | // drawRect(where, wh, 20, YELLOW); 101 | if (_error_string.length()) { 102 | text(_error_string, 120, 120, WHITE, SMALL, middle_center); 103 | } else { 104 | text(_reading ? "Reading Macros" : "No Macros", { 0, 0 }, WHITE, SMALL, middle_center); 105 | } 106 | } else { 107 | if (_selected > 1) { 108 | _items[_selected - 2]->show({ 0, 80 }); 109 | } 110 | if (_selected > 0) { 111 | _items[_selected - 1]->show({ 0, 45 }); 112 | } 113 | _items[_selected]->show({ 0, 0 }); 114 | if (_selected < num_items() - 1) { 115 | _items[_selected + 1]->show({ 0, -45 }); 116 | } 117 | if (_selected < num_items() - 2) { 118 | _items[_selected + 2]->show({ 0, -80 }); 119 | } 120 | } 121 | buttonLegends(); 122 | refreshDisplay(); 123 | } 124 | 125 | void buttonLegends() { 126 | const char* orangeLabel = ""; 127 | const char* grnLabel = ""; 128 | 129 | if (state == Idle) { 130 | if (num_items()) { 131 | orangeLabel = "Run"; 132 | grnLabel = "Load"; 133 | } 134 | } 135 | 136 | drawButtonLegends(_reading ? "" : "Refresh", grnLabel, orangeLabel); 137 | } 138 | 139 | void rotate(int delta) override { 140 | if (_selected == 0 && delta <= 0) { 141 | return; 142 | } 143 | if (_selected == num_items() && delta >= 0) { 144 | return; 145 | } 146 | if (_selected != -1) { 147 | _items[_selected]->unhighlight(); 148 | } 149 | _selected += delta; 150 | if (_selected < 0) { 151 | _selected = 0; 152 | } 153 | if (_selected >= num_items()) { 154 | _selected = num_items() - 1; 155 | } 156 | _items[_selected]->highlight(); 157 | reDisplay(); 158 | } 159 | 160 | int touchedItem(int x, int y) override { return -1; }; 161 | 162 | void onStateChange(state_t old_state) { 163 | if (state == Cycle) { 164 | push_scene(&statusScene); 165 | } 166 | } 167 | 168 | void menuBackground() override { 169 | background(); 170 | 171 | if (num_items()) { 172 | // Draw dot showing the selected file 173 | if (num_items() > 1) { 174 | int span = 100; // degrees 175 | int dtheta = span * _selected / (num_items() - 1); 176 | int theta = (span / 2) - dtheta; 177 | int dx, dy; 178 | r_degrees_to_xy(110, theta, &dx, &dy); 179 | 180 | drawFilledCircle({ dx, dy }, 8, WHITE); 181 | } 182 | } 183 | if (state != Idle) { 184 | drawStatus(); 185 | } 186 | 187 | text("Macros", { 0, 100 }, YELLOW, SMALL); 188 | } 189 | } macroMenu; 190 | -------------------------------------------------------------------------------- /src/MacroMenu.cppxx: -------------------------------------------------------------------------------- 1 | class MacroMenu : public Menu { 2 | private: 3 | int _selected = 0; 4 | 5 | public: 6 | MacroMenu() : Menu("Macros") {} 7 | 8 | const std::string& selected_name(); 9 | void onEntry(void* arg) override; 10 | 11 | void onRedButtonPress() { request_macro_list(); } 12 | void onFilesList() { 13 | _selected = 0; 14 | reDisplay(); 15 | } 16 | 17 | void onDialButtonPress() { pop_scene(); } 18 | 19 | void onGreenButtonPress() { 20 | if (state != Idle) { 21 | return; 22 | } 23 | if (num_items()) { 24 | invoke(); 25 | } 26 | } 27 | void reDisplay() override { 28 | dbg_printf("MM redisp sel %d ni %d\n", _selected, num_items()); 29 | menuBackground(); 30 | if (num_items() == 0) { 31 | Point where { 0, 0 }; 32 | Point wh { 200, 45 }; 33 | drawRect(where, wh, 20, YELLOW); 34 | text("No Files", where, BLACK, MEDIUM, middle_center); 35 | } else { 36 | if (_selected > 1) { 37 | _items[_selected - 2]->show({ 0, 70 }); 38 | } 39 | if (_selected > 0) { 40 | _items[_selected - 1]->show({ 0, 40 }); 41 | } 42 | dbg_println(selected_name()); 43 | _items[_selected]->show({ 0, 0 }); 44 | if (_selected < num_items() - 1) { 45 | _items[_selected + 1]->show({ 0, -40 }); 46 | } 47 | if (_selected < num_items() - 2) { 48 | _items[_selected + 2]->show({ 0, -70 }); 49 | } 50 | } 51 | buttonLegends(); 52 | refreshDisplay(); 53 | } 54 | 55 | void buttonLegends() { 56 | const char* grnLabel = ""; 57 | const char* redLabel = ""; 58 | 59 | if (state == Idle) { 60 | redLabel = "Refresh"; 61 | if (num_items()) { 62 | grnLabel = "Load"; 63 | } 64 | } 65 | 66 | drawButtonLegends(redLabel, grnLabel, "Back"); 67 | } 68 | 69 | void rotate(int delta) override { 70 | if (_selected == 0 && delta <= 0) { 71 | return; 72 | } 73 | if (_selected == num_items() && delta >= 0) { 74 | return; 75 | } 76 | if (_selected != -1) { 77 | _items[_selected]->unhighlight(); 78 | } 79 | _selected += delta; 80 | if (_selected < 0) { 81 | _selected = 0; 82 | } 83 | if (_selected >= num_items()) { 84 | _selected = num_items() - 1; 85 | } 86 | _items[_selected]->highlight(); 87 | reDisplay(); 88 | } 89 | 90 | int touchedItem(int x, int y) override { return -1; }; 91 | 92 | void menuBackground() override { 93 | background(); 94 | drawPngBackground("/filesbg.png"); 95 | 96 | text("Macros", { 0, 100 }, YELLOW, MEDIUM); 97 | 98 | // Draw dot showing the selected file 99 | if (num_items() > 1) { 100 | int span = 100; // degrees 101 | int dtheta = span * _selected / (num_items() - 1); 102 | int theta = (span / 2) - dtheta; 103 | int dx, dy; 104 | r_degrees_to_xy(110, theta, &dx, &dy); 105 | 106 | drawFilledCircle({ dx, dy }, 8, WHITE); 107 | } 108 | } 109 | } macroMenu; 110 | -------------------------------------------------------------------------------- /src/Menu.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Menu.h" 5 | #include "System.h" 6 | #include "Drawing.h" 7 | 8 | void do_nothing(void* foo) {} 9 | 10 | void RoundButton::show(const Point& where) { 11 | drawOutlinedCircle(where, _radius, _highlighted ? _hl_fill_color : _fill_color, _highlighted ? _hl_outline_color : _outline_color); 12 | text(name().substr(0, 1), where, _highlighted ? MAROON : WHITE, MEDIUM); 13 | } 14 | void ImageButton::show(const Point& where) { 15 | if (_highlighted) { 16 | drawFilledCircle(where, _radius + 3, _disabled ? DARKGREY : _outline_color); 17 | } else { 18 | drawFilledCircle(where, _radius - 2, _disabled ? DARKGREY : LIGHTGREY); 19 | } 20 | drawPngFile(_filename, where); 21 | } 22 | void RectangularButton::show(const Point& where) { 23 | drawOutlinedRect(where, _width, _height, _highlighted ? BLUE : _outline_color, _bg_color); 24 | text(_text, where, _text_color, SMALL); 25 | } 26 | 27 | void Menu::removeAllItems() { 28 | for (auto const& item : _items) { 29 | delete item; 30 | } 31 | _items.clear(); 32 | _positions.clear(); 33 | _num_items = 0; 34 | } 35 | 36 | void Menu::reDisplay() { 37 | menuBackground(); 38 | show_items(); 39 | refreshDisplay(); 40 | } 41 | void Menu::rotate(int delta) { 42 | if (_selected != -1) { 43 | _items[_selected]->unhighlight(); 44 | } 45 | 46 | int previous = _selected; 47 | do { 48 | _selected += delta; 49 | while (_selected < 0) { 50 | _selected += _num_items; 51 | } 52 | while (_selected >= _num_items) { 53 | _selected -= _num_items; 54 | } 55 | if (!_items[_selected]->hidden()) { 56 | break; 57 | } 58 | // If we land on a hidden item, move to the next item in the 59 | // same direction. 60 | delta = delta < 0 ? -1 : 1; 61 | } while (_selected != previous); 62 | 63 | _items[_selected]->highlight(); 64 | reDisplay(); 65 | } 66 | -------------------------------------------------------------------------------- /src/Menu.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include "Scene.h" 7 | #include 8 | #include 9 | #include 10 | 11 | extern Scene helpScene; 12 | 13 | typedef int color_t; 14 | typedef void (*callback_t)(void*); 15 | 16 | void do_nothing(void* arg); 17 | class Item { 18 | protected: 19 | std::string _name; 20 | 21 | bool _highlighted = false; 22 | bool _disabled = false; 23 | bool _hidden = false; 24 | callback_t _callback = nullptr; 25 | Scene* _scene = nullptr; 26 | 27 | public: 28 | Item(const char* name, callback_t callback = do_nothing) : _name(name), _callback(callback) {} 29 | Item(const char* name, Scene* scene) : _name(name), _callback(nullptr), _scene(scene) {} 30 | Item() : Item("") {} 31 | 32 | // Virtual so we can delete derived classes via pointer 33 | virtual ~Item() {} 34 | 35 | virtual void show(const Point& where) {}; 36 | 37 | virtual void invoke(void* arg = nullptr) { 38 | if (_disabled || _hidden) { 39 | return; 40 | } 41 | if (_scene) { 42 | push_scene(_scene, arg); 43 | return; 44 | } 45 | if (_callback) { 46 | _callback(arg); 47 | return; 48 | } 49 | }; 50 | 51 | const std::string& name() { return _name; } 52 | 53 | void highlight() { _highlighted = true; } 54 | void unhighlight() { _highlighted = false; } 55 | bool highlighted() { return _highlighted; } 56 | void disable() { _disabled = true; } 57 | void enable() { _disabled = false; } 58 | bool enabled() { return !_disabled; } 59 | bool disabled() { return _disabled; } 60 | void hide() { _hidden = true; } 61 | void unhide() { _hidden = false; } 62 | bool hidden() { return _hidden; } 63 | 64 | void set_action(callback_t callback) { _callback = callback; } 65 | }; 66 | 67 | class EmptyItem : public Item { 68 | public: 69 | EmptyItem() { hide(); } 70 | }; 71 | 72 | class RoundButton : public Item { 73 | private: 74 | color_t _hl_fill_color; 75 | color_t _outline_color; 76 | color_t _hl_outline_color; 77 | 78 | public: 79 | int _radius; 80 | color_t _fill_color; 81 | RoundButton(const char* name, 82 | callback_t callback, 83 | int radius, 84 | color_t fill_color, 85 | color_t hl_fill_color, 86 | color_t outline_color, 87 | color_t hl_outline_color) : 88 | Item(name, callback), 89 | _radius(radius), _fill_color(fill_color), _hl_fill_color(hl_fill_color), _outline_color(outline_color), 90 | _hl_outline_color(hl_outline_color) {} 91 | RoundButton( 92 | const char* name, Scene* scene, int radius, color_t fill_color, color_t hl_fill_color, color_t outline_color, color_t hl_outline_color) : 93 | Item(name, scene), 94 | _radius(radius), _fill_color(fill_color), _hl_fill_color(hl_fill_color), _outline_color(outline_color), 95 | _hl_outline_color(hl_outline_color) {} 96 | void show(const Point& where) override; 97 | }; 98 | 99 | class ImageButton : public Item { 100 | private: 101 | const char* _filename; 102 | int _radius; 103 | color_t _outline_color; 104 | 105 | public: 106 | ImageButton(const char* name, callback_t callback, const char* filename, int radius, color_t outline_color = WHITE) : 107 | Item(name, callback), _filename(filename), _radius(radius), _outline_color(outline_color) {} 108 | 109 | ImageButton(const char* name, Scene* scene, const char* filename, int radius, color_t outline_color = WHITE) : 110 | Item(name, scene), _filename(filename), _radius(radius), _outline_color(outline_color) {} 111 | 112 | void show(const Point& where) override; 113 | }; 114 | 115 | class RectangularButton : public Item { 116 | private: 117 | const char* _text; 118 | int _width; 119 | int _height; 120 | int _radius; 121 | int _bg_color; 122 | int _text_color; 123 | int _outline_color; 124 | 125 | public: 126 | RectangularButton(const char* name, 127 | callback_t callback, 128 | const char* text, 129 | int width, 130 | int height, 131 | int radius, 132 | color_t bg_color, 133 | color_t text_color, 134 | color_t outline_color) : 135 | Item(name, callback), 136 | _text(text), _width(width), _height(height), _radius(radius), _bg_color(bg_color), _text_color(text_color), 137 | _outline_color(outline_color) {} 138 | void show(const Point& where) override; 139 | }; 140 | 141 | class Menu : public Scene { 142 | private: 143 | void show_items() { 144 | for (size_t i = 0; i < _items.size(); ++i) { 145 | _items[i]->show(_positions[i]); 146 | } 147 | _items[_selected]->show(_positions[_selected]); 148 | } 149 | 150 | int _num_items = 0; 151 | 152 | public: 153 | std::vector _positions; 154 | std::vector _items; 155 | 156 | int _selected = 0; 157 | 158 | Menu(const char* name, const char** help_text = nullptr) : Scene(name, 4, help_text) {} 159 | 160 | Menu(const char* name, int num_items, const char** help_text = nullptr) : Scene(name, 4, help_text), _num_items(num_items) { 161 | _items.reserve(num_items); 162 | _positions.reserve(num_items); 163 | } 164 | 165 | Item* selectedItem() { return _items[_selected]; } 166 | 167 | int num_items() { return _num_items; } 168 | 169 | void reDisplay(); 170 | 171 | virtual void menuBackground() {} 172 | virtual int touchedItem(int x, int y) { return -1; } 173 | virtual void rotate(int delta); 174 | 175 | void onEncoder(int delta) override { rotate(delta); } 176 | 177 | void setPosition(int item_num, Point position) { _positions[item_num] = position; } 178 | void setItem(int item_num, Item* item) { _items[item_num] = item; } 179 | void addItem(Item* item, Point position = { 0, 0 }) { 180 | _items.push_back(item); 181 | _positions.push_back(position); 182 | ++_num_items; 183 | } 184 | void removeAllItems(); 185 | 186 | void onEntry(void* arg) override { 187 | if (num_items() && _selected != -1) { 188 | _items[_selected]->highlight(); 189 | } 190 | } 191 | 192 | void select(int item) { 193 | if (item == -1 || item >= _num_items) { 194 | return; 195 | } 196 | if (_items[item]->hidden()) { 197 | return; 198 | } 199 | if (_selected != -1) { 200 | _items[_selected]->unhighlight(); 201 | } 202 | _selected = item; 203 | _items[item]->highlight(); 204 | reDisplay(); 205 | } 206 | void onTouchClick() override { 207 | if (_help_text && touchIsCenter()) { 208 | push_scene(&helpScene), (void*)_help_text; 209 | return; 210 | } 211 | select(touchedItem(touchX, touchY)); 212 | ackBeep(); 213 | } 214 | void invoke(void* arg = nullptr) { _items[_selected]->invoke(arg); } 215 | }; 216 | -------------------------------------------------------------------------------- /src/MenuScene.cpp: -------------------------------------------------------------------------------- 1 | #include "Menu.h" 2 | #include "PieMenu.h" 3 | #ifdef USE_WMB_FSS 4 | # include "FileMenu.h" 5 | #endif 6 | #include "System.h" 7 | 8 | void noop(void* arg) {} 9 | 10 | const int buttonRadius = 30; 11 | 12 | static const char* menu_help_text[] = { "FluidDial", 13 | "Touch icon for scene", 14 | "Touch center for help", 15 | "Flick left to go back", 16 | "Authors: @bdring,@Mitch", 17 | "Bradley,@bDuthieDev,", 18 | "@Design8Studio", 19 | NULL }; 20 | 21 | // PieMenu axisMenu("Axes", buttonRadius); 22 | 23 | class LB : public RoundButton { 24 | public: 25 | LB(const char* text, callback_t callback, color_t base_color) : 26 | RoundButton(text, callback, buttonRadius, base_color, GREEN, BLUE, WHITE) {} 27 | LB(const char* text, Scene* scene, color_t base_color) : RoundButton(text, scene, buttonRadius, base_color, GREEN, BLUE, WHITE) {} 28 | }; 29 | 30 | constexpr int LIGHTYELLOW = 0xFFF0; 31 | class IB : public ImageButton { 32 | public: 33 | IB(const char* text, callback_t callback, const char* filename) : ImageButton(text, callback, filename, buttonRadius, WHITE) {} 34 | IB(const char* text, Scene* scene, const char* filename) : ImageButton(text, scene, filename, buttonRadius, WHITE) {} 35 | }; 36 | 37 | extern Scene homingScene; 38 | //extern Scene joggingScene; 39 | //extern Scene joggingScene2; 40 | extern Scene multiJogScene; 41 | extern Scene probingScene; 42 | extern Scene toolchangeScene; 43 | extern Scene statusScene; 44 | extern Scene macroMenu; 45 | 46 | #ifdef USE_WMB_FSS 47 | extern Scene wmbFileSelectScene; 48 | #else 49 | extern Scene fileSelectScene; 50 | #endif 51 | 52 | Scene& jogScene = multiJogScene; 53 | 54 | extern Scene controlScene; 55 | extern Scene aboutScene; 56 | 57 | IB statusButton("Status", &statusScene, "statustp.png"); 58 | IB homingButton("Homing", &homingScene, "hometp.png"); 59 | IB jogButton("Jog", &jogScene, "jogtp.png"); 60 | IB probeButton("Probe", &probingScene, "probetp.png"); 61 | IB toolchangeButton("Tools", &toolchangeScene, "toolchangetp.png"); 62 | 63 | #ifdef USE_WMB_FSS 64 | IB filesButton("Files", &wmbFileSelectScene, "filestp.png"); 65 | #else 66 | IB filesButton("Files", &fileSelectScene, "filestp.png"); 67 | #endif 68 | 69 | IB controlButton("Macros", ¯oMenu, "macrostp.png"); 70 | IB setupButton("About", &aboutScene, "abouttp.png"); 71 | 72 | class MenuScene : public PieMenu { 73 | public: 74 | MenuScene() : PieMenu("Main", buttonRadius, menu_help_text) {} 75 | void disableIcons() { 76 | statusButton.disable(); 77 | homingButton.disable(); 78 | jogButton.disable(); 79 | probeButton.disable(); 80 | toolchangeButton.disable(); 81 | filesButton.disable(); 82 | controlButton.disable(); 83 | setupButton.enable(); 84 | } 85 | void enableIcons() { 86 | statusButton.enable(); 87 | homingButton.enable(); 88 | jogButton.enable(); 89 | probeButton.enable(); 90 | toolchangeButton.enable(); 91 | filesButton.enable(); 92 | controlButton.enable(); 93 | setupButton.enable(); 94 | } 95 | void onEntry(void* arg) { 96 | PieMenu::onEntry(arg); 97 | if (state == Disconnected) { 98 | disableIcons(); 99 | } else { 100 | enableIcons(); 101 | } 102 | } 103 | void onStateChange(state_t old_state) override { 104 | if (state != Disconnected) { 105 | enableIcons(); 106 | if (old_state == Disconnected) { 107 | #ifdef AUTO_JOG_SCENE 108 | if (state == Idle) { 109 | push_scene(&jogScene); 110 | return; 111 | } 112 | #endif 113 | #ifdef AUTO_HOMING_SCENE 114 | if (state == Alarm && lastAlarm == 14) { // Unknown or Unhomed 115 | push_scene(&homingScene, (void*)"auto"); 116 | return; 117 | } 118 | #endif 119 | } 120 | } 121 | reDisplay(); 122 | } 123 | } menuScene; 124 | 125 | Scene* initMenus() { 126 | menuScene.addItem(&statusButton); 127 | menuScene.addItem(&homingButton); 128 | menuScene.addItem(&jogButton); 129 | menuScene.addItem(&probeButton); 130 | menuScene.addItem(&toolchangeButton); 131 | menuScene.addItem(&filesButton); 132 | menuScene.addItem(&controlButton); 133 | menuScene.addItem(&setupButton); 134 | 135 | return &menuScene; 136 | } 137 | -------------------------------------------------------------------------------- /src/NVS.h: -------------------------------------------------------------------------------- 1 | #ifdef ESP32 2 | # include "nvs_flash.h" 3 | #else 4 | typedef const char* nvs_handle_t; 5 | void nvs_get_str(nvs_handle_t handle, const char* name, char* value, size_t* len); 6 | void nvs_set_str(nvs_handle_t handle, const char* name, const char* value); 7 | void nvs_get_i32(nvs_handle_t handle, const char* name, int* value); 8 | void nvs_set_i32(nvs_handle_t handle, const char* name, int value); 9 | #endif 10 | 11 | nvs_handle_t nvs_init(const char* name); 12 | -------------------------------------------------------------------------------- /src/PieMenu.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "PieMenu.h" 5 | #include "System.h" 6 | #include "Drawing.h" 7 | #include "polar.h" 8 | 9 | void PieMenu::calculatePositions() { 10 | _num_slopes = num_items() / 2; // Rounded down 11 | 12 | _slopes.clear(); 13 | int dtheta = 360 / num_items(); 14 | 15 | int angle = 90 - (dtheta / 2); 16 | for (size_t i = 0; i < _num_slopes; i++) { 17 | int slope = r_degrees_to_slope(1024, angle); 18 | _slopes.push_back(slope); 19 | angle -= dtheta; 20 | } 21 | 22 | int layout_radius = display_short_side() / 2 - _item_radius - 3; 23 | angle = 90; 24 | for (size_t i = 0; i < num_items(); i++) { 25 | int x, y; 26 | r_degrees_to_xy(layout_radius, angle, &x, &y); 27 | Point center { x, y }; 28 | setPosition(i, center); 29 | angle -= dtheta; 30 | } 31 | } 32 | 33 | int PieMenu::touchedItem(int x, int y) { 34 | // Convert from screen coordinates to 0,0 in the center 35 | Point ctr = Point { x, y }.from_display(); 36 | 37 | fnc_realtime(StatusReport); // used to update if status is out of sync 38 | 39 | x = ctr.x; 40 | y = ctr.y; 41 | 42 | int dead_radius = display_short_side() / 2 - _item_radius * 2; 43 | 44 | if ((x * x + y * y) < (dead_radius * dead_radius)) { 45 | return -1; // In middle dead zone 46 | } 47 | if (x == 0) { 48 | x = 1; // Don't divide by zero 49 | } 50 | int slope = y * 1024 / x; 51 | if (x < 0) { 52 | slope = -slope; 53 | } 54 | if (slope > _slopes[0]) { 55 | // Top item 56 | return 0; 57 | } 58 | // Side items 59 | size_t i; 60 | for (i = 1; i < _num_slopes; ++i) { 61 | if (slope > _slopes[i]) { 62 | return x > 0 ? i : num_items() - i; 63 | } 64 | } 65 | // If num_items() is even, return the bottom item (i == num_items()-i) 66 | // If it is odd, return one of two bottom items stradding -Y axis 67 | 68 | reDisplay(); 69 | return x > 0 ? i : num_items() - i; 70 | } 71 | void PieMenu::menuBackground() { 72 | background(); 73 | text(selectedItem()->name(), { 0, -15 }, WHITE, SMALL); 74 | drawStatusSmall(90); 75 | } 76 | 77 | void PieMenu::onTouchFlick() { 78 | int item = touchedItem(touchX, touchY); 79 | if (item != -1) { 80 | select(item); 81 | ackBeep(); 82 | invoke(); 83 | } 84 | } 85 | void PieMenu::onDialButtonPress() { 86 | invoke(); 87 | } 88 | 89 | void PieMenu::onTouchHold() { 90 | int item = touchedItem(touchX, touchY); 91 | if (item != -1) { 92 | select(item); 93 | } 94 | } 95 | 96 | void PieMenu::onTouchClick() { 97 | if (_help_text && touchIsCenter()) { 98 | push_scene(&helpScene, (void*)_help_text); 99 | return; 100 | } 101 | 102 | int item = touchedItem(touchX, touchY); 103 | if (item != -1) { 104 | select(item); 105 | invoke(); 106 | } 107 | } 108 | 109 | void PieMenu::onStateChange(state_t old_state) { 110 | reDisplay(); 111 | } 112 | -------------------------------------------------------------------------------- /src/PieMenu.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include "Menu.h" 7 | 8 | // Pie menus were originally invented by Don Hopkins at Sun Microsystems 9 | class PieMenu : public Menu { 10 | private: 11 | int _item_radius; 12 | int _num_slopes; 13 | 14 | std::vector _slopes; // Slopes of lines dividing switch positions 15 | 16 | public: 17 | PieMenu(const char* name, int item_radius, const char** help_text = nullptr) : Menu(name, help_text), _item_radius(item_radius) {} 18 | PieMenu(const char* name, int item_radius, int num_items, const char** help_text = nullptr) : 19 | Menu(name, num_items, help_text), _item_radius(item_radius) { 20 | calculatePositions(); 21 | } 22 | void menuBackground() override; 23 | void calculatePositions(); 24 | void onEncoder(int delta) override { Menu::onEncoder(delta); } 25 | void onTouchHold() override; 26 | void onTouchClick() override; 27 | void onTouchFlick() override; 28 | void onDialButtonPress() override; 29 | void addItem(Item* item) { 30 | Menu::addItem(item); 31 | calculatePositions(); 32 | } 33 | int touchedItem(int x, int y) override; 34 | void onStateChange(state_t old_state) override; 35 | }; 36 | -------------------------------------------------------------------------------- /src/Point.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Point.h" 5 | #include "System.h" 6 | 7 | Point Point::to_display() const { 8 | int center = display_short_side() / 2; 9 | return { center + x, center - y }; 10 | } 11 | Point Point::from_display() const { 12 | int center = display_short_side() / 2; 13 | return { x - center, center - y }; 14 | } 15 | -------------------------------------------------------------------------------- /src/Point.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Point { 4 | public: 5 | int x; 6 | int y; 7 | 8 | void operator+=(Point const& other) { 9 | x += other.x; 10 | y += other.y; 11 | } 12 | void operator+=(int addend) { 13 | x += addend; 14 | y += addend; 15 | } 16 | 17 | void operator-=(Point const& other) { 18 | x -= other.x; 19 | y -= other.y; 20 | } 21 | void operator-=(int subtrahend) { 22 | x -= subtrahend; 23 | y -= subtrahend; 24 | } 25 | 26 | void operator/=(Point const& other) { 27 | x /= other.x; 28 | y /= other.y; 29 | } 30 | 31 | void operator/=(int divisor) { 32 | x /= divisor; 33 | y /= divisor; 34 | } 35 | 36 | void operator*=(Point const& other) { 37 | x *= other.x; 38 | y *= other.y; 39 | } 40 | 41 | void operator*=(int multiplier) { 42 | x *= multiplier; 43 | y *= multiplier; 44 | } 45 | 46 | friend Point operator+(Point const& a, Point const& b) { 47 | Point ret(a); 48 | 49 | ret += b; 50 | return ret; 51 | } 52 | friend Point operator+(Point const& a, int b) { 53 | Point ret(a); 54 | 55 | ret += b; 56 | return ret; 57 | } 58 | 59 | friend Point operator-(Point const& a, Point const& b) { 60 | Point ret(a); 61 | 62 | ret -= b; 63 | return ret; 64 | } 65 | 66 | friend Point operator-(Point const& a, int b) { 67 | Point ret(a); 68 | 69 | ret -= b; 70 | return ret; 71 | } 72 | 73 | friend Point operator/(Point const& a, Point const& b) { 74 | Point ret(a); 75 | 76 | ret /= b; 77 | return ret; 78 | } 79 | friend Point operator/(Point const& a, int divisor) { 80 | Point ret(a); 81 | 82 | ret /= divisor; 83 | return ret; 84 | } 85 | 86 | friend Point operator*(Point const& a, Point const& b) { 87 | Point ret(a); 88 | 89 | ret *= b; 90 | return ret; 91 | } 92 | friend Point operator*(Point const& a, int multiplier) { 93 | Point ret(a); 94 | 95 | ret *= multiplier; 96 | return ret; 97 | } 98 | 99 | friend bool operator==(Point const& a, Point const& b) { return a.x == b.x && a.y == b.y; } 100 | 101 | Point to_display() const; 102 | Point from_display() const; 103 | 104 | Point() : x(0), y(0) {} 105 | 106 | Point(int x_, int y_) { 107 | x = x_; 108 | y = y_; 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /src/ProbingScene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Barton Dring 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include 5 | #include "Scene.h" 6 | #include "e4math.h" 7 | 8 | class ProbingScene : public Scene { 9 | private: 10 | int selection = 0; 11 | long oldPosition = 0; 12 | 13 | // Saved to NVS 14 | e4_t _offset = e4_from_int(0); 15 | int _travel = -20; 16 | int _rate = 80; 17 | int _retract = 20; 18 | int _axis = 2; // Z is default 19 | 20 | public: 21 | ProbingScene() : Scene("Probe") {} 22 | 23 | void onDialButtonPress() { pop_scene(); } 24 | 25 | void onGreenButtonPress() { 26 | // G38.2 G91 F80 Z-20 P8.00 27 | switch (state) { 28 | case Idle: 29 | send_linef("G38.2G91F%d%c%dP%s", _rate, axisNumToChar(_axis), _travel, e4_to_cstr(_offset, 2)); 30 | break; 31 | case Cycle: 32 | fnc_realtime(FeedHold); 33 | break; 34 | case Hold: 35 | case DoorClosed: 36 | fnc_realtime(CycleStart); 37 | break; 38 | case Alarm: 39 | send_line("$X"); // unlock 40 | break; 41 | } 42 | } 43 | 44 | void onRedButtonPress() { 45 | // G38.2 G91 F80 Z-20 P8.00 46 | if (state == Cycle || state == Alarm) { 47 | fnc_realtime(Reset); 48 | return; 49 | } else if (state == Idle) { 50 | int retract = _travel < 0 ? _retract : -_retract; 51 | send_linef("$J=G91F1000%c%d", axisNumToChar(_axis), retract); 52 | return; 53 | } else if (state == Hold || state == DoorClosed) { 54 | fnc_realtime(Reset); 55 | } 56 | } 57 | 58 | void onTouchClick() { 59 | // Rotate through the items to be adjusted. 60 | rotateNumberLoop(selection, 1, 0, 4); 61 | reDisplay(); 62 | ackBeep(); 63 | } 64 | 65 | void onDROChange() { reDisplay(); } 66 | 67 | void onEncoder(int delta) { 68 | if (abs(delta) > 0) { 69 | switch (selection) { 70 | case 0: 71 | _offset += delta * 100; // Increment by 0.0100 72 | setPref("Offset", _offset); 73 | break; 74 | case 1: 75 | _travel += delta; 76 | setPref("Travel", _travel); 77 | break; 78 | case 2: 79 | _rate += delta; 80 | if (_rate < 1) { 81 | _rate = 1; 82 | } 83 | setPref("Rate", _rate); 84 | break; 85 | case 3: 86 | _retract += delta; 87 | if (_retract < 0) { 88 | _retract = 0; 89 | } 90 | setPref("Retract", _retract); 91 | break; 92 | case 4: 93 | rotateNumberLoop(_axis, 1, 0, 2); 94 | setPref("Axis", _axis); 95 | } 96 | reDisplay(); 97 | } 98 | } 99 | void onEntry(void* arg) override { 100 | if (initPrefs()) { 101 | static_assert(sizeof(e4_t) == sizeof(int)); 102 | getPref("Offset", reinterpret_cast(&_offset)); 103 | getPref("Travel", &_travel); 104 | getPref("Rate", &_rate); 105 | getPref("Retract", &_retract); 106 | getPref("Axis", &_axis); 107 | } 108 | } 109 | 110 | void reDisplay() { 111 | background(); 112 | drawMenuTitle(current_scene->name()); 113 | drawStatus(); 114 | 115 | const char* grnLabel = ""; 116 | const char* redLabel = ""; 117 | 118 | if (state == Idle) { 119 | int x = 40; 120 | int y = 62; 121 | int width = display_short_side() - (x * 2); 122 | int height = 25; 123 | int pitch = 27; // for spacing of buttons 124 | Stripe button(x, y, width, height, TINY); 125 | button.draw("Offset", e4_to_cstr(_offset, 2), selection == 0); 126 | button.draw("Max Travel", intToCStr(_travel), selection == 1); 127 | y = button.y(); // For LED 128 | button.draw("Feed Rate", intToCStr(_rate), selection == 2); 129 | 130 | button.draw("Retract", intToCStr(_retract), selection == 3); 131 | button.draw("Axis", axisNumToCStr(_axis), selection == 4); 132 | 133 | //LED led(x - 20, y + height / 2, 10, button.gap()); 134 | //led.draw(myProbeSwitch); 135 | 136 | grnLabel = "Probe"; 137 | redLabel = "Retract"; 138 | } else { 139 | if (state == Jog || state == Alarm) { // there is no Probing state, so Cycle is a valid state on this 140 | //centered_text("Invalid State", 105, WHITE, MEDIUM); 141 | //centered_text("For Probing", 145, WHITE, MEDIUM); 142 | redLabel = "Reset"; 143 | grnLabel = "Unlock"; 144 | } else { 145 | int x = 14; 146 | int height = 35; 147 | int y = 82 - height / 2; 148 | 149 | LED led(120, 190, 10, 5); 150 | led.draw(myProbeSwitch); 151 | 152 | int width = display_short_side() - x * 2; 153 | DRO dro(x, y, width, height); 154 | dro.draw(0, _axis == 0); 155 | dro.draw(1, _axis == 1); 156 | dro.draw(2, _axis == 2); 157 | 158 | switch (state) { 159 | case Cycle: 160 | redLabel = "E-Stop"; 161 | grnLabel = "Hold"; 162 | break; 163 | case Hold: 164 | case DoorClosed: 165 | redLabel = "Reset"; 166 | grnLabel = "Resume"; 167 | break; 168 | } 169 | } 170 | } 171 | 172 | drawButtonLegends(redLabel, grnLabel, "Back"); 173 | drawError(); // only if one just happened 174 | refreshDisplay(); 175 | } 176 | }; 177 | ProbingScene probingScene; 178 | -------------------------------------------------------------------------------- /src/Scene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Scene.h" 5 | #include "System.h" 6 | 7 | #ifndef ARDUINO 8 | # include 9 | # include 10 | #endif 11 | #include 12 | 13 | extern Scene homingScene; 14 | extern Scene statusScene; 15 | 16 | Scene* current_scene = nullptr; 17 | 18 | int touchX; 19 | int touchY; 20 | int touchDeltaX; 21 | int touchDeltaY; 22 | 23 | std::vector scene_stack; 24 | 25 | void activate_scene(Scene* scene, void* arg) { 26 | if (current_scene) { 27 | current_scene->onExit(); 28 | } 29 | current_scene = scene; 30 | current_scene->onEntry(arg); 31 | current_scene->reDisplay(); 32 | } 33 | void push_scene(Scene* scene, void* arg) { 34 | scene_stack.push_back(current_scene); 35 | activate_scene(scene, arg); 36 | } 37 | void pop_scene(void* arg) { 38 | if (scene_stack.size()) { 39 | Scene* last_scene = scene_stack.back(); 40 | scene_stack.pop_back(); 41 | activate_scene(last_scene, arg); 42 | } 43 | } 44 | void activate_at_top_level(Scene* scene, void* arg) { 45 | scene_stack.clear(); 46 | activate_scene(scene, arg); 47 | } 48 | Scene* parent_scene() { 49 | return scene_stack.size() ? scene_stack.back() : nullptr; 50 | } 51 | 52 | bool touchIsCenter() { 53 | // Convert from screen coordinates to 0,0 in the center 54 | Point ctr = Point { touchX, touchY }.from_display(); 55 | 56 | int center_radius = display_short_side() / 6; 57 | 58 | return (ctr.x * ctr.x + ctr.y * ctr.y) < (center_radius * center_radius); 59 | } 60 | 61 | void dispatch_button(bool pressed, int button) { 62 | switch (button) { 63 | case 0: 64 | if (pressed) { 65 | current_scene->onRedButtonPress(); 66 | } else { 67 | current_scene->onRedButtonRelease(); 68 | } 69 | break; 70 | case 1: 71 | if (pressed) { 72 | current_scene->onDialButtonPress(); 73 | } else { 74 | current_scene->onDialButtonRelease(); 75 | } 76 | break; 77 | case 2: 78 | if (pressed) { 79 | current_scene->onGreenButtonPress(); 80 | } else { 81 | current_scene->onGreenButtonRelease(); 82 | } 83 | break; 84 | default: 85 | break; 86 | } 87 | } 88 | void dispatch_touch() { 89 | static m5::touch_state_t last_touch_state = {}; 90 | 91 | auto t = touch.getDetail(); 92 | if (t.state != last_touch_state) { 93 | last_touch_state = t.state; 94 | touchX = t.x - sprite_offset.x; 95 | touchY = t.y - sprite_offset.y; 96 | int delta; 97 | if (screen_encoder(t.x, t.y, delta) && t.state == m5::touch_state_t::touch) { 98 | current_scene->onEncoder(delta); 99 | return; 100 | } 101 | int button; 102 | if (screen_button_touched(t.state == m5::touch_state_t::touch, t.x, t.y, button)) { 103 | if (t.state == m5::touch_state_t::touch) { 104 | dispatch_button(true, button); 105 | } else if (t.state == m5::touch_state_t::none) { 106 | dispatch_button(false, button); 107 | } 108 | return; 109 | } 110 | if (touchX < 0) { 111 | return; 112 | } 113 | if (t.state == m5::touch_state_t::touch) { 114 | current_scene->onTouchPress(); 115 | } else if (t.state == m5::touch_state_t::none) { 116 | current_scene->onTouchRelease(); 117 | } 118 | if (t.wasClicked()) { 119 | current_scene->onTouchClick(); 120 | } else if (t.wasHold()) { 121 | current_scene->onTouchHold(); 122 | } else if (t.state == m5::touch_state_t::flick_end) { 123 | touchDeltaX = t.distanceX(); 124 | touchDeltaY = t.distanceY(); 125 | 126 | int absX = abs(touchDeltaX); 127 | int absY = abs(touchDeltaY); 128 | if (absY > 60 && absX < (absY * 2)) { 129 | if (touchDeltaY > 0) { 130 | current_scene->onDownFlick(); 131 | } else { 132 | current_scene->onUpFlick(); 133 | } 134 | } else if (absX > 60 && absY < (absX * 2)) { 135 | if (touchDeltaX > 0) { 136 | current_scene->onRightFlick(); 137 | } else { 138 | current_scene->onLeftFlick(); 139 | } 140 | } else { 141 | current_scene->onTouchFlick(); 142 | } 143 | } 144 | } 145 | } 146 | 147 | ActionHandler action = nullptr; 148 | 149 | void schedule_action(ActionHandler _action) { 150 | action = _action; 151 | } 152 | 153 | void dispatch_events() { 154 | if (!ui_locked()) { 155 | update_events(); 156 | 157 | static int16_t oldEncoder = 0; 158 | int16_t newEncoder = get_encoder(); 159 | int16_t encoderDelta = newEncoder - oldEncoder; 160 | if (encoderDelta) { 161 | oldEncoder = newEncoder; 162 | 163 | int16_t scaledDelta = current_scene->scale_encoder(encoderDelta); 164 | if (scaledDelta) { 165 | current_scene->onEncoder(scaledDelta); 166 | } 167 | } 168 | 169 | bool pressed; 170 | int button; 171 | if (switch_button_touched(pressed, button)) { 172 | dispatch_button(pressed, button); 173 | } 174 | 175 | dispatch_touch(); 176 | } 177 | 178 | if (!fnc_is_connected()) { 179 | if (state != Disconnected) { 180 | set_disconnected_state(); 181 | extern Scene menuScene; 182 | activate_at_top_level(&menuScene); 183 | } 184 | } 185 | if (action) { 186 | action(); 187 | action = nullptr; 188 | } 189 | } 190 | 191 | static const char* setting_name(const char* base_name, int axis) { 192 | static char name[32]; 193 | if (axis == -1) { 194 | return base_name; 195 | } 196 | sprintf(name, "%s%c", base_name, axisNumToChar(axis)); 197 | return name; 198 | } 199 | 200 | void Scene::setPref(const char* name, int value) { 201 | setPref(name, -1, value); 202 | } 203 | void Scene::getPref(const char* name, int* value) { 204 | getPref(name, -1, value); 205 | } 206 | 207 | void Scene::setPref(const char* base_name, int axis, int value) { 208 | if (!_prefs) { 209 | return; 210 | } 211 | nvs_set_i32(_prefs, setting_name(base_name, axis), value); 212 | } 213 | void Scene::getPref(const char* base_name, int axis, int* value) { 214 | if (!_prefs) { 215 | return; 216 | } 217 | nvs_get_i32(_prefs, setting_name(base_name, axis), reinterpret_cast(value)); 218 | } 219 | void Scene::setPref(const char* base_name, int axis, const char* value) { 220 | if (!_prefs) { 221 | return; 222 | } 223 | nvs_set_str(_prefs, setting_name(base_name, axis), value); 224 | } 225 | void Scene::getPref(const char* base_name, int axis, char* value, int maxlen) { 226 | if (!_prefs) { 227 | return; 228 | } 229 | size_t len = maxlen; 230 | nvs_get_str(_prefs, setting_name(base_name, axis), value, &len); 231 | } 232 | bool Scene::initPrefs() { 233 | if (_prefs) { 234 | return false; // Already open 235 | } 236 | _prefs = nvs_init(name()); 237 | return _prefs; 238 | } 239 | 240 | int Scene::scale_encoder(int delta) { 241 | _encoder_accum += delta; 242 | int res = _encoder_accum / _encoder_scale; 243 | _encoder_accum %= _encoder_scale; 244 | return res; 245 | } 246 | 247 | void Scene::background() { 248 | system_background(); 249 | } 250 | 251 | void act_on_state_change() { 252 | current_scene->onStateChange(previous_state); 253 | } 254 | -------------------------------------------------------------------------------- /src/Scene.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include "GrblParserC.h" 7 | #include "Drawing.h" 8 | #include "NVS.h" 9 | #include 10 | 11 | void pop_scene(void* arg = nullptr); 12 | 13 | extern int touchX; 14 | extern int touchY; 15 | extern int touchDeltaX; 16 | extern int touchDeltaY; 17 | 18 | class Scene { 19 | private: 20 | const char* _name; 21 | 22 | nvs_handle_t _prefs {}; 23 | 24 | int _encoder_accum = 0; 25 | int _encoder_scale = 1; 26 | 27 | protected: 28 | const char** _help_text = nullptr; 29 | 30 | public: 31 | Scene(const char* name, int encoder_scale = 1, const char** help_text = nullptr) : 32 | _name(name), _help_text(help_text), _encoder_scale(encoder_scale) {} 33 | 34 | const char* name() { return _name; } 35 | 36 | virtual void onRedButtonPress() {} 37 | virtual void onRedButtonRelease() {} 38 | virtual void onGreenButtonPress() {} 39 | virtual void onGreenButtonRelease() {} 40 | virtual void onDialButtonPress() {} 41 | virtual void onDialButtonRelease() {} 42 | virtual void onTouchPress() {} 43 | virtual void onTouchRelease() {} 44 | virtual void onTouchClick() {} 45 | virtual void onTouchHold() {} 46 | virtual void onLeftFlick() { pop_scene(); } 47 | virtual void onRightFlick() { onTouchFlick(); } 48 | virtual void onUpFlick() { onTouchFlick(); } 49 | virtual void onDownFlick() { onTouchFlick(); } 50 | virtual void onTouchFlick() {} 51 | 52 | virtual void onError(const char* errstr) {} 53 | 54 | virtual void onStateChange(state_t) {} 55 | virtual void onDROChange() {} 56 | virtual void onLimitsChange() {} 57 | virtual void onMessage(char* command, char* arguments) {} 58 | virtual void onEncoder(int delta) {} 59 | virtual void reDisplay() {} 60 | virtual void onEntry(void* arg = nullptr) {} 61 | virtual void onExit() {} 62 | 63 | virtual void onFileLines(int firstline, const std::vector& lines) {} 64 | virtual void onFilesList() {} 65 | 66 | bool initPrefs(); 67 | 68 | int scale_encoder(int delta); 69 | 70 | void setPref(const char* name, int value); 71 | void getPref(const char* name, int* value); 72 | void setPref(const char* name, int axis, int value); 73 | void getPref(const char* name, int axis, int* value); 74 | void setPref(const char* name, int axis, const char* value); 75 | void getPref(const char* name, int axis, char* value, int maxlen); 76 | 77 | void background(); 78 | }; 79 | 80 | bool touchIsCenter(); 81 | 82 | void activate_at_top_level(Scene* scene, void* arg = nullptr); 83 | void activate_scene(Scene* scene, void* arg = nullptr); 84 | void push_scene(Scene* scene, void* arg = nullptr); 85 | Scene* parent_scene(); 86 | 87 | // helper functions 88 | 89 | // Function to rotate through an aaray of numbers 90 | // Example: rotateNumberLoop(variable, 1, 0, 2) 91 | // The variable can be integer or float. If it is float, you need 92 | // to cast increment, min, and max to float in the calls, otherwise 93 | // the compiler will try to use double and complain 94 | 95 | template 96 | void rotateNumberLoop(T& currentVal, T increment, T min, T max) { 97 | currentVal += increment; 98 | if (currentVal > max) { 99 | currentVal = min; 100 | } 101 | if (currentVal < min) { 102 | currentVal = max; 103 | } 104 | } 105 | 106 | // schedule_action() defers a function call until the 107 | // event dispatcher loop runs. That is useful for 108 | // avoiding recursion in FileParser.cpp 109 | typedef void (*ActionHandler)(void); 110 | void schedule_action(ActionHandler action); 111 | 112 | extern Scene* current_scene; 113 | 114 | void dispatch_events(); 115 | void act_on_state_change(); 116 | -------------------------------------------------------------------------------- /src/StatusScene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Barton Dring 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Scene.h" 5 | 6 | extern Scene menuScene; 7 | 8 | class StatusScene : public Scene { 9 | private: 10 | const char* _entry = nullptr; 11 | 12 | // the fro/sro/rt rotating display state 13 | typedef enum { 14 | FRO, 15 | SRO, 16 | RT_FEED_SPEED, 17 | } ovrd_display_t; 18 | 19 | ovrd_display_t overd_display = FRO; 20 | 21 | public: 22 | StatusScene() : Scene("Status") {} 23 | 24 | void onExit() override {} 25 | 26 | void onDialButtonPress() { 27 | if (state == Cycle || state == Hold) { 28 | if (overd_display == FRO) 29 | fnc_realtime(FeedOvrReset); 30 | else if (overd_display == SRO) 31 | fnc_realtime(SpindleOvrReset); 32 | } else { 33 | pop_scene(); 34 | } 35 | } 36 | 37 | void onStateChange(state_t old_state) { 38 | if (old_state == Cycle && state == Idle && parent_scene() != &menuScene) { 39 | pop_scene(); 40 | } 41 | } 42 | 43 | void onTouchClick() { 44 | if (touchY > 150 && (state == Cycle || state == Hold)) { 45 | switch (overd_display) { 46 | case FRO: 47 | overd_display = SRO; 48 | break; 49 | case SRO: 50 | overd_display = RT_FEED_SPEED; 51 | break; 52 | case RT_FEED_SPEED: 53 | overd_display = FRO; 54 | } 55 | reDisplay(); 56 | } 57 | fnc_realtime(StatusReport); // sometimes you want an extra status 58 | } 59 | 60 | void onRedButtonPress() { 61 | switch (state) { 62 | case Alarm: 63 | if (alarm_is_critical()) { 64 | // Critical alarm that must be hard-cleared with a CTRL-X reset 65 | // since streaming execution of GCode is blocked 66 | fnc_realtime(Reset); 67 | } else { 68 | // Non-critical alarm that can be soft-cleared 69 | send_line("$X"); 70 | } 71 | break; 72 | case Cycle: 73 | case Homing: 74 | case Hold: 75 | case DoorClosed: 76 | fnc_realtime(Reset); 77 | break; 78 | } 79 | } 80 | 81 | bool alarm_is_homing() { return lastAlarm == 14 || (lastAlarm >= 6 && lastAlarm <= 9); } 82 | bool alarm_is_critical() { 83 | // HardLimit or SoftLimit or SpindleControl or HardStop 84 | return lastAlarm == 1 || lastAlarm == 2 || lastAlarm == 10 || lastAlarm == 13; 85 | } 86 | void onGreenButtonPress() { 87 | switch (state) { 88 | case Cycle: 89 | fnc_realtime(FeedHold); 90 | break; 91 | case Hold: 92 | case DoorClosed: 93 | fnc_realtime(CycleStart); 94 | break; 95 | case Alarm: 96 | if (alarm_is_homing()) { 97 | send_line("$H"); 98 | } 99 | break; 100 | } 101 | fnc_realtime(StatusReport); 102 | } 103 | 104 | void onEncoder(int delta) { 105 | if (state == Cycle) { 106 | switch (overd_display) { 107 | case FRO: 108 | if (delta > 0 && myFro < 200) { 109 | fnc_realtime(FeedOvrFinePlus); 110 | } else if (delta < 0 && myFro > 10) { 111 | fnc_realtime(FeedOvrFineMinus); 112 | } 113 | break; 114 | case SRO: 115 | if (delta > 0 && mySro < 200) { 116 | fnc_realtime(SpindleOvrFinePlus); 117 | } else if (delta < 0 && mySro > 10) { 118 | fnc_realtime(SpindleOvrFineMinus); 119 | } 120 | break; 121 | case RT_FEED_SPEED: 122 | overd_display = FRO; 123 | } 124 | 125 | reDisplay(); 126 | } 127 | } 128 | 129 | void onDROChange() { reDisplay(); } 130 | void onLimitsChange() { reDisplay(); } 131 | 132 | void reDisplay() { 133 | background(); 134 | drawMenuTitle(current_scene->name()); 135 | drawStatus(); 136 | 137 | DRO dro(16, 68, 210, 32); 138 | dro.draw(0, -1, true); 139 | dro.draw(1, -1, true); 140 | dro.draw(2, -1, true); 141 | 142 | int y = 170; 143 | if (state == Cycle || state == Hold) { 144 | int width = 192; 145 | int height = 10; 146 | if (myPercent > 0) { 147 | drawRect(20, y, width, height, 5, LIGHTGREY); 148 | width = (width * myPercent) / 100; 149 | if (width > 0) { 150 | drawRect(20, y, width, height, 5, GREEN); 151 | } 152 | } 153 | // Feed override 154 | char legend[50]; 155 | switch (overd_display) { 156 | case FRO: 157 | sprintf(legend, "Feed Rate Ovr:%d%%", myFro); 158 | break; 159 | case SRO: 160 | sprintf(legend, "Spindle Ovr:%d%%", mySro); 161 | break; 162 | case RT_FEED_SPEED: 163 | sprintf(legend, "Fd:%d Spd:%d", myFeed, mySpeed); 164 | } 165 | centered_text(legend, y + 23); 166 | } else { 167 | centered_text(mode_string(), y + 23, GREEN, TINY); 168 | } 169 | 170 | const char* encoder_button_text = "Menu"; 171 | 172 | const char* grnLabel = ""; 173 | const char* redLabel = ""; 174 | const char* yellowLabel = "Back"; 175 | 176 | switch (state) { 177 | case Alarm: 178 | if (alarm_is_critical()) { 179 | redLabel = "Reset"; 180 | } else { 181 | redLabel = "Unlock"; 182 | } 183 | if (alarm_is_homing()) { 184 | grnLabel = "Home All"; 185 | } 186 | break; 187 | case Homing: 188 | redLabel = "Reset"; 189 | break; 190 | case Cycle: 191 | redLabel = "E-Stop"; 192 | grnLabel = "Hold"; 193 | yellowLabel = "Rst Ovr"; 194 | break; 195 | case Hold: 196 | case DoorClosed: 197 | redLabel = "Quit"; 198 | grnLabel = "Resume"; 199 | yellowLabel = "Rst Ovr"; 200 | break; 201 | case Jog: 202 | redLabel = "Jog Cancel"; 203 | break; 204 | case Idle: 205 | break; 206 | } 207 | drawButtonLegends(redLabel, grnLabel, yellowLabel); 208 | 209 | refreshDisplay(); 210 | } 211 | }; 212 | StatusScene statusScene; 213 | -------------------------------------------------------------------------------- /src/System.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "System.h" 5 | #include "FluidNCModel.h" 6 | 7 | #if 0 8 | // Helpful for debugging touch development. 9 | const char* M5TouchStateName(m5::touch_state_t state_num) { 10 | static constexpr const char* state_name[16] = { "none", "touch", "touch_end", "touch_begin", "___", "hold", "hold_end", "hold_begin", 11 | "___", "flick", "flick_end", "flick_begin", "___", "drag", "drag_end", "drag_begin" }; 12 | 13 | return state_name[state_num]; 14 | } 15 | #endif 16 | 17 | void dbg_printf(const char* format, ...) { 18 | va_list args; 19 | va_start(args, format); 20 | vprintf(format, args); 21 | va_end(args); 22 | } 23 | 24 | void dbg_print(const std::string& s) { 25 | dbg_print(s.c_str()); 26 | } 27 | 28 | void dbg_println(const std::string& s) { 29 | dbg_println(s.c_str()); 30 | } 31 | 32 | void dbg_println(const char* s) { 33 | dbg_print(s); 34 | dbg_print("\r\n"); 35 | } 36 | -------------------------------------------------------------------------------- /src/System.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #pragma once 5 | 6 | #include "Config.h" 7 | #include "Encoder.h" 8 | 9 | #ifdef ARDUINO 10 | # include 11 | # include 12 | constexpr static const int UPDATE_RATE_MS = 30; // minimum refresh rate in milliseconds 13 | extern Stream& debugPort; 14 | void init_fnc_uart(int uart_num, int tx_pin, int rx_pin); 15 | #endif // ARDUINO 16 | 17 | #ifdef USE_LOVYANGFX 18 | # include "LovyanGFX.h" 19 | # include "Touch_Class.hpp" 20 | 21 | # define WHITE TFT_WHITE 22 | # define BLACK TFT_BLACK 23 | # define RED TFT_RED 24 | # define YELLOW TFT_YELLOW 25 | # define BLUE TFT_BLUE 26 | # define LIGHTGREY TFT_LIGHTGREY 27 | # define DARKGREY TFT_DARKGREY 28 | # define GREEN TFT_GREEN 29 | # define NAVY TFT_NAVY 30 | # define CYAN TFT_CYAN 31 | # define ORANGE TFT_ORANGE 32 | # define BROWN TFT_BROWN 33 | # define MAROON TFT_MAROON 34 | #endif // USE_LOVYANGFX 35 | 36 | #ifdef USE_M5 37 | # include "M5Unified.h" 38 | #endif // USE_M5 39 | 40 | extern LGFX_Device& display; 41 | extern LGFX_Sprite canvas; 42 | extern m5::Touch_Class& touch; 43 | 44 | void drawPngFile(const char* filename, int x, int y); 45 | void drawPngFile(LGFX_Sprite* sprite, const char* filename, int x, int y); 46 | 47 | void init_system(); 48 | 49 | void ackBeep(); 50 | 51 | void dbg_write(uint8_t c); 52 | void dbg_print(const char* s); 53 | void dbg_println(const char* s); 54 | void dbg_print(const std::string& s); 55 | void dbg_println(const std::string& s); 56 | void dbg_printf(const char* format, ...); 57 | 58 | void update_events(); 59 | void delay_ms(uint32_t ms); 60 | 61 | void resetFlowControl(); 62 | 63 | extern bool round_display; 64 | 65 | void system_background(); 66 | 67 | bool screen_encoder(int x, int y, int& delta); 68 | bool screen_button_touched(bool pressed, int x, int y, int& button); 69 | bool switch_button_touched(bool& pressed, int& button); 70 | 71 | void deep_sleep(int us); 72 | 73 | inline int display_short_side() { 74 | return (display.width() < display.height()) ? display.width() : display.height(); 75 | } 76 | 77 | void base_display(); 78 | void set_layout(int n); 79 | void next_layout(int delta); 80 | 81 | bool ui_locked(); 82 | -------------------------------------------------------------------------------- /src/SystemArduino.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | // System interface routines for the Arduino framework 5 | 6 | #include "System.h" 7 | #include "FluidNCModel.h" 8 | #include "NVS.h" 9 | 10 | #include // ESP.restart() 11 | 12 | #include 13 | #include "hal/uart_hal.h" 14 | 15 | uart_port_t fnc_uart_port; 16 | 17 | // We use the ESP-IDF UART driver instead of the Arduino 18 | // HardwareSerial driver so we can use software (XON/XOFF) 19 | // flow control. The ESP-IDF driver supports the ESP32's 20 | // hardware implementation of XON/XOFF, but Arduino does not. 21 | 22 | extern "C" void fnc_putchar(uint8_t c) { 23 | uart_write_bytes(fnc_uart_port, (const char*)&c, 1); 24 | #ifdef ECHO_FNC_TO_DEBUG 25 | dbg_write(c); 26 | #endif 27 | } 28 | 29 | void ledcolor(int n) { 30 | digitalWrite(4, !(n & 1)); 31 | digitalWrite(16, !(n & 2)); 32 | digitalWrite(17, !(n & 4)); 33 | } 34 | extern "C" int fnc_getchar() { 35 | char c; 36 | int res = uart_read_bytes(fnc_uart_port, &c, 1, 0); 37 | if (res == 1) { 38 | #ifdef LED_DEBUG 39 | if (c == '\r' || c == '\n') { 40 | ledcolor(0); 41 | } else { 42 | ledcolor(c & 7); 43 | } 44 | #endif 45 | update_rx_time(); 46 | #ifdef ECHO_FNC_TO_DEBUG 47 | dbg_write(c); 48 | #endif 49 | return c; 50 | } 51 | return -1; 52 | } 53 | 54 | extern "C" void poll_extra() { 55 | #ifdef DEBUG_TO_USB 56 | if (debugPort.available()) { 57 | char c = debugPort.read(); 58 | if (c == 0x12) { // CTRL-R 59 | ESP.restart(); 60 | while (1) {} 61 | } 62 | fnc_putchar(c); // So you can type commands to FluidNC 63 | } 64 | #endif 65 | } 66 | 67 | void drawPngFile(const char* filename, int x, int y) { 68 | drawPngFile(&canvas, filename, x, y); 69 | } 70 | void drawPngFile(LGFX_Sprite* sprite, const char* filename, int x, int y) { 71 | // When datum is middle_center, the origin is the center of the canvas and the 72 | // +Y direction is down. 73 | std::string fn { "/" }; 74 | fn += filename; 75 | sprite->drawPngFile(LittleFS, fn.c_str(), x, -y, 0, 0, 0, 0, 1.0f, 1.0f, datum_t::middle_center); 76 | } 77 | 78 | #define FORMAT_LITTLEFS_IF_FAILED true 79 | 80 | // Baud rates up to 10M work 81 | #ifndef FNC_BAUD 82 | # define FNC_BAUD 115200 83 | #endif 84 | 85 | extern void init_hardware(); 86 | 87 | void init_fnc_uart(int uart_num, int tx_pin, int rx_pin) { 88 | fnc_uart_port = (uart_port_t)uart_num; 89 | int baudrate = FNC_BAUD; 90 | uart_driver_delete(fnc_uart_port); 91 | uart_set_pin(fnc_uart_port, (gpio_num_t)tx_pin, (gpio_num_t)rx_pin, -1, -1); 92 | uart_config_t conf; 93 | #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) 94 | conf.source_clk = UART_SCLK_APB; // ESP32, ESP32S2 95 | #endif 96 | #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) 97 | // UART_SCLK_XTAL is independent of the APB frequency 98 | conf.source_clk = UART_SCLK_XTAL; // ESP32C3, ESP32S3 99 | #endif 100 | conf.baud_rate = baudrate; 101 | 102 | conf.data_bits = UART_DATA_8_BITS; 103 | conf.parity = UART_PARITY_DISABLE; 104 | conf.stop_bits = UART_STOP_BITS_1; 105 | conf.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; 106 | conf.rx_flow_ctrl_thresh = 0; 107 | if (uart_param_config(fnc_uart_port, &conf) != ESP_OK) { 108 | dbg_println("UART config failed"); 109 | while (1) {} 110 | return; 111 | }; 112 | uart_driver_install(fnc_uart_port, 256, 0, 0, NULL, ESP_INTR_FLAG_IRAM); 113 | uart_set_sw_flow_ctrl(fnc_uart_port, true, 64, 120); 114 | uint32_t baud; 115 | uart_get_baudrate(fnc_uart_port, &baud); 116 | } 117 | 118 | void init_system() { 119 | init_hardware(); 120 | 121 | if (!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) { 122 | dbg_println("LittleFS Mount Failed"); 123 | return; 124 | } 125 | 126 | // Make an offscreen canvas that can be copied to the screen all at once 127 | canvas.setColorDepth(8); 128 | canvas.createSprite(240, 240); // display.width(), display.height()); 129 | } 130 | void resetFlowControl() { 131 | fnc_putchar(0x11); 132 | uart_ll_force_xon(fnc_uart_port); 133 | } 134 | 135 | extern "C" int milliseconds() { 136 | return millis(); 137 | } 138 | 139 | void delay_ms(uint32_t ms) { 140 | delay(ms); 141 | } 142 | 143 | void dbg_write(uint8_t c) { 144 | #ifdef DEBUG_TO_USB 145 | if (debugPort.availableForWrite() > 1) { 146 | debugPort.write(c); 147 | } 148 | #endif 149 | } 150 | 151 | void dbg_print(const char* s) { 152 | #ifdef DEBUG_TO_USB 153 | if (debugPort.availableForWrite() > strlen(s)) { 154 | debugPort.print(s); 155 | } 156 | #endif 157 | } 158 | 159 | nvs_handle_t nvs_init(const char* name) { 160 | nvs_handle_t handle; 161 | esp_err_t err = nvs_open(name, NVS_READWRITE, &handle); 162 | return err == ESP_OK ? handle : 0; 163 | } 164 | -------------------------------------------------------------------------------- /src/Text.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "Text.h" 5 | #include 6 | 7 | const GFXfont* font[] = { 8 | // lgfx::v1::IFont* font[] = { 9 | &fonts::FreeSansBold9pt7b, // TINY 10 | &fonts::FreeSansBold12pt7b, // SMALL 11 | &fonts::FreeSansBold18pt7b, // MEDIUM 12 | &fonts::FreeSansBold24pt7b, // LARGE 13 | &fonts::FreeMonoBold18pt7b, // MEDIUM_MONO 14 | }; 15 | 16 | void text(const char* msg, int x, int y, int color, fontnum_t fontnum, int datum) { 17 | canvas.setFont(font[fontnum]); 18 | canvas.setTextDatum(datum); 19 | canvas.setTextColor(color); 20 | canvas.drawString(msg, x, y); 21 | } 22 | void text(const std::string& msg, int x, int y, int color, fontnum_t fontnum, int datum) { 23 | text(msg.c_str(), x, y, color, fontnum, datum); 24 | } 25 | 26 | void text(const char* msg, Point xy, int color, fontnum_t fontnum, int datum) { 27 | Point dispxy = xy.to_display(); 28 | text(msg, dispxy.x, dispxy.y, color, fontnum, datum); 29 | } 30 | void text(const std::string& msg, Point xy, int color, fontnum_t fontnum, int datum) { 31 | text(msg.c_str(), xy, color, fontnum, datum); 32 | } 33 | 34 | void centered_text(const char* msg, int y, int color, fontnum_t fontnum) { 35 | // text(msg, display_short_side() / 2, y, color, fontnum); 36 | text(msg, canvas.width() / 2, y, color, fontnum); 37 | } 38 | 39 | void auto_text(const std::string& txt, int x, int y, int w, int color, fontnum_t fontnum, int datum, bool tryfonts, bool trimleft) { 40 | bool doesnotfit = true; 41 | while (true) { // forever loop 42 | int f = fontnum; 43 | if (canvas.textWidth(txt.c_str(), font[fontnum]) <= w) { 44 | doesnotfit = false; 45 | break; 46 | } 47 | if (fontnum && tryfonts) { 48 | fontnum = (fontnum_t)(--f); 49 | } else { 50 | break; 51 | } 52 | } 53 | 54 | std::string s(txt); 55 | 56 | if (doesnotfit) { 57 | int dotswidth = canvas.textWidth(" ..."); 58 | 59 | while (s.length() > 4) { 60 | if (trimleft) { 61 | s.erase(0, 1); 62 | } else { 63 | s.erase(s.length() - 1); 64 | } 65 | if (canvas.textWidth(s.c_str()) + dotswidth <= w) { 66 | if (trimleft) { 67 | s.insert(0, "... "); 68 | } else { 69 | s += " ..."; 70 | } 71 | break; 72 | } 73 | } 74 | } 75 | 76 | text(s, x, y, color, fontnum, datum); 77 | } 78 | void auto_text(const std::string& txt, Point xy, int w, int color, fontnum_t fontnum, int datum, bool tryfonts, bool trimleft) { 79 | Point dispxy = xy.to_display(); 80 | auto_text(txt, dispxy.x, dispxy.y, w, color, fontnum, datum, tryfonts, trimleft); 81 | } 82 | -------------------------------------------------------------------------------- /src/Text.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | // Simplified ways to display text, hiding details of the graphics layer 5 | 6 | #pragma once 7 | #include "System.h" 8 | #include "Point.h" 9 | 10 | enum fontnum_t { 11 | TINY = 0, 12 | SMALL = 1, 13 | MEDIUM = 2, 14 | LARGE = 3, 15 | MEDIUM_MONO = 4, 16 | }; 17 | 18 | // adjusts text to fit in (w) display area. reduces font size until it. tryfonts::false just uses fontnum 19 | void auto_text(const std::string& txt, 20 | int x, 21 | int y, 22 | int w, 23 | int color, 24 | fontnum_t fontnum = MEDIUM, 25 | int datum = middle_center, 26 | bool tryfonts = true, 27 | bool trimleft = false); 28 | 29 | void auto_text(const std::string& txt, 30 | Point xy, 31 | int w, 32 | int color, 33 | fontnum_t fontnum = MEDIUM, 34 | int datum = middle_center, 35 | bool tryfonts = true, 36 | bool trimleft = false); 37 | 38 | void text(const char* msg, int x, int y, int color, fontnum_t fontnum = TINY, int datum = middle_center); 39 | void text(const std::string& msg, int x, int y, int color, fontnum_t fontnum = TINY, int datum = middle_center); 40 | 41 | void text(const char* msg, Point xy, int color, fontnum_t fontnum = TINY, int datum = middle_center); 42 | void text(const std::string& msg, Point xy, int color, fontnum_t fontnum = TINY, int datum = middle_center); 43 | 44 | void centered_text(const char* msg, int y, int color = WHITE, fontnum_t fontnum = TINY); 45 | -------------------------------------------------------------------------------- /src/ToolChangeScene.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Barton Dring 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include 5 | #include "Scene.h" 6 | #include "e4math.h" 7 | 8 | class ToolChangeScene : public Scene { 9 | private: 10 | int selection = 0; 11 | long oldPosition = 0; 12 | 13 | // Saved to NVS 14 | e4_t _offset = e4_from_int(0); 15 | int _travel = -20; 16 | int _rate = 80; 17 | int _retract = 20; 18 | int _axis = 2; // Z is default 19 | 20 | int _new_tool = 0; 21 | 22 | public: 23 | ToolChangeScene() : Scene("Tools", 4) {} 24 | 25 | void onDialButtonPress() { pop_scene(); } 26 | 27 | void onRedButtonPress() { 28 | if (state == Idle) { 29 | } else if (state == Cycle) { 30 | } 31 | 32 | switch (state) { 33 | case Idle: 34 | send_linef("T%d", _new_tool); 35 | break; 36 | 37 | case Hold: 38 | case Cycle: 39 | fnc_realtime(Reset); 40 | break; 41 | 42 | default: 43 | break; 44 | } 45 | } 46 | 47 | void onGreenButtonPress() { 48 | switch (state) { 49 | case Idle: 50 | send_line("M6"); 51 | break; 52 | 53 | case Hold: 54 | fnc_realtime(CycleStart); 55 | break; 56 | 57 | case Cycle: 58 | fnc_realtime(FeedHold); 59 | break; 60 | 61 | default: 62 | break; 63 | } 64 | } 65 | 66 | void onStateChange(state_t old_state) { reDisplay(); } 67 | 68 | void onTouchClick() { 69 | if (state == Idle) { 70 | send_linef("M61Q%d", _new_tool); 71 | } 72 | } 73 | 74 | void onEncoder(int delta) { 75 | if (abs(delta) > 0) { 76 | rotateNumberLoop(_new_tool, delta, 0, 255); 77 | reDisplay(); 78 | } 79 | } 80 | void onEntry(void* arg) override {} 81 | 82 | void reDisplay() { 83 | background(); 84 | drawMenuTitle(current_scene->name()); 85 | drawStatus(); 86 | 87 | bool M6Q_button_enabled = false; 88 | 89 | int y = 80; 90 | int y_spacing = 30; 91 | 92 | const char* grnLabel = ""; 93 | const char* redLabel = ""; 94 | static char buffer[20]; 95 | 96 | sprintf(buffer, "Current T Value: %d", mySelectedTool); 97 | centered_text(buffer, y, LIGHTGREY, TINY); 98 | 99 | switch (state) { 100 | case Idle: 101 | grnLabel = "M6"; 102 | redLabel = "T"; 103 | 104 | M6Q_button_enabled = true; 105 | 106 | sprintf(buffer, "T%d", _new_tool); 107 | redLabel = buffer; 108 | break; 109 | 110 | case Hold: 111 | grnLabel = "Resume"; 112 | break; 113 | 114 | case Cycle: 115 | redLabel = "Reset"; 116 | grnLabel = "Hold"; 117 | break; 118 | 119 | default: 120 | break; 121 | } 122 | int x = 50; 123 | int width = display_short_side() - (x * 2); 124 | if (M6Q_button_enabled) { 125 | Stripe button(x, 110, width, 50, SMALL); 126 | button.draw("M61Q", intToCStr(_new_tool), M6Q_button_enabled); 127 | } 128 | 129 | drawButtonLegends(redLabel, grnLabel, "Back"); 130 | drawError(); // only if one just happened 131 | refreshDisplay(); 132 | } 133 | }; 134 | ToolChangeScene toolchangeScene; 135 | -------------------------------------------------------------------------------- /src/Touch_Class.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) M5Stack. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #include "Touch_Class.hpp" 5 | 6 | namespace m5 { 7 | void Touch_Class::update(std::uint32_t msec) { 8 | if (msec - _last_msec <= TOUCH_MIN_UPDATE_MSEC) /// Avoid high frequency updates 9 | { 10 | if (_detail_count == 0) { 11 | return; 12 | } 13 | std::size_t count = 0; 14 | for (std::size_t i = 0; i < TOUCH_MAX_POINTS; ++i) { 15 | count += update_detail(&_touch_detail[i], msec); 16 | } 17 | _detail_count = count; 18 | return; 19 | } 20 | 21 | _last_msec = msec; 22 | std::size_t count = _gfx->getTouchRaw(_touch_raw, TOUCH_MAX_POINTS); 23 | if (!(count || _detail_count)) { 24 | return; 25 | } 26 | 27 | uint32_t updated_id = 0; 28 | if (count) { 29 | lgfx::touch_point_t tp[TOUCH_MAX_POINTS]; 30 | memcpy(tp, _touch_raw, sizeof(lgfx::touch_point_t) * count); 31 | _gfx->convertRawXY(tp, count); 32 | for (std::size_t i = 0; i < count; ++i) { 33 | if (tp[i].id < TOUCH_MAX_POINTS) { 34 | updated_id |= 1 << tp[i].id; 35 | update_detail(&_touch_detail[tp[i].id], msec, true, &tp[i]); 36 | } 37 | } 38 | } 39 | { 40 | for (std::size_t i = 0; i < TOUCH_MAX_POINTS; ++i) { 41 | if ((!(updated_id & (1 << i))) && update_detail(&_touch_detail[i], msec, false, nullptr) && (count < TOUCH_MAX_POINTS)) { 42 | ++count; 43 | } 44 | } 45 | } 46 | _detail_count = count; 47 | } 48 | 49 | bool Touch_Class::update_detail(touch_detail_t* det, std::uint32_t msec, bool pressed, lgfx::touch_point_t* tp) { 50 | touch_state_t tm = det->state; 51 | if (tm == touch_state_t::none && !pressed) { 52 | return false; 53 | } 54 | tm = static_cast(tm & ~touch_state_t::mask_change); 55 | if (pressed) { 56 | det->prev_x = det->x; 57 | det->prev_y = det->y; 58 | det->size = tp->size; 59 | det->id = tp->id; 60 | if (!(tm & touch_state_t::mask_moving)) { // Processing when not flicked. 61 | if (tm & touch_state_t::mask_touch) { // Not immediately after the touch. 62 | if (abs(det->base_x - tp->x) > _flickThresh || abs(det->base_y - tp->y) > _flickThresh) { 63 | det->prev = det->base; 64 | tm = static_cast(tm | touch_state_t::flick_begin); 65 | } else if ((tm == touch) && (msec - det->base_msec > _msecHold)) { 66 | tm = touch_state_t::hold_begin; 67 | } 68 | } else { 69 | *(static_cast(det)) = *tp; 70 | tm = touch_state_t::touch_begin; 71 | det->base_msec = msec; 72 | det->base_x = tp->x; 73 | det->base_y = tp->y; 74 | det->prev = det->base; 75 | } 76 | } 77 | if (tm & mask_moving) { 78 | det->x = tp->x; 79 | det->y = tp->y; 80 | } 81 | } else { 82 | tm = (tm & touch_state_t::mask_touch) ? 83 | static_cast((tm | touch_state_t::mask_change) & ~touch_state_t::mask_touch) : 84 | touch_state_t::none; 85 | } 86 | det->state = tm; 87 | return true; 88 | } 89 | 90 | bool Touch_Class::update_detail(touch_detail_t* det, std::uint32_t msec) { 91 | touch_state_t tm = det->state; 92 | if (tm == touch_state_t::none) { 93 | return false; 94 | } 95 | tm = static_cast(tm & ~touch_state_t::mask_change); 96 | if (tm & touch) { 97 | det->prev_x = det->x; 98 | det->prev_y = det->y; 99 | if ((tm == touch) && (msec - det->base_msec > _msecHold)) { 100 | tm = touch_state_t::hold_begin; 101 | } 102 | } else { 103 | tm = touch_state_t::none; 104 | } 105 | det->state = tm; 106 | return true; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Touch_Class.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) M5Stack. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #ifndef __Touch_Class_H__ 5 | #define __Touch_Class_H__ 6 | 7 | #include 8 | 9 | #include 10 | 11 | namespace m5 { 12 | enum touch_state_t { 13 | none = 0b0000, 14 | touch = 0b0001, 15 | touch_end = 0b0010, 16 | touch_begin = 0b0011 17 | 18 | , 19 | hold = 0b0101, 20 | hold_end = 0b0110, 21 | hold_begin = 0b0111 22 | 23 | , 24 | flick = 0b1001, 25 | flick_end = 0b1010, 26 | flick_begin = 0b1011 27 | 28 | , 29 | drag = 0b1101, 30 | drag_end = 0b1110, 31 | drag_begin = 0b1111 32 | 33 | , 34 | mask_touch = 0b0001, 35 | mask_change = 0b0010, 36 | mask_holding = 0b0100, 37 | mask_moving = 0b1000 38 | }; 39 | 40 | class Touch_Class { 41 | public: 42 | static constexpr std::size_t TOUCH_MAX_POINTS = 3; 43 | static constexpr std::size_t TOUCH_MIN_UPDATE_MSEC = 4; 44 | 45 | struct point_t { 46 | std::int16_t x; 47 | std::int16_t y; 48 | }; 49 | 50 | struct touch_detail_t : public lgfx::touch_point_t { 51 | union { /// Previous point 52 | point_t prev; 53 | struct { 54 | std::int16_t prev_x; 55 | std::int16_t prev_y; 56 | }; 57 | }; 58 | union { /// Flick start point 59 | point_t base; 60 | struct { 61 | std::int16_t base_x; 62 | std::int16_t base_y; 63 | }; 64 | }; 65 | 66 | std::uint32_t base_msec; 67 | touch_state_t state = touch_state_t::none; 68 | 69 | inline int deltaX(void) const { return x - prev_x; } 70 | inline int deltaY(void) const { return y - prev_y; } 71 | inline int distanceX(void) const { return x - base_x; } 72 | inline int distanceY(void) const { return y - base_y; } 73 | inline bool isPressed(void) const { return state & touch_state_t::mask_touch; }; 74 | inline bool wasPressed(void) const { return state == touch_state_t::touch_begin; }; 75 | inline bool wasClicked(void) const { return state == touch_state_t::touch_end; }; 76 | inline bool isReleased(void) const { return !(state & touch_state_t::mask_touch); }; 77 | inline bool wasReleased(void) const { 78 | return (state & (touch_state_t::mask_touch | touch_state_t::mask_change)) == touch_state_t::mask_change; 79 | }; 80 | inline bool isHolding(void) const { 81 | return (state & (touch_state_t::mask_touch | touch_state_t::mask_holding)) == 82 | (touch_state_t::mask_touch | touch_state_t::mask_holding); 83 | } 84 | inline bool wasHold(void) const { return state == touch_state_t::hold_begin; } 85 | inline bool wasFlickStart(void) const { return state == touch_state_t::flick_begin; } 86 | inline bool isFlicking(void) const { return (state & touch_state_t::drag) == touch_state_t::flick; } 87 | inline bool wasFlicked(void) const { return state == touch_state_t::flick_end; } 88 | inline bool wasDragStart(void) const { return state == touch_state_t::drag_begin; } 89 | inline bool isDragging(void) const { return (state & touch_state_t::drag) == touch_state_t::drag; } 90 | inline bool wasDragged(void) const { return state == touch_state_t::drag_end; } 91 | }; 92 | 93 | /// Get the current number of touchpoints. 94 | /// @return number of touchpoints. 95 | inline std::uint8_t getCount(void) const { return _detail_count; } 96 | 97 | /// 98 | inline const touch_detail_t& getDetail(std::size_t index = 0) const { 99 | return _touch_detail[_touch_raw[index].id < TOUCH_MAX_POINTS ? _touch_raw[index].id : 0]; 100 | } 101 | 102 | inline const lgfx::touch_point_t& getTouchPointRaw(std::size_t index = 0) const { 103 | return _touch_raw[index < _detail_count ? index : 0]; 104 | } 105 | 106 | void setHoldThresh(std::uint16_t msec) { _msecHold = msec; } 107 | 108 | void setFlickThresh(std::uint16_t distance) { _flickThresh = distance; } 109 | 110 | bool isEnabled(void) const { return _gfx; } 111 | 112 | void begin(lgfx::LGFX_Device* gfx) { _gfx = gfx; } 113 | void update(std::uint32_t msec); 114 | void end(void) { _gfx = nullptr; } 115 | 116 | protected: 117 | std::uint32_t _last_msec = 0; 118 | std::int32_t _flickThresh = 8; 119 | std::int32_t _msecHold = 500; 120 | lgfx::LGFX_Device* _gfx = nullptr; 121 | touch_detail_t _touch_detail[TOUCH_MAX_POINTS]; 122 | lgfx::touch_point_t _touch_raw[TOUCH_MAX_POINTS]; 123 | std::uint8_t _detail_count; 124 | 125 | bool update_detail(touch_detail_t* dt, std::uint32_t msec, bool pressed, lgfx::touch_point_t* tp); 126 | bool update_detail(touch_detail_t* dt, std::uint32_t msec); 127 | }; 128 | 129 | using touch_detail_t = Touch_Class::touch_detail_t; 130 | } 131 | #endif 132 | -------------------------------------------------------------------------------- /src/ardmain.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 - Barton Dring 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | #include "System.h" 5 | #include "FileParser.h" 6 | #include "Scene.h" 7 | #include "AboutScene.h" 8 | 9 | extern void base_display(); 10 | extern void show_logo(); 11 | 12 | extern const char* git_info; 13 | 14 | extern AboutScene aboutScene; 15 | 16 | void setup() { 17 | init_system(); 18 | 19 | display.setBrightness(aboutScene.getBrightness()); 20 | 21 | show_logo(); 22 | delay_ms(2000); // view the logo and wait for the debug port to connect 23 | 24 | base_display(); 25 | 26 | dbg_printf("FluidNC Pendant %s\n", git_info); 27 | 28 | fnc_realtime(StatusReport); // Kick FluidNC into action 29 | 30 | // init_file_list(); 31 | 32 | extern Scene* initMenus(); 33 | activate_scene(initMenus()); 34 | } 35 | 36 | void loop() { 37 | fnc_poll(); // Handle messages from FluidNC 38 | dispatch_events(); // Handle dial, touch, buttons 39 | } 40 | -------------------------------------------------------------------------------- /src/polar.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | // Integer trig for converting between rectangular and polar coordinates, 5 | // useful for calculating circular layouts on a graphics screen. 6 | 7 | #include "polar.h" 8 | 9 | #include 10 | 11 | // 2*pi << REVS_BITS 12 | #define TWOPISCALED 25736 13 | 14 | // Internal factor that converges quickly for angles less than 45 degrees 15 | static void _r_revs_to_xy(int radius, int angle, int* px, int* py) { 16 | angle *= TWOPISCALED; 17 | angle >>= REVS_BITS; 18 | int adjust = radius * 4; 19 | int cos = 0, sin = 0; 20 | for (int i = 0; i < 10;) { 21 | cos += adjust; 22 | adjust *= angle; 23 | adjust /= ++i; 24 | adjust >>= REVS_BITS; 25 | if (adjust == 0) { 26 | break; 27 | } 28 | sin += adjust; 29 | adjust *= angle; 30 | adjust /= ++i; 31 | adjust >>= REVS_BITS; 32 | if (adjust == 0) { 33 | break; 34 | } 35 | adjust = -adjust; 36 | } 37 | *py = (sin + 2) / 4; 38 | *px = (cos + 2) / 4; 39 | } 40 | 41 | // External API. Uses symmetries to reduce the angle to the range 42 | // where _rtheta_to_xy() converges quickly. The angle is scaled 43 | // such that (1 << REVS_BITS) represents a full revolution. 44 | void r_revs_to_xy(int radius, int angle, int* px, int* py) { 45 | if (angle < 0) { 46 | r_revs_to_xy(radius, -angle, px, py); 47 | *py = -(*py); 48 | return; 49 | } 50 | if (angle > (1 << (REVS_BITS - 1))) { // > 180 degrees 51 | r_revs_to_xy(radius, (1 << (REVS_BITS)) - angle, px, py); 52 | *py = -*py; 53 | return; 54 | } 55 | if (angle > (1 << (REVS_BITS - 2))) { // > 90 degrees 56 | r_revs_to_xy(radius, (1 << (REVS_BITS - 1)) - angle, px, py); 57 | *px = -(*px); 58 | return; 59 | } 60 | if (angle > (1 << (REVS_BITS - 3))) { 61 | r_revs_to_xy(radius, ((1 << (REVS_BITS - 2)) - angle), px, py); 62 | int sin = *px; 63 | *px = *py; 64 | *py = sin; 65 | return; 66 | } 67 | _r_revs_to_xy(radius, angle, px, py); 68 | } 69 | 70 | void r_degrees_to_xy(int radius, int degrees, int* px, int* py) { 71 | return r_revs_to_xy(radius, to_revs(degrees, 360), px, py); 72 | } 73 | 74 | // The result is scaled by the radius. E.g. if degrees is 45, 75 | // for a slope of 1, the return value is radius. 76 | int r_degrees_to_slope(int radius, int degrees) { 77 | int x, y; 78 | r_degrees_to_xy(radius, degrees, &x, &y); 79 | if (x == 0) { 80 | x = 1; 81 | } 82 | return y * radius / x; 83 | } 84 | 85 | // arctangent approximation per https://nghiaho.com/?p=997 86 | // Radians: z(M_PI_4 - (z - 1)*(0.2447 + 0.0663*z); 87 | // Degrees: z(45°−(z−1)(14°+3.83°z)) 88 | #define ATAN_SCALER 1024 89 | #define ATAN_SCALE(n) (n * ATAN_SCALER) 90 | #define ATAN_DOWNSCALE(n) \ 91 | do { \ 92 | n += ATAN_SCALER / 2; \ 93 | n /= ATAN_SCALER; \ 94 | } while (0) 95 | #define ATAN_MUL(n, m) \ 96 | do { \ 97 | n *= m; \ 98 | ATAN_DOWNSCALE(n); \ 99 | } while (0) 100 | 101 | // Internal factor limited to 0 <= y/x <= 1 102 | static int _iatan2_degrees(int x, int y) { 103 | if (x == 0) { 104 | return 90; 105 | } 106 | int z = ATAN_SCALE(y) / x; 107 | int res = 3922 * z; // 3922 = 3.83 * 1024 108 | ATAN_DOWNSCALE(res); 109 | res += ATAN_SCALE(14); 110 | ATAN_MUL(res, (z - ATAN_SCALE(1))); 111 | res = ATAN_SCALE(45) - res; 112 | 113 | ATAN_MUL(res, z); 114 | ATAN_DOWNSCALE(res); 115 | return res; 116 | } 117 | 118 | // XY to angle, result in degrees 119 | int iatan2_degrees(int x, int y) { 120 | if (y < 0) { 121 | return -iatan2_degrees(x, -y); 122 | } 123 | if (x < 0) { 124 | return 180 - iatan2_degrees(-x, y); 125 | } 126 | if (y > x) { 127 | return 90 - iatan2_degrees(y, x); 128 | } 129 | return _iatan2_degrees(x, y); 130 | } 131 | 132 | // sqrt(x*x + y*y) 133 | int imagnitude(int x, int y) { 134 | int n = x * x + y * y; 135 | int res = n; 136 | int trial = 1; 137 | while (res > trial) { 138 | res = (res + trial) >> 1; 139 | trial = n / res; 140 | } 141 | return res; 142 | } 143 | 144 | void xy_to_r_degrees(int x, int y, int* radius, int* degrees) { 145 | *degrees = iatan2_degrees(x, y); 146 | *radius = imagnitude(x, y); 147 | } 148 | -------------------------------------------------------------------------------- /src/polar.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 - Mitch Bradley 2 | // Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. 3 | 4 | // Integer trig for converting between rectangular and polar coordinates, 5 | // useful for calculating circular layouts on a graphics screen. 6 | 7 | #pragma once 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | #define REVS_BITS 12 14 | 15 | // 1 << REVS_BITS is a full circle 16 | // For, say, 45 degrees, use theta(45, 360); 17 | inline int to_revs(int num, int denom) { 18 | return (num << REVS_BITS) / denom; 19 | } 20 | void r_revs_to_xy(int radius, int angle, int* px, int* py); 21 | void r_degrees_to_xy(int radius, int degrees, int* px, int* py); 22 | int r_degrees_to_slope(int radius, int degrees); 23 | int iatan2(int x, int y); 24 | int imagnitude(int x, int y); 25 | void xy_to_r_degrees(int x, int y, int* radius, int* theta); 26 | 27 | #ifdef __cplusplus 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /src/sdlmain.c: -------------------------------------------------------------------------------- 1 | #ifndef ARDUINO 2 | # include 3 | # if defined(SDL_h_) 4 | 5 | extern void setup(); 6 | extern void loop(); 7 | 8 | char* comname; 9 | int main(int argc, char** argv) { 10 | if (argc != 2) { 11 | printf("Usage: %s COMn\n", argv[0]); 12 | exit(1); 13 | } 14 | comname = argv[1]; 15 | 16 | setup(); 17 | 18 | while (1) { 19 | loop(); 20 | } 21 | return 0; 22 | } 23 | # endif 24 | #endif 25 | -------------------------------------------------------------------------------- /test/M5_test.yaml: -------------------------------------------------------------------------------- 1 | name: "UART M5Dial Tesy" 2 | board: "6x CNC Controller" 3 | 4 | stepping: 5 | engine: I2S_STREAM 6 | idle_ms: 254 7 | pulse_us: 4 8 | dir_delay_us: 1 9 | disable_delay_us: 0 10 | 11 | axes: 12 | shared_stepper_disable_pin: NO_PIN 13 | x: 14 | steps_per_mm: 800.000 15 | max_rate_mm_per_min: 5000.000 16 | acceleration_mm_per_sec2: 100.000 17 | max_travel_mm: 300.000 18 | soft_limits: false 19 | homing: 20 | cycle: 2 21 | positive_direction: false 22 | mpos_mm: 150.000 23 | feed_mm_per_min: 100.000 24 | seek_mm_per_min: 200.000 25 | settle_ms: 500 26 | seek_scaler: 1.100 27 | feed_scaler: 1.100 28 | 29 | motor0: 30 | limit_neg_pin: gpio.2:low:pu 31 | limit_pos_pin: NO_PIN 32 | limit_all_pin: NO_PIN 33 | hard_limits: false 34 | pulloff_mm: 1.000 35 | standard_stepper: 36 | step_pin: I2SO.2 37 | direction_pin: I2SO.1 38 | disable_pin: I2SO.0 39 | 40 | y: 41 | steps_per_mm: 800.000 42 | max_rate_mm_per_min: 5000.000 43 | acceleration_mm_per_sec2: 100.000 44 | max_travel_mm: 300.000 45 | soft_limits: false 46 | homing: 47 | cycle: 2 48 | positive_direction: true 49 | mpos_mm: 150.000 50 | feed_mm_per_min: 100.000 51 | seek_mm_per_min: 200.000 52 | settle_ms: 500 53 | seek_scaler: 1.100 54 | feed_scaler: 1.100 55 | 56 | motor0: 57 | limit_neg_pin: gpio.26:low:pu 58 | limit_pos_pin: NO_PIN 59 | limit_all_pin: NO_PIN 60 | hard_limits: false 61 | pulloff_mm: 1.000 62 | standard_stepper: 63 | step_pin: I2SO.5 64 | direction_pin: I2SO.4 65 | disable_pin: I2SO.7 66 | 67 | z: 68 | steps_per_mm: 800.000 69 | max_rate_mm_per_min: 5000.000 70 | acceleration_mm_per_sec2: 100.000 71 | max_travel_mm: 300.000 72 | soft_limits: false 73 | homing: 74 | cycle: 1 75 | positive_direction: true 76 | mpos_mm: 150.000 77 | feed_mm_per_min: 100.000 78 | seek_mm_per_min: 800.000 79 | settle_ms: 500 80 | seek_scaler: 1.100 81 | feed_scaler: 1.100 82 | 83 | start: 84 | must_home: true 85 | 86 | uart1: 87 | txd_pin: gpio.25 88 | rxd_pin: gpio.27 89 | baud: 115200 90 | mode: 8N1 91 | 92 | uart_channel1: 93 | report_interval_ms: 75 94 | uart_num: 1 95 | 96 | i2so: 97 | bck_pin: gpio.22 98 | data_pin: gpio.21 99 | ws_pin: gpio.17 100 | 101 | spi: 102 | miso_pin: gpio.19 103 | mosi_pin: gpio.23 104 | sck_pin: gpio.18 105 | 106 | sdcard: 107 | cs_pin: gpio.5 108 | card_detect_pin: NO_PIN 109 | 110 | probe: 111 | pin: gpio.36 112 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Test Runner and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html 12 | --------------------------------------------------------------------------------