Compare commits
30 commits
Author | SHA1 | Date | |
---|---|---|---|
6bc6a38531 | |||
b855a8c388 | |||
24e93521f9 | |||
fd043e41b5 | |||
8400584ebb | |||
69dec00654 | |||
1e382f069f | |||
93c3165678 | |||
681c4e3bd8 | |||
494122a037 | |||
7ffb351379 | |||
3a8d98f8f5 | |||
a6da6c0a4f | |||
dac0b85e9f | |||
83f21036b2 | |||
363e623214 | |||
9f511e3a0a | |||
70a0bd179b | |||
7e2eb9e9bb | |||
43e486c911 | |||
636b683ccc | |||
d4c8001bf5 | |||
73abf75886 | |||
ef4fb943f1 | |||
0ae33a44ff | |||
6edee4b073 | |||
6146f60574 | |||
e08c2d4c55 | |||
f52d85be3e | |||
ad8f6c44ef |
105 changed files with 10435 additions and 490 deletions
351
.drone.yml
Normal file
351
.drone.yml
Normal file
|
@ -0,0 +1,351 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
|
||||
steps:
|
||||
- name: restore ASDF cache
|
||||
image: meltwater/drone-cache
|
||||
pull: "always"
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: SECRET_ACCESS_KEY
|
||||
AWS_PLUGIN_PATH_STYLE: true
|
||||
settings:
|
||||
restore: true
|
||||
endpoint:
|
||||
from_secret: S3_ENDPOINT
|
||||
bucket:
|
||||
from_secret: CACHE_BUCKET
|
||||
region: us-east-1
|
||||
path-style: true
|
||||
cache_key: 'asdf-{{ os }}-{{ arch }}-{{ checksum ".tool-versions" }}'
|
||||
mount:
|
||||
- .asdf
|
||||
|
||||
- name: restore build cache
|
||||
image: meltwater/drone-cache
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: SECRET_ACCESS_KEY
|
||||
AWS_PLUGIN_PATH_STYLE: true
|
||||
settings:
|
||||
restore: true
|
||||
endpoint:
|
||||
from_secret: S3_ENDPOINT
|
||||
bucket:
|
||||
from_secret: CACHE_BUCKET
|
||||
region: us-east-1
|
||||
path-style: true
|
||||
cache_key: 'elixir-{{ checksum "mix.lock" }}-{{ checksum ".tool-versions" }}'
|
||||
mount:
|
||||
- deps
|
||||
- _build
|
||||
- .hex
|
||||
- .mix
|
||||
- .rebar3
|
||||
|
||||
- name: install dependencies
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
pull: "always"
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
ASDF_DIR: /root/.asdf
|
||||
depends_on:
|
||||
- restore ASDF cache
|
||||
- restore build cache
|
||||
commands:
|
||||
- asdf_install
|
||||
- rm -rf .asdf/downloads
|
||||
- . $ASDF_DIR/asdf.sh
|
||||
- mix local.hex --if-missing --force
|
||||
- mix local.rebar --if-missing --force
|
||||
- mix deps.get
|
||||
- mix deps.compile
|
||||
|
||||
- name: store ASDF cache
|
||||
image: meltwater/drone-cache
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: SECRET_ACCESS_KEY
|
||||
AWS_PLUGIN_PATH_STYLE: true
|
||||
depends_on:
|
||||
- install dependencies
|
||||
settings:
|
||||
rebuild: true
|
||||
override: false
|
||||
endpoint:
|
||||
from_secret: S3_ENDPOINT
|
||||
bucket:
|
||||
from_secret: CACHE_BUCKET
|
||||
region: us-east-1
|
||||
path-style: true
|
||||
cache_key: 'asdf-{{ os }}-{{ arch }}-{{ checksum ".tool-versions" }}'
|
||||
mount:
|
||||
- .asdf
|
||||
|
||||
- name: store build cache
|
||||
image: meltwater/drone-cache
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: SECRET_ACCESS_KEY
|
||||
AWS_PLUGIN_PATH_STYLE: true
|
||||
depends_on:
|
||||
- install dependencies
|
||||
settings:
|
||||
rebuild: true
|
||||
override: false
|
||||
endpoint:
|
||||
from_secret: S3_ENDPOINT
|
||||
bucket:
|
||||
from_secret: CACHE_BUCKET
|
||||
region: us-east-1
|
||||
path-style: true
|
||||
cache_key: 'elixir-{{ checksum "mix.lock" }}-{{ checksum ".tool-versions" }}'
|
||||
mount:
|
||||
- deps
|
||||
- _build
|
||||
- .hex
|
||||
- .mix
|
||||
- .rebar3
|
||||
|
||||
- name: mix compile
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
depends_on:
|
||||
- install dependencies
|
||||
commands:
|
||||
- asdf mix compile --warnings-as-errors
|
||||
|
||||
- name: mix test
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
depends_on:
|
||||
- mix compile
|
||||
commands:
|
||||
- asdf mix test
|
||||
|
||||
- name: mix credo
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
depends_on:
|
||||
- mix compile
|
||||
commands:
|
||||
- asdf mix credo --strict
|
||||
|
||||
- name: mix hex.audit
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
depends_on:
|
||||
- mix compile
|
||||
commands:
|
||||
- asdf mix hex.audit
|
||||
|
||||
- name: mix format
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
depends_on:
|
||||
- mix compile
|
||||
commands:
|
||||
- asdf mix format --check-formatted
|
||||
|
||||
- name: mix deps.unlock
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
depends_on:
|
||||
- mix compile
|
||||
commands:
|
||||
- asdf mix deps.unlock --check-unused
|
||||
|
||||
- name: mix git_ops.check_message
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
depends_on:
|
||||
- mix compile
|
||||
commands:
|
||||
- git log -1 --format=%s > .last_commit_message
|
||||
- asdf mix git_ops.check_message .last_commit_message
|
||||
|
||||
- name: mix git_ops.release
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
depends_on:
|
||||
- mix test
|
||||
- mix credo
|
||||
- mix hex.audit
|
||||
- mix format
|
||||
- mix deps.unlock
|
||||
- mix git_ops.check_message
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
ASDF_DIR: /root/.asdf
|
||||
DRONE_TOKEN:
|
||||
from_secret: DRONE_TOKEN
|
||||
commands:
|
||||
- git fetch --tags
|
||||
- . $ASDF_DIR/asdf.sh
|
||||
- mix git_ops.project_info --format=shell > before.env
|
||||
- mix git_ops.release --yes --no-major || true
|
||||
- mix git_ops.project_info --format=shell > after.env
|
||||
- . ./before.env
|
||||
- export OLD_APP_VERSION=$${APP_VERSION}
|
||||
- . ./after.env
|
||||
- export NEW_APP_VERSION=$${APP_VERSION}
|
||||
- if [ "v$${OLD_APP_VERSION}" != "v$${NEW_APP_VERSION}" ]; then
|
||||
- export GIT_URL=$(echo $DRONE_GIT_HTTP_URL | sed -e "s/:\\/\\//:\\/\\/$DRONE_REPO_OWNER:$DRONE_TOKEN@/")
|
||||
- git push $${GIT_URL} "HEAD:${DRONE_COMMIT_REF}" "refs/tags/v$${NEW_APP_VERSION}"
|
||||
- fi
|
||||
|
||||
- name: build artifacts
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
refs:
|
||||
include:
|
||||
- refs/tags/v*
|
||||
depends_on:
|
||||
- mix test
|
||||
- mix credo
|
||||
- mix hex.audit
|
||||
- mix format
|
||||
- mix deps.unlock
|
||||
- mix git_ops.check_message
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
ASDF_DIR: /root/.asdf
|
||||
commands:
|
||||
- . $ASDF_DIR/asdf.sh
|
||||
- mix git_ops.project_info --format=shell > app.env
|
||||
- . ./app.env
|
||||
- mkdir artifacts
|
||||
- mix hex.build -o "artifacts/$${APP_NAME}-$${APP_VERSION}-pkg.tar"
|
||||
- gzip "artifacts/$${APP_NAME}-$${APP_VERSION}-pkg.tar"
|
||||
- mix docs
|
||||
- tar zcvf "artifacts/$${APP_NAME}-$${APP_VERSION}-docs.tar.gz" doc/
|
||||
- git tag -l --format='%(contents:subject)' v$${APP_VERSION} > tag_subject
|
||||
- git tag -l --format='%(contents:body)' v$${APP_VERSION} > tag_body
|
||||
|
||||
- name: gitea release
|
||||
image: plugins/gitea-release
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
refs:
|
||||
include:
|
||||
- refs/tags/v*
|
||||
depends_on:
|
||||
- build artifacts
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: DRONE_TOKEN
|
||||
base_url: https://harton.dev
|
||||
files: artifacts/*.tar.gz
|
||||
checksum: sha256
|
||||
title: tag_subject
|
||||
note: tag_body
|
||||
|
||||
- name: docs release
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
refs:
|
||||
include:
|
||||
- refs/tags/v*
|
||||
image: minio/mc
|
||||
environment:
|
||||
S3_ENDPOINT:
|
||||
from_secret: S3_ENDPOINT
|
||||
ACCESS_KEY:
|
||||
from_secret: ACCESS_KEY_ID
|
||||
SECRET_KEY:
|
||||
from_secret: SECRET_ACCESS_KEY
|
||||
depends_on:
|
||||
- build artifacts
|
||||
commands:
|
||||
- mc alias set store $${S3_ENDPOINT} $${ACCESS_KEY} $${SECRET_KEY}
|
||||
- mc mb -p store/docs.harton.nz
|
||||
- mc mirror --overwrite doc/ store/docs.harton.nz/$${DRONE_REPO}/$${DRONE_TAG}
|
||||
- mc mirror --overwrite doc/ store/docs.harton.nz/$${DRONE_REPO}
|
||||
|
||||
- name: hex release
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
refs:
|
||||
include:
|
||||
- refs/tags/v*
|
||||
depends_on:
|
||||
- build artifacts
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
MIX_HOME: /drone/src/.mix
|
||||
REBAR_BASE_DIR: /drone/src/.rebar3
|
||||
ASDF_DATA_DIR: /drone/src/.asdf
|
||||
ASDF_DIR: /root/.asdf
|
||||
HEX_API_KEY:
|
||||
from_secret: HEX_API_KEY
|
||||
commands:
|
||||
- . $ASDF_DIR/asdf.sh
|
||||
- mix hex.publish --yes
|
140
.gitlab-ci.yml
140
.gitlab-ci.yml
|
@ -1,140 +0,0 @@
|
|||
image: elixir:latest
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- release
|
||||
|
||||
variables:
|
||||
MIX_ENV: "test"
|
||||
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/"
|
||||
HEX_HOME: "$CI_PROJECT_DIR/.hex"
|
||||
MIX_HOME: "$CI_PROJECT_DIR/.mix"
|
||||
|
||||
build:
|
||||
image: elixir:latest
|
||||
stage: build
|
||||
cache:
|
||||
key: "$CI_JOB_NAME"
|
||||
paths:
|
||||
- deps
|
||||
- _build
|
||||
- .hex
|
||||
- .mix
|
||||
script:
|
||||
- mix local.hex --force
|
||||
- mix local.rebar --force
|
||||
- mix deps.get
|
||||
- mix deps.compile
|
||||
- mix git_ops.project_info -f dotenv > project_info.env
|
||||
artifacts:
|
||||
paths:
|
||||
- _build/
|
||||
- deps/
|
||||
- .hex
|
||||
- .mix
|
||||
reports:
|
||||
dotenv: project_info.env
|
||||
|
||||
test:
|
||||
image: elixir:latest
|
||||
dependencies:
|
||||
- build
|
||||
stage: test
|
||||
script:
|
||||
- mix test
|
||||
|
||||
credo:
|
||||
image: elixir:latest
|
||||
dependencies:
|
||||
- build
|
||||
stage: test
|
||||
script:
|
||||
- mix credo
|
||||
|
||||
audit:
|
||||
image: elixir:latest
|
||||
dependencies:
|
||||
- build
|
||||
stage: test
|
||||
script:
|
||||
- mix hex.audit
|
||||
|
||||
format:
|
||||
image: elixir:latest
|
||||
dependencies:
|
||||
- build
|
||||
stage: test
|
||||
script:
|
||||
- mix format --check-formatted
|
||||
|
||||
pages:
|
||||
image: elixir:latest
|
||||
dependencies:
|
||||
- build
|
||||
stage: release
|
||||
script:
|
||||
- mix docs -o public
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- main
|
||||
|
||||
git_ops:
|
||||
image: elixir:latest
|
||||
dependencies:
|
||||
- build
|
||||
stage: release
|
||||
only:
|
||||
- main
|
||||
except:
|
||||
variables:
|
||||
- $CI_COMMIT_MESSAGE =~ /chore\:\ release version/
|
||||
script:
|
||||
- |
|
||||
export OLD_APP_VERSION=$APP_VERSION
|
||||
mkdir -p artifacts
|
||||
git config --global user.name "Gitlab Runner for ${GITLAB_USER_NAME}"
|
||||
git config --global user.email "${GITLAB_USER_EMAIL}"
|
||||
mix git_ops.release --yes || true
|
||||
mix git_ops.project_info -f shell > artifacts/env
|
||||
source artifacts/env
|
||||
if [ "v${OLD_APP_VERSION}" != "v${APP_VERSION}" ]; then
|
||||
mix hex.build -o "artifacts/${APP_NAME}-${APP_VERSION}.tar"
|
||||
gzip "artifacts/${APP_NAME}-${APP_VERSION}.tar"
|
||||
mix docs && tar zcvf "artifacts/${APP_NAME}-${APP_VERSION}-docs.tar.gz" doc/
|
||||
curl --header "JOB_TOKEN: ${CI_JOB_TOKEN}" --upload-file "artifacts/${APP_NAME}-${APP_VERSION}.tar.gz" "${PACKAGE_REGISTRY_URL}/${APP_NAME}/${APP_VERSION}/${APP_NAME}-${APP_VERSION}.tar.gz"
|
||||
curl --header "JOB_TOKEN: ${CI_JOB_TOKEN}" --upload-file "artifacts/${APP_NAME}-${APP_VERSION}-docs.tar.gz" "${PACKAGE_REGISTRY_URL}/${APP_NAME}/${APP_VERSION}/${APP_NAME}-${APP_VERSION}-docs.tar.gz"
|
||||
git push "https://project_${CI_PROJECT_ID}_bot:${RELEASE_TOKEN}@gitlab.com/${CI_PROJECT_PATH}.git" "HEAD:${CI_COMMIT_REF_NAME}" "refs/tags/v${APP_VERSION}"
|
||||
fi
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*
|
||||
|
||||
release-gitlab:
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
dependencies:
|
||||
- build
|
||||
stage: release
|
||||
only:
|
||||
- tags
|
||||
- /^v\d+\.\d+\.\d+(-\w+)?$/
|
||||
script:
|
||||
- release-cli create \
|
||||
--name "Release ${APP_NAME} ${APP_VERSION}" \
|
||||
--description "./CHANGELOG.md" \
|
||||
--tag-name "v${APP_VERSION}" \
|
||||
--assets-link "{\"name\":\"${APP_NAME}-${APP_VERSION}.tar.gz\",\"url\":\"${PACKAGE_REGISTRY_URL}/${APP_NAME}/${APP_VERSION}/${APP_NAME}-${APP_VERSION}.tar.gz\"}" \
|
||||
--assets-link "{\"name\":\"${APP_NAME}-${APP_VERSION}-docs.tar.gz\",\"url\":\"${PACKAGE_REGISTRY_URL}/${APP_NAME}/${APP_VERSION}/${APP_NAME}-${APP_VERSION}-docs.tar.gz\"}"
|
||||
# We're not ready to do hex releases yet.
|
||||
# release-hex:
|
||||
# image: elixir:latest
|
||||
# dependencies:
|
||||
# - build
|
||||
# stage: release
|
||||
# only:
|
||||
# - tags
|
||||
# - /^v\d+\.\d+\.\d+(-\w+)?$/
|
||||
# script:
|
||||
# - mix hex.publish --yes
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
|
@ -0,0 +1,2 @@
|
|||
erlang 26.0.2
|
||||
elixir 1.15.4
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -5,6 +5,31 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
|
|||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.0.0](https://gitlab.com/jimsy/gcode/compare/v0.4.1...v1.0.0) (2023-01-16)
|
||||
### Breaking Changes:
|
||||
|
||||
* Relicense to HL3-FULL.
|
||||
|
||||
|
||||
|
||||
## [v0.4.1](https://gitlab.com/jimsy/gcode/compare/v0.4.0...v0.4.1) (2021-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* bug in parsing not returning errors
|
||||
|
||||
## [v0.4.0](https://gitlab.com/jimsy/gcode/compare/v0.3.0...v0.4.0) (2021-01-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Implement (a common subset) G-code parsing.
|
||||
|
||||
## [v0.3.0](https://gitlab.com/jimsy/gcode/compare/v0.2.1...v0.3.0) (2021-01-05)
|
||||
|
||||
|
||||
|
|
16
LICENSE
16
LICENSE
|
@ -1,16 +0,0 @@
|
|||
Copyright 2021 James Harton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
* No Harm: The software may not be used by anyone for systems or activities that actively and knowingly endanger, harm, or otherwise threaten the physical, mental, economic, or general well-being of other individuals or groups, in violation of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/).
|
||||
|
||||
* Services: If the Software is used to provide a service to others, the licensee shall, as a condition of use, require those others not to use the service in any way that violates the No Harm clause above.
|
||||
|
||||
* Enforceability: If any portion or provision of this License shall to any extent be declared illegal or unenforceable by a court of competent jurisdiction, then the remainder of this License, or the application of such portion or provision in circumstances other than those as to which it is so declared illegal or unenforceable, shall not be affected thereby, and each portion and provision of this Agreement shall be valid and enforceable to the fullest extent permitted by law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
This Hippocratic License is an Ethical Source license (https://ethicalsource.dev) derived from the MIT License, amended to limit the impact of the unethical use of open source software.
|
||||
|
151
LICENSE.md
Normal file
151
LICENSE.md
Normal file
|
@ -0,0 +1,151 @@
|
|||
Copyright 2022 James Harton ("Licensor")
|
||||
|
||||
**HIPPOCRATIC LICENSE**
|
||||
|
||||
**Version 3.0, October 2021**
|
||||
|
||||
<https://firstdonoharm.dev/version/3/0/full.md>
|
||||
|
||||
**TERMS AND CONDITIONS**
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, COPY, MODIFICATION, PREPARATION OF DERIVATIVE WORK, REPRODUCTION, AND DISTRIBUTION:
|
||||
|
||||
**[1.](#1) DEFINITIONS:**
|
||||
|
||||
_This section defines certain terms used throughout this license agreement._
|
||||
|
||||
[1.1.](#1.1) “License” means the terms and conditions, as stated herein, for use, copy, modification, preparation of derivative work, reproduction, and distribution of Software (as defined below).
|
||||
|
||||
[1.2.](#1.2) “Licensor” means the copyright and/or patent owner or entity authorized by the copyright and/or patent owner that is granting the License.
|
||||
|
||||
[1.3.](#1.3) “Licensee” means the individual or entity exercising permissions granted by this License, including the use, copy, modification, preparation of derivative work, reproduction, and distribution of Software (as defined below).
|
||||
|
||||
[1.4.](#1.4) “Software” means any copyrighted work, including but not limited to software code, authored by Licensor and made available under this License.
|
||||
|
||||
[1.5.](#1.5) “Supply Chain” means the sequence of processes involved in the production and/or distribution of a commodity, good, or service offered by the Licensee.
|
||||
|
||||
[1.6.](#1.6) “Supply Chain Impacted Party” or “Supply Chain Impacted Parties” means any person(s) directly impacted by any of Licensee’s Supply Chain, including the practices of all persons or entities within the Supply Chain prior to a good or service reaching the Licensee.
|
||||
|
||||
[1.7.](#1.7) “Duty of Care” is defined by its use in tort law, delict law, and/or similar bodies of law closely related to tort and/or delict law, including without limitation, a requirement to act with the watchfulness, attention, caution, and prudence that a reasonable person in the same or similar circumstances would use towards any Supply Chain Impacted Party.
|
||||
|
||||
[1.8.](#1.8) “Worker” is defined to include any and all permanent, temporary, and agency workers, as well as piece-rate, salaried, hourly paid, legal young (minors), part-time, night, and migrant workers.
|
||||
|
||||
**[2.](#2) INTELLECTUAL PROPERTY GRANTS:**
|
||||
|
||||
_This section identifies intellectual property rights granted to a Licensee_.
|
||||
|
||||
[2.1.](#2.1) _Grant of Copyright License_: Subject to the terms and conditions of this License, Licensor hereby grants to Licensee a worldwide, non-exclusive, no-charge, royalty-free copyright license to use, copy, modify, prepare derivative work, reproduce, or distribute the Software, Licensor authored modified software, or other work derived from the Software.
|
||||
|
||||
[2.2.](#2.2) _Grant of Patent License_: Subject to the terms and conditions of this License, Licensor hereby grants Licensee a worldwide, non-exclusive, no-charge, royalty-free patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer Software.
|
||||
|
||||
**[3.](#3) ETHICAL STANDARDS:**
|
||||
|
||||
_This section lists conditions the Licensee must comply with in order to have rights under this License._
|
||||
|
||||
The rights granted to the Licensee by this License are expressly made subject to the Licensee’s ongoing compliance with the following conditions:
|
||||
|
||||
* [3.1.](#3.1) The Licensee SHALL NOT, whether directly or indirectly, through agents or assigns:
|
||||
* [3.1.1.](#3.1.1) Infringe upon any person’s right to life or security of person, engage in extrajudicial killings, or commit murder, without lawful cause (See Article 3, _United Nations Universal Declaration of Human Rights_; Article 6, _International Covenant on Civil and Political Rights_)
|
||||
* [3.1.2.](#3.1.2) Hold any person in slavery, servitude, or forced labor (See Article 4, _United Nations Universal Declaration of Human Rights_; Article 8, _International Covenant on Civil and Political Rights_);
|
||||
* [3.1.3.](#3.1.3) Contribute to the institution of slavery, slave trading, forced labor, or unlawful child labor (See Article 4, _United Nations Universal Declaration of Human Rights_; Article 8, _International Covenant on Civil and Political Rights_);
|
||||
* [3.1.4.](#3.1.4) Torture or subject any person to cruel, inhumane, or degrading treatment or punishment (See Article 5, _United Nations Universal Declaration of Human Rights_; Article 7, _International Covenant on Civil and Political Rights_);
|
||||
* [3.1.5.](#3.1.5) Discriminate on the basis of sex, gender, sexual orientation, race, ethnicity, nationality, religion, caste, age, medical disability or impairment, and/or any other like circumstances (See Article 7, _United Nations Universal Declaration of Human Rights_; Article 2, _International Covenant on Economic, Social and Cultural Rights_; Article 26, _International Covenant on Civil and Political Rights_);
|
||||
* [3.1.6.](#3.1.6) Prevent any person from exercising his/her/their right to seek an effective remedy by a competent court or national tribunal (including domestic judicial systems, international courts, arbitration bodies, and other adjudicating bodies) for actions violating the fundamental rights granted to him/her/them by applicable constitutions, applicable laws, or by this License (See Article 8, _United Nations Universal Declaration of Human Rights_; Articles 9 and 14, _International Covenant on Civil and Political Rights_);
|
||||
* [3.1.7.](#3.1.7) Subject any person to arbitrary arrest, detention, or exile (See Article 9, _United Nations Universal Declaration of Human Rights_; Article 9, _International Covenant on Civil and Political Rights_);
|
||||
* [3.1.8.](#3.1.8) Subject any person to arbitrary interference with a person’s privacy, family, home, or correspondence without the express written consent of the person (See Article 12, _United Nations Universal Declaration of Human Rights_; Article 17, _International Covenant on Civil and Political Rights_);
|
||||
* [3.1.9.](#3.1.9) Arbitrarily deprive any person of his/her/their property (See Article 17, _United Nations Universal Declaration of Human Rights_);
|
||||
* [3.1.10.](#3.1.10) Forcibly remove indigenous peoples from their lands or territories or take any action with the aim or effect of dispossessing indigenous peoples from their lands, territories, or resources, including without limitation the intellectual property or traditional knowledge of indigenous peoples, without the free, prior, and informed consent of indigenous peoples concerned (See Articles 8 and 10, _United Nations Declaration on the Rights of Indigenous Peoples_);
|
||||
* [3.1.11.](#3.1.11) _Fossil Fuel Divestment_: Be an individual or entity, or a representative, agent, affiliate, successor, attorney, or assign of an individual or entity, on the [FFI Solutions Carbon Underground 200 list](https://www.ffisolutions.com/research-analytics-index-solutions/research-screening/the-carbon-underground-200/?cn-reloaded=1);
|
||||
* [3.1.12.](#3.1.12) _Ecocide_: Commit ecocide:
|
||||
* [3.1.12.1.](#3.1.12.1) For the purpose of this section, “ecocide” means unlawful or wanton acts committed with knowledge that there is a substantial likelihood of severe and either widespread or long-term damage to the environment being caused by those acts;
|
||||
* [3.1.12.2.](#3.1.12.2) For the purpose of further defining ecocide and the terms contained in the previous paragraph:
|
||||
* [3.1.12.2.1.](#3.1.12.2.1) “Wanton” means with reckless disregard for damage which would be clearly excessive in relation to the social and economic benefits anticipated;
|
||||
* [3.1.12.2.2.](#3.1.12.2.2) “Severe” means damage which involves very serious adverse changes, disruption, or harm to any element of the environment, including grave impacts on human life or natural, cultural, or economic resources;
|
||||
* [3.1.12.2.3.](#3.1.12.2.3) “Widespread” means damage which extends beyond a limited geographic area, crosses state boundaries, or is suffered by an entire ecosystem or species or a large number of human beings;
|
||||
* [3.1.12.2.4.](#3.1.12.2.4) “Long-term” means damage which is irreversible or which cannot be redressed through natural recovery within a reasonable period of time; and
|
||||
* [3.1.12.2.5.](#3.1.12.2.5) “Environment” means the earth, its biosphere, cryosphere, lithosphere, hydrosphere, and atmosphere, as well as outer space
|
||||
(See Section II, _Independent Expert Panel for the Legal Definition of Ecocide_, Stop Ecocide Foundation and the Promise Institute for Human Rights at UCLA School of Law, June 2021);
|
||||
* [3.1.13.](#3.1.13) _Extractive Industries_: Be an individual or entity, or a representative, agent, affiliate, successor, attorney, or assign of an individual or entity, that engages in fossil fuel or mineral exploration, extraction, development, or sale;
|
||||
* [3.1.14.](#3.1.14) _Boycott / Divestment / Sanctions_: Be an individual or entity, or a representative, agent, affiliate, successor, attorney, or assign of an individual or entity, identified by the Boycott, Divestment, Sanctions (“BDS”) movement on its website (<https://bdsmovement.net/> and <https://bdsmovement.net/get-involved/what-to-boycott>) as a target for boycott;
|
||||
* [3.1.15.](#3.1.15) _Taliban_: Be an individual or entity that:
|
||||
* [3.1.15.1.](#3.1.15.1) engages in any commercial transactions with the Taliban; or
|
||||
* [3.1.15.2.](#3.1.15.2) is a representative, agent, affiliate, successor, attorney, or assign of the Taliban;
|
||||
* [3.1.16.](#3.1.16) _Myanmar_: Be an individual or entity that:
|
||||
* [3.1.16.1.](#3.1.16.1) engages in any commercial transactions with the Myanmar/Burmese military junta; or
|
||||
* [3.1.16.2.](#3.1.16.2) is a representative, agent, affiliate, successor, attorney, or assign of the Myanmar/Burmese government;
|
||||
* [3.1.17.](#3.1.17) _Xinjiang Uygur Autonomous Region_: Be an individual or entity, or a representative, agent, affiliate, successor, attorney, or assign of any individual or entity, that does business in, purchases goods from, or otherwise benefits from goods produced in the Xinjiang Uygur Autonomous Region of China;
|
||||
* [3.1.18.](#3.1.18) _US Tariff Act_: Be an individual or entity:
|
||||
* [3.1.18.1.](#3.1.18.1) which U.S. Customs and Border Protection (CBP) has currently issued a Withhold Release Order (WRO) or finding against based on reasonable suspicion of forced labor; or
|
||||
* [3.1.18.2.](#3.1.18.2) that is a representative, agent, affiliate, successor, attorney, or assign of an individual or entity that does business with an individual or entity which currently has a WRO or finding from CBP issued against it based on reasonable suspicion of forced labor;
|
||||
* [3.1.19.](#3.1.19) _Mass Surveillance_: Be a government agency or multinational corporation, or a representative, agent, affiliate, successor, attorney, or assign of a government or multinational corporation, which participates in mass surveillance programs;
|
||||
* [3.1.20.](#3.1.20) _Military Activities_: Be an entity or a representative, agent, affiliate, successor, attorney, or assign of an entity which conducts military activities;
|
||||
* [3.1.21.](#3.1.21) _Law Enforcement_: Be an individual or entity, or a or a representative, agent, affiliate, successor, attorney, or assign of an individual or entity, that provides good or services to, or otherwise enters into any commercial contracts with, any local, state, or federal law enforcement agency;
|
||||
* [3.1.22.](#3.1.22) _Media_: Be an individual or entity, or a or a representative, agent, affiliate, successor, attorney, or assign of an individual or entity, that broadcasts messages promoting killing, torture, or other forms of extreme violence;
|
||||
* [3.1.23.](#3.1.23) Interfere with Workers' free exercise of the right to organize and associate (See Article 20, United Nations Universal Declaration of Human Rights; C087 - Freedom of Association and Protection of the Right to Organise Convention, 1948 (No. 87), International Labour Organization; Article 8, International Covenant on Economic, Social and Cultural Rights); and
|
||||
* [3.1.24.](#3.1.24) Harm the environment in a manner inconsistent with local, state, national, or international law.
|
||||
* [3.2.](#3.2) The Licensee SHALL:
|
||||
* [3.2.1.](#3.2.1) _Social Auditing_: Only use social auditing mechanisms that adhere to Worker-Driven Social Responsibility Network’s Statement of Principles (<https://wsr-network.org/what-is-wsr/statement-of-principles/>) over traditional social auditing mechanisms, to the extent the Licensee uses any social auditing mechanisms at all;
|
||||
* [3.2.2.](#3.2.2) _Workers on Board of Directors_: Ensure that if the Licensee has a Board of Directors, 30% of Licensee’s board seats are held by Workers paid no more than 200% of the compensation of the lowest paid Worker of the Licensee;
|
||||
* [3.2.3.](#3.2.3) _Supply Chain_: Provide clear, accessible supply chain data to the public in accordance with the following conditions:
|
||||
* [3.2.3.1.](#3.2.3.1) All data will be on Licensee’s website and/or, to the extent Licensee is a representative, agent, affiliate, successor, attorney, subsidiary, or assign, on Licensee’s principal’s or parent’s website or some other online platform accessible to the public via an internet search on a common internet search engine; and
|
||||
* [3.2.3.2.](#3.2.3.2) Data published will include, where applicable, manufacturers, top tier suppliers, subcontractors, cooperatives, component parts producers, and farms;
|
||||
* [3.2.4.](#3.2.4) Provide equal pay for equal work where the performance of such work requires equal skill, effort, and responsibility, and which are performed under similar working conditions, except where such payment is made pursuant to:
|
||||
* [3.2.4.1.](#3.2.4.1) A seniority system;
|
||||
* [3.2.4.2.](#3.2.4.2) A merit system;
|
||||
* [3.2.4.3.](#3.2.4.3) A system which measures earnings by quantity or quality of production; or
|
||||
* [3.2.4.4.](#3.2.4.4) A differential based on any other factor other than sex, gender, sexual orientation, race, ethnicity, nationality, religion, caste, age, medical disability or impairment, and/or any other like circumstances (See 29 U.S.C.A. § 206(d)(1); Article 23, _United Nations Universal Declaration of Human Rights_; Article 7, _International Covenant on Economic, Social and Cultural Rights_; Article 26, _International Covenant on Civil and Political Rights_); and
|
||||
* [3.2.5.](#3.2.5) Allow for reasonable limitation of working hours and periodic holidays with pay (See Article 24, _United Nations Universal Declaration of Human Rights_; Article 7, _International Covenant on Economic, Social and Cultural Rights_).
|
||||
|
||||
**[4.](#4) SUPPLY CHAIN IMPACTED PARTIES:**
|
||||
|
||||
_This section identifies additional individuals or entities that a Licensee could harm as a result of violating the Ethical Standards section, the condition that the Licensee must voluntarily accept a Duty of Care for those individuals or entities, and the right to a private right of action that those individuals or entities possess as a result of violations of the Ethical Standards section._
|
||||
|
||||
[4.1.](#4.1) In addition to the above Ethical Standards, Licensee voluntarily accepts a Duty of Care for Supply Chain Impacted Parties of this License, including individuals and communities impacted by violations of the Ethical Standards. The Duty of Care is breached when a provision within the Ethical Standards section is violated by a Licensee, one of its successors or assigns, or by an individual or entity that exists within the Supply Chain prior to a good or service reaching the Licensee.
|
||||
|
||||
[4.2.](#4.2) Breaches of the Duty of Care, as stated within this section, shall create a private right of action, allowing any Supply Chain Impacted Party harmed by the Licensee to take legal action against the Licensee in accordance with applicable negligence laws, whether they be in tort law, delict law, and/or similar bodies of law closely related to tort and/or delict law, regardless if Licensee is directly responsible for the harms suffered by a Supply Chain Impacted Party. Nothing in this section shall be interpreted to include acts committed by individuals outside of the scope of his/her/their employment.
|
||||
|
||||
[5.](#5) **NOTICE:** _This section explains when a Licensee must notify others of the License._
|
||||
|
||||
[5.1.](#5.1) _Distribution of Notice_: Licensee must ensure that everyone who receives a copy of or uses any part of Software from Licensee, with or without changes, also receives the License and the copyright notice included with Software (and if included by the Licensor, patent, trademark, and attribution notice). Licensee must ensure that License is prominently displayed so that any individual or entity seeking to download, copy, use, or otherwise receive any part of Software from Licensee is notified of this License and its terms and conditions. Licensee must cause any modified versions of the Software to carry prominent notices stating that Licensee changed the Software.
|
||||
|
||||
[5.2.](#5.2) _Modified Software_: Licensee is free to create modifications of the Software and distribute only the modified portion created by Licensee, however, any derivative work stemming from the Software or its code must be distributed pursuant to this License, including this Notice provision.
|
||||
|
||||
[5.3.](#5.3) _Recipients as Licensees_: Any individual or entity that uses, copies, modifies, reproduces, distributes, or prepares derivative work based upon the Software, all or part of the Software’s code, or a derivative work developed by using the Software, including a portion of its code, is a Licensee as defined above and is subject to the terms and conditions of this License.
|
||||
|
||||
**[6.](#6) REPRESENTATIONS AND WARRANTIES:**
|
||||
|
||||
[6.1.](#6.1) _Disclaimer of Warranty_: TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES “AS IS,” WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR SHALL NOT BE LIABLE TO ANY PERSON OR ENTITY FOR ANY DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THIS LICENSE, UNDER ANY LEGAL CLAIM.
|
||||
|
||||
[6.2.](#6.2) _Limitation of Liability_: LICENSEE SHALL HOLD LICENSOR HARMLESS AGAINST ANY AND ALL CLAIMS, DEBTS, DUES, LIABILITIES, LIENS, CAUSES OF ACTION, DEMANDS, OBLIGATIONS, DISPUTES, DAMAGES, LOSSES, EXPENSES, ATTORNEYS' FEES, COSTS, LIABILITIES, AND ALL OTHER CLAIMS OF EVERY KIND AND NATURE WHATSOEVER, WHETHER KNOWN OR UNKNOWN, ANTICIPATED OR UNANTICIPATED, FORESEEN OR UNFORESEEN, ACCRUED OR UNACCRUED, DISCLOSED OR UNDISCLOSED, ARISING OUT OF OR RELATING TO LICENSEE’S USE OF THE SOFTWARE. NOTHING IN THIS SECTION SHOULD BE INTERPRETED TO REQUIRE LICENSEE TO INDEMNIFY LICENSOR, NOR REQUIRE LICENSOR TO INDEMNIFY LICENSEE.
|
||||
|
||||
**[7.](#7) TERMINATION**
|
||||
|
||||
[7.1.](#7.1) _Violations of Ethical Standards or Breaching Duty of Care_: If Licensee violates the Ethical Standards section or Licensee, or any other person or entity within the Supply Chain prior to a good or service reaching the Licensee, breaches its Duty of Care to Supply Chain Impacted Parties, Licensee must remedy the violation or harm caused by Licensee within 30 days of being notified of the violation or harm. If Licensee fails to remedy the violation or harm within 30 days, all rights in the Software granted to Licensee by License will be null and void as between Licensor and Licensee.
|
||||
|
||||
[7.2.](#7.2) _Failure of Notice_: If any person or entity notifies Licensee in writing that Licensee has not complied with the Notice section of this License, Licensee can keep this License by taking all practical steps to comply within 30 days after the notice of noncompliance. If Licensee does not do so, Licensee’s License (and all rights licensed hereunder) will end immediately.
|
||||
|
||||
[7.3.](#7.3) _Judicial Findings_: In the event Licensee is found by a civil, criminal, administrative, or other court of competent jurisdiction, or some other adjudicating body with legal authority, to have committed actions which are in violation of the Ethical Standards or Supply Chain Impacted Party sections of this License, all rights granted to Licensee by this License will terminate immediately.
|
||||
|
||||
[7.4.](#7.4) _Patent Litigation_: If Licensee institutes patent litigation against any entity (including a cross-claim or counterclaim in a suit) alleging that the Software, all or part of the Software’s code, or a derivative work developed using the Software, including a portion of its code, constitutes direct or contributory patent infringement, then any patent license, along with all other rights, granted to Licensee under this License will terminate as of the date such litigation is filed.
|
||||
|
||||
[7.5.](#7.5) _Additional Remedies_: Termination of the License by failing to remedy harms in no way prevents Licensor or Supply Chain Impacted Party from seeking appropriate remedies at law or in equity.
|
||||
|
||||
**[8.](#8) MISCELLANEOUS:**
|
||||
|
||||
[8.1.](#8.1) _Conditions_: Sections 3, 4.1, 5.1, 5.2, 7.1, 7.2, 7.3, and 7.4 are conditions of the rights granted to Licensee in the License.
|
||||
|
||||
[8.2.](#8.2) _Equitable Relief_: Licensor and any Supply Chain Impacted Party shall be entitled to equitable relief, including injunctive relief or specific performance of the terms hereof, in addition to any other remedy to which they are entitled at law or in equity.
|
||||
|
||||
[8.3.](#8.3) _Copyleft_: Modified software, source code, or other derivative work must be licensed, in its entirety, under the exact same conditions as this License.
|
||||
|
||||
[8.4.](#8.4) _Severability_: If any term or provision of this License is determined to be invalid, illegal, or unenforceable by a court of competent jurisdiction, any such determination of invalidity, illegality, or unenforceability shall not affect any other term or provision of this License or invalidate or render unenforceable such term or provision in any other jurisdiction. If the determination of invalidity, illegality, or unenforceability by a court of competent jurisdiction pertains to the terms or provisions contained in the Ethical Standards section of this License, all rights in the Software granted to Licensee shall be deemed null and void as between Licensor and Licensee.
|
||||
|
||||
[8.5.](#8.5) _Section Titles_: Section titles are solely written for organizational purposes and should not be used to interpret the language within each section.
|
||||
|
||||
[8.6.](#8.6) _Citations_: Citations are solely written to provide context for the source of the provisions in the Ethical Standards.
|
||||
|
||||
[8.7.](#8.7) _Section Summaries_: Some sections have a brief _italicized description_ which is provided for the sole purpose of briefly describing the section and should not be used to interpret the terms of the License.
|
||||
|
||||
[8.8.](#8.8) _Entire License_: This is the entire License between the Licensor and Licensee with respect to the claims released herein and that the consideration stated herein is the only consideration or compensation to be paid or exchanged between them for this License. This License cannot be modified or amended except in a writing signed by Licensor and Licensee.
|
||||
|
||||
[8.9.](#8.9) _Successors and Assigns_: This License shall be binding upon and inure to the benefit of the Licensor’s and Licensee’s respective heirs, successors, and assigns.
|
37
README.md
37
README.md
|
@ -1,21 +1,42 @@
|
|||
# G-code
|
||||
|
||||
[![Build Status](https://drone.harton.dev/api/badges/james/gcode/status.svg?ref=refs/heads/main)](https://drone.harton.dev/james/gcode)
|
||||
[![Hex.pm](https://img.shields.io/hexpm/v/gcode.svg)](https://hex.pm/packages/gcode)
|
||||
[![Hippocratic License HL3-FULL](https://img.shields.io/static/v1?label=Hippocratic%20License&message=HL3-FULL&labelColor=5e2751&color=bc8c3d)](https://firstdonoharm.dev/version/3/0/full.html)
|
||||
|
||||
`gcode` is an Elixir library for parsing and generating [G-code](https://en.wikipedia.org/wiki/G-code), which is a common language for working with CNC machines and 3D printers.
|
||||
|
||||
## Installation
|
||||
|
||||
This package is not yet available in [Hex](https://hex.pm/), because it's not ready yet.
|
||||
Gcode is [available in Hex](https://hex.pm/packages/gcode), the package can be
|
||||
installed by adding `gcode` to your list of dependencies in `mix.exs`:
|
||||
|
||||
You can install it from git by adding `{:gcode, git: "https://gitlab.com/jimsy/gcode", tag: "v0.2.1"}` to your dependencies.
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:gcode, "~> 1.0.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Documentation for the latest release can be found on
|
||||
[HexDocs](https://hexdocs.pm/gcode) and for the `main` branch on
|
||||
[docs.harton.nz](https://docs.harton.nz/james/gcode).
|
||||
|
||||
Documentation for the main branch of this package is always available [on gitlab pages](https://jimsy.gitlab.io/gcode)
|
||||
## Github Mirror
|
||||
|
||||
## Contributing
|
||||
|
||||
This project uses the wonderful [git_ops](https://hex.pm/packages/git_ops) package to automate releases and as such requires the use of [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
||||
This repository is mirrored [on Github](https://github.com/jimsynz/gcode)
|
||||
from it's primary location [on my Forgejo instance](https://harton.dev/james/gcode).
|
||||
Feel free to raise issues and open PRs on Github.
|
||||
|
||||
## License
|
||||
|
||||
This library is licensed with the [Hippocratic License 2.1](https://firstdonoharm.dev/version/2/1/license.html).
|
||||
This software is licensed under the terms of the
|
||||
[HL3-FULL](https://firstdonoharm.dev), see the `LICENSE.md` file included with
|
||||
this package for the terms.
|
||||
|
||||
This license actively proscribes this software being used by and for some
|
||||
industries, countries and activities. If your usage of this software doesn't
|
||||
comply with the terms of this license, then [contact me](mailto:james@harton.nz)
|
||||
with the details of your use-case to organise the purchase of a license - the
|
||||
cost of which may include a donation to a suitable charity or NGO.
|
||||
|
|
|
@ -3,7 +3,7 @@ import Config
|
|||
config :git_ops,
|
||||
mix_project: Mix.Project.get!(),
|
||||
changelog_file: "CHANGELOG.md",
|
||||
repository_url: "https://gitlab.com/jimsy/gcode",
|
||||
repository_url: "https://harton.dev/james/gcode",
|
||||
manage_mix_version?: true,
|
||||
manage_readme_version: "README.md",
|
||||
version_tag_prefix: "v"
|
||||
|
|
20
lib/gcode.ex
20
lib/gcode.ex
|
@ -1,8 +1,20 @@
|
|||
defmodule Gcode do
|
||||
alias Gcode.{Model.Program, Model.Serialise, Result}
|
||||
alias Gcode.{Model.Program, Model.Serialise}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Documentation for `Gcode`.
|
||||
Gcode - a library for parsing and serialising G-code.
|
||||
|
||||
If you haven't heard of G-code before, then you probably don't need this
|
||||
library, but if you're working with CNC machines or 3D printers then G-code is
|
||||
the defacto standard for working with these machines. As such it behoves us
|
||||
to have first class support for working with G-code in Elixir.
|
||||
|
||||
You're welcome.
|
||||
|
||||
For functions related to parsing G-code files and commands, see the `Parser`
|
||||
module. For generating your own programs see the contents of `Model`, and for
|
||||
converting programs back into G-code see the `Model.Serialise` protocol.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
|
@ -12,9 +24,7 @@ defmodule Gcode do
|
|||
def serialise(%Program{} = program) do
|
||||
program
|
||||
|> Serialise.serialise()
|
||||
|> Result.Enum.map(fn block ->
|
||||
{:ok, "#{block}\r\n"}
|
||||
end)
|
||||
|> Result.Enum.map(&ok("#{&1}\r\n"))
|
||||
|> Result.Enum.join("")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,16 +2,21 @@ defmodule Gcode.Model.Block do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct words: [], comment: none()
|
||||
alias Gcode.Model.{Block, Comment, Skip, Word}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
alias Gcode.Model.{Block, Comment, Expr, Skip, Word}
|
||||
|
||||
defguardp is_pushable(value)
|
||||
when is_struct(value, Block) or is_struct(value, Comment) or is_struct(value, Skip) or
|
||||
is_struct(value, Word) or is_expression(value)
|
||||
|
||||
@moduledoc """
|
||||
A sequence of G-code words.
|
||||
A sequence of G-code words on a single line.
|
||||
"""
|
||||
|
||||
@type t :: %Block{words: [block_contents], comment: Option.t(Comment)}
|
||||
@typedoc "Any error results in this module will return this type"
|
||||
@type block_error :: {:block_error, String.t()}
|
||||
@type block_contents :: Word.t() | Skip.t()
|
||||
@type block_contents :: Word.t() | Skip.t() | Expr.t()
|
||||
|
||||
@doc """
|
||||
Initialise a new empty G-code program.
|
||||
|
@ -37,20 +42,10 @@ defmodule Gcode.Model.Block do
|
|||
...> {:ok, block} = Block.comment(block, comment)
|
||||
...> Result.ok?(block.comment)
|
||||
true
|
||||
|
||||
iex> {:ok, comment} = Comment.init("Jen, in the swing seat, with her night terrors")
|
||||
...> {:ok, block} = Block.init()
|
||||
...> {:ok, block} = Block.comment(block, comment)
|
||||
...> Block.comment(block, comment)
|
||||
{:error, {:block_error, "Block already contains a comment"}}
|
||||
|
||||
"""
|
||||
@spec comment(t, Comment.t()) :: Result.t(t, block_error)
|
||||
def comment(%Block{comment: none()} = block, %Comment{} = comment),
|
||||
do: {:ok, %Block{block | comment: some(comment)}}
|
||||
|
||||
def comment(%Block{comment: some(_)}, _comment),
|
||||
do: {:error, {:block_error, "Block already contains a comment"}}
|
||||
def comment(%Block{} = block, comment),
|
||||
do: ok(%Block{block | comment: some(comment)})
|
||||
|
||||
@doc """
|
||||
Pushes a `Word` onto the word list.
|
||||
|
@ -66,19 +61,26 @@ defmodule Gcode.Model.Block do
|
|||
...> {:ok, block} = Block.push(block, word)
|
||||
...> {:ok, word} = Word.init("N", 100)
|
||||
...> Block.push(block, word)
|
||||
{:ok, %Block{words: [%Word{word: "N", address: 100}, %Word{word: "G", address: 0}]}}
|
||||
{:ok, %Block{words: [%Word{word: "N", address: %Integer{i: 100}}, %Word{word: "G", address: %Integer{i: 0}}]}}
|
||||
"""
|
||||
@spec push(t, block_contents) :: Result.t(t, block_error)
|
||||
def push(%Block{words: words} = block, word)
|
||||
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
|
||||
do: {:ok, %Block{block | words: [word | words]}}
|
||||
def push(%Block{words: words} = block, pushable)
|
||||
when is_pushable(pushable) and is_list(words),
|
||||
do: ok(%Block{block | words: [pushable | words]})
|
||||
|
||||
def push(%Block{words: words}, word)
|
||||
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
|
||||
do:
|
||||
{:error,
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
|
||||
def push(%Block{words: words}, pushable) when is_pushable(pushable),
|
||||
do:
|
||||
error(
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
|
||||
)
|
||||
|
||||
def push(%Block{words: words}, pushable) when is_list(words),
|
||||
do:
|
||||
error(
|
||||
{:block_error,
|
||||
"Expected element to be pushable, but it is not. Received #{inspect(pushable)}"}
|
||||
)
|
||||
|
||||
@doc """
|
||||
An accessor which returns the block's words in the correct order.
|
||||
|
@ -89,14 +91,15 @@ defmodule Gcode.Model.Block do
|
|||
...> {:ok, word} = Word.init("N", 100)
|
||||
...> {:ok, block} = Block.push(block, word)
|
||||
...> Block.words(block)
|
||||
{:ok, [%Word{word: "G", address: 0}, %Word{word: "N", address: 100}]}
|
||||
{:ok, [%Word{word: "G", address: %Integer{i: 0}}, %Word{word: "N", address: %Integer{i: 100}}]}
|
||||
"""
|
||||
@spec words(t) :: Result.t([Word.t()], block_error)
|
||||
def words(%Block{words: words}) when is_list(words), do: {:ok, Enum.reverse(words)}
|
||||
def words(%Block{words: words}) when is_list(words), do: ok(Enum.reverse(words))
|
||||
|
||||
def words(%Block{words: words}),
|
||||
do:
|
||||
{:error,
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
|
||||
error(
|
||||
{:block_error,
|
||||
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -3,6 +3,12 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Block`, meaning that we can convert
|
||||
blocks into human-readable strings.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@spec describe(Block.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(%Block{words: words, comment: some(comment)}, options) do
|
||||
words = describe_words(words, options)
|
||||
|
@ -17,9 +23,9 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
|
|||
defp describe_words(words, options) do
|
||||
words
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(&describe_or_serialise(&1, options))
|
||||
|> Enum.reject(&Option.none?/1)
|
||||
|> Enum.map(&Option.unwrap!/1)
|
||||
|> Stream.map(&describe_or_serialise(&1, options))
|
||||
|> Stream.reject(&Option.none?/1)
|
||||
|> Stream.map(&Option.unwrap!/1)
|
||||
|> Enum.join(", ")
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Block do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Block`, meaning that blocks can be
|
||||
turned into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Block.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Block{words: words, comment: some(comment)}) do
|
||||
words
|
||||
|
|
|
@ -2,6 +2,11 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Comment do
|
|||
alias Gcode.Model.Comment
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Comment`.
|
||||
"""
|
||||
|
||||
@doc "Stubbornly refuse to describe comments"
|
||||
@spec describe(Comment.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(_comment, _opts \\ []), do: none()
|
||||
end
|
||||
|
|
|
@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
|
|||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Comment`, allowing it to be turned
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Comment.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Comment{comment: comment}) when is_binary(comment) do
|
||||
comment
|
||||
|
@ -12,5 +17,5 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
|
|||
|> ok()
|
||||
end
|
||||
|
||||
def serialise(_comment), do: {:error, {:serialise_error, "Invalid comment"}}
|
||||
def serialise(_comment), do: error({:serialise_error, "Invalid comment"})
|
||||
end
|
||||
|
|
15
lib/gcode/model/expr.ex
Normal file
15
lib/gcode/model/expr.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defprotocol Gcode.Model.Expr do
|
||||
use Gcode.Result
|
||||
alias Gcode.Model.Expr
|
||||
|
||||
@moduledoc """
|
||||
A protocol for evaluating expressions.
|
||||
"""
|
||||
|
||||
@type scalar :: number | boolean | String.t()
|
||||
@type expr :: scalar | [scalar]
|
||||
@type result :: Result.t(expr)
|
||||
|
||||
@spec evaluate(Expr.t()) :: result
|
||||
def evaluate(_expr)
|
||||
end
|
34
lib/gcode/model/expr/binary.ex
Normal file
34
lib/gcode/model/expr/binary.ex
Normal file
|
@ -0,0 +1,34 @@
|
|||
defmodule Gcode.Model.Expr.Binary do
|
||||
use Gcode.Option
|
||||
defstruct op: none(), lhs: none(), rhs: none()
|
||||
alias Gcode.Model.{Expr, Expr.Binary}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a binary (or infix) expression in G-code, consisting of two
|
||||
operands (`lhs` and `rhs`) and an operator to apply.
|
||||
"""
|
||||
|
||||
@operators ~w[* / + - == != < <= > >= && || ^]a
|
||||
|
||||
@typedoc "Valid infix operators"
|
||||
@type operator :: :* | :/ | :+ | :- | :== | :!= | :< | :<= | :> | :>= | :&& | :|| | :^
|
||||
|
||||
@type t :: %Binary{op: Option.t(operator), lhs: Option.t(Expr.t()), rhs: Option.t(Expr.t())}
|
||||
|
||||
@doc """
|
||||
Wrap an operator and two expressions in a binary expression.
|
||||
"""
|
||||
@spec init(operator, Expr.t(), Expr.t()) :: Result.t(t)
|
||||
def init(operator, lhs, rhs)
|
||||
when operator in @operators and is_expression(lhs) and is_expression(rhs),
|
||||
do: ok(%Binary{op: some(operator), lhs: some(lhs), rhs: some(rhs)})
|
||||
|
||||
def init(operator, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:expression_error,
|
||||
"Expected an operator and two expressions, but received #{inspect(operator: operator, lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
end
|
151
lib/gcode/model/expr/binary/expr.ex
Normal file
151
lib/gcode/model/expr/binary/expr.ex
Normal file
|
@ -0,0 +1,151 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Binary do
|
||||
alias Gcode.Model.{Expr, Expr.Binary}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Binary`, which will try and perform the
|
||||
infix calculation to the best of it's ability.
|
||||
"""
|
||||
|
||||
@spec evaluate(Binary.t()) :: Expr.result()
|
||||
def evaluate(%Binary{op: some(op), lhs: some(lhs), rhs: some(rhs)}) do
|
||||
with ok(lhs) <- Expr.evaluate(lhs),
|
||||
ok(rhs) <- Expr.evaluate(rhs),
|
||||
do: do_evaluate(op, lhs, rhs)
|
||||
end
|
||||
|
||||
def evaluate(_binary), do: error({:program_error, "Unable to evaluate binary expression"})
|
||||
|
||||
defp do_evaluate(:*, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs * rhs)
|
||||
defp do_evaluate(:*, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs * rhs)
|
||||
|
||||
defp do_evaluate(:*, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a multiplication expression must be the same type, and either integers or floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:/, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs / rhs)
|
||||
|
||||
defp do_evaluate(:/, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a division expression must be the floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:+, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs + rhs)
|
||||
defp do_evaluate(:+, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs + rhs)
|
||||
|
||||
defp do_evaluate(:+, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an addition expression must be the same type, and either integers or floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:-, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs - rhs)
|
||||
defp do_evaluate(:-, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs - rhs)
|
||||
|
||||
defp do_evaluate(:-, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a subtraction expression must be the same type, and either integers or floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:==, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs == rhs)
|
||||
defp do_evaluate(:==, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs == rhs)
|
||||
defp do_evaluate(:==, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs == rhs)
|
||||
|
||||
defp do_evaluate(:==, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an equality expression must be the same type, and either integers, floats or strings. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:!=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs != rhs)
|
||||
defp do_evaluate(:!=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs != rhs)
|
||||
defp do_evaluate(:!=, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs != rhs)
|
||||
|
||||
defp do_evaluate(:!=, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an inequality expression must be the same type, and either integers, floats or strings. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:<, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs < rhs)
|
||||
defp do_evaluate(:<, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs < rhs)
|
||||
|
||||
defp do_evaluate(:<, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an LT expression must be the same type, and either integers or floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:<=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs <= rhs)
|
||||
defp do_evaluate(:<=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs <= rhs)
|
||||
|
||||
defp do_evaluate(:<=, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an LTE expression must be the same type, and either integers or floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:>, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs > rhs)
|
||||
defp do_evaluate(:>, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs > rhs)
|
||||
|
||||
defp do_evaluate(:>, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an GT expression must be the same type, and either integers or floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:>=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs >= rhs)
|
||||
defp do_evaluate(:>=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs >= rhs)
|
||||
|
||||
defp do_evaluate(:>=, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of an GTE expression must be the same type, and either integers or floats. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:&&, lhs, rhs) when is_boolean(lhs) and is_boolean(rhs), do: ok(lhs && rhs)
|
||||
|
||||
defp do_evaluate(:&&, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a logical and expression must be booleans. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:||, lhs, rhs) when is_boolean(lhs) and is_boolean(rhs), do: ok(lhs || rhs)
|
||||
|
||||
defp do_evaluate(:||, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a logical or expression must be booleans. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(:^, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs <> rhs)
|
||||
|
||||
defp do_evaluate(:^, lhs, rhs),
|
||||
do:
|
||||
error(
|
||||
{:program_error,
|
||||
"Both sides of a string concatenation must be strings. Received #{inspect(lhs: lhs, rhs: rhs)}"}
|
||||
)
|
||||
|
||||
defp do_evaluate(op, lhs, rhs),
|
||||
do:
|
||||
error({:program_error, "Invalid infix expression. #{inspect(op: op, lhs: lhs, rhs: rhs)}"})
|
||||
end
|
18
lib/gcode/model/expr/binary/serialse.ex
Normal file
18
lib/gcode/model/expr/binary/serialse.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Binary do
|
||||
alias Gcode.Model.{Expr.Binary, Serialise}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Binary`, allowing them to be convered
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Binary.t()) :: Serialise.result()
|
||||
def serialise(%Binary{op: some(op), lhs: some(lhs), rhs: some(rhs)}) do
|
||||
with ok(lhs) <- Serialise.serialise(lhs),
|
||||
ok(rhs) <- Serialise.serialise(rhs) do
|
||||
ok([lhs, to_string(op), rhs])
|
||||
end
|
||||
end
|
||||
end
|
21
lib/gcode/model/expr/boolean.ex
Normal file
21
lib/gcode/model/expr/boolean.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Gcode.Model.Expr.Boolean do
|
||||
defstruct b: false
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a boolean expression in G-code. Can be either `true` or `false`.
|
||||
"""
|
||||
|
||||
@type t :: %Boolean{b: boolean}
|
||||
|
||||
@doc """
|
||||
Initialise a `Boolean` from a boolean value.
|
||||
"""
|
||||
@spec init(boolean) :: Result.t(t)
|
||||
def init(value) when is_boolean(value),
|
||||
do: ok(%Boolean{b: value})
|
||||
|
||||
def init(value),
|
||||
do: error({:expression_error, "Expected a boolean value, instead received #{inspect(value)}"})
|
||||
end
|
13
lib/gcode/model/expr/boolean/expr.ex
Normal file
13
lib/gcode/model/expr/boolean/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Boolean do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Boolean`, which will return either true or
|
||||
false.
|
||||
"""
|
||||
|
||||
@spec evaluate(Boolean.t()) :: Expr.result()
|
||||
def evaluate(%Boolean{b: b}), do: ok(b)
|
||||
end
|
14
lib/gcode/model/expr/boolean/serialise.ex
Normal file
14
lib/gcode/model/expr/boolean/serialise.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Boolean do
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Boolean`, allowing them to be convered
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Boolean.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Boolean{b: true}), do: ok(["true"])
|
||||
def serialise(%Boolean{b: false}), do: ok(["false"])
|
||||
def serialise(_), do: error({:serialise_error, "Invalid boolean"})
|
||||
end
|
31
lib/gcode/model/expr/constant.ex
Normal file
31
lib/gcode/model/expr/constant.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule Gcode.Model.Expr.Constant do
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct name: none()
|
||||
alias Gcode.Model.Expr.Constant
|
||||
|
||||
@moduledoc """
|
||||
Represents a number of special constant values defined by some G-code
|
||||
controllers:
|
||||
|
||||
* `iterations` - the number of completed iterations of the innermost loop.
|
||||
* `line` - the current line number in the file being executed.
|
||||
* `null` - the null object.
|
||||
* `pi` - the constant π.
|
||||
* `result` - 0 if the last G-, M- or T-command on this input channel was
|
||||
successful, 1 if it returned a warning, 2 if it returned an error.
|
||||
"""
|
||||
|
||||
@type constant :: :iterations | :line | :null | :pi | :result
|
||||
@type t :: %Constant{name: Option.t(constant)}
|
||||
|
||||
@doc """
|
||||
Initialise a `Constant`.
|
||||
"""
|
||||
def init(name) when name in ~w[iterations line null pi result]a,
|
||||
do: ok(%Constant{name: name})
|
||||
|
||||
def init(name),
|
||||
do:
|
||||
error({:expression_error, "Expected a valid constant name, but received #{inspect(name)}"})
|
||||
end
|
25
lib/gcode/model/expr/constant/expr.ex
Normal file
25
lib/gcode/model/expr/constant/expr.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Constant do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Constant
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Constant`, which will return either true
|
||||
or false.
|
||||
|
||||
Currently only knows how to evaluate the following constants:
|
||||
|
||||
* `pi` evaluates to the result of `:math.pi()`
|
||||
* `null` evaulates to `nil`
|
||||
|
||||
Other constants cannot be evaluated at this time, because they need to
|
||||
understand the machine state.
|
||||
"""
|
||||
|
||||
@spec evaluate(Constant.t()) :: Expr.result()
|
||||
def evaluate(%Constant{name: :pi}), do: ok(:math.pi())
|
||||
def evaluate(%Constant{name: :null}), do: ok(nil)
|
||||
|
||||
def evaluate(%Constant{name: name}),
|
||||
do: error({:program_error, "Unable to evaluate constant `#{name}`"})
|
||||
end
|
12
lib/gcode/model/expr/constant/serialise.ex
Normal file
12
lib/gcode/model/expr/constant/serialise.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Constant do
|
||||
alias Gcode.Model.Expr.Constant
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Constant`, allowing them to be
|
||||
convered into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Constant.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Constant{name: name}), do: ok([to_string(name)])
|
||||
end
|
21
lib/gcode/model/expr/float.ex
Normal file
21
lib/gcode/model/expr/float.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Gcode.Model.Expr.Float do
|
||||
defstruct f: 0.0
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a floating-point number expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %Float{f: float}
|
||||
|
||||
@doc """
|
||||
Initialise a `Float` from a floating-point value.
|
||||
"""
|
||||
@spec init(float) :: Result.t(t)
|
||||
def init(value) when is_float(value),
|
||||
do: ok(%Float{f: value})
|
||||
|
||||
def init(value),
|
||||
do: error({:expression_error, "Expected a float value, instead received #{inspect(value)}"})
|
||||
end
|
13
lib/gcode/model/expr/float/expr.ex
Normal file
13
lib/gcode/model/expr/float/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Float do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Float`, which will return evaluate to a
|
||||
float.
|
||||
"""
|
||||
|
||||
@spec evaluate(Float.t()) :: Expr.result()
|
||||
def evaluate(%Float{f: f}), do: ok(f)
|
||||
end
|
13
lib/gcode/model/expr/float/serialise.ex
Normal file
13
lib/gcode/model/expr/float/serialise.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Float do
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Float`, allowing them to be
|
||||
convered into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Float.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Float{f: value}) when is_float(value), do: ok([Elixir.Float.to_string(value)])
|
||||
def serialise(_), do: error({:serialise_error, "Invalid float"})
|
||||
end
|
17
lib/gcode/model/expr/helpers.ex
Normal file
17
lib/gcode/model/expr/helpers.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Gcode.Model.Expr.Helpers do
|
||||
alias Gcode.Model.Expr
|
||||
|
||||
@moduledoc """
|
||||
Helpers for working with expressions.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
A guard which ensures that `value` is an expression struct.
|
||||
"""
|
||||
@spec is_expression(any) :: Macro.t()
|
||||
defguard is_expression(value)
|
||||
when is_struct(value, Expr.Binary) or is_struct(value, Expr.Boolean) or
|
||||
is_struct(value, Expr.Constant) or is_struct(value, Expr.Float) or
|
||||
is_struct(value, Expr.Integer) or is_struct(value, Expr.List) or
|
||||
is_struct(value, Expr.String) or is_struct(value, Expr.Unary)
|
||||
end
|
18
lib/gcode/model/expr/integer.ex
Normal file
18
lib/gcode/model/expr/integer.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Gcode.Model.Expr.Integer do
|
||||
defstruct i: 0
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents an integer number expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %Integer{i: integer}
|
||||
|
||||
@spec init(integer) :: Result.t(t)
|
||||
def init(value) when is_integer(value),
|
||||
do: ok(%Integer{i: value})
|
||||
|
||||
def init(value),
|
||||
do: error({:expression_error, "Expected an integer value, but received #{inspect(value)}"})
|
||||
end
|
13
lib/gcode/model/expr/integer/expr.ex
Normal file
13
lib/gcode/model/expr/integer/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Integer do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Integer`, which will return evaluate to a
|
||||
integer.
|
||||
"""
|
||||
|
||||
@spec evaluate(Integer.t()) :: Expr.result()
|
||||
def evaluate(%Integer{i: i}), do: ok(i)
|
||||
end
|
15
lib/gcode/model/expr/integer/serialise.ex
Normal file
15
lib/gcode/model/expr/integer/serialise.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Integer do
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Integer`, allowing them to be
|
||||
convered into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Integer.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Integer{i: value}) when is_integer(value),
|
||||
do: ok([Elixir.Integer.to_string(value)])
|
||||
|
||||
def serialise(_), do: error({:serialise_error, "Invalid integer"})
|
||||
end
|
28
lib/gcode/model/expr/list.ex
Normal file
28
lib/gcode/model/expr/list.ex
Normal file
|
@ -0,0 +1,28 @@
|
|||
defmodule Gcode.Model.Expr.List do
|
||||
defstruct elements: []
|
||||
alias Gcode.Model.{Expr, Expr.List}
|
||||
use Gcode.Result
|
||||
import Gcode.Model.Expr.Helpers
|
||||
|
||||
@moduledoc """
|
||||
Represents an array expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %List{elements: [Expr.t()]}
|
||||
|
||||
@doc """
|
||||
Initialise a `List` from a boolean value.
|
||||
"""
|
||||
@spec init :: Result.t(t)
|
||||
def init, do: ok(%List{})
|
||||
|
||||
@doc """
|
||||
Push an expressions onto the list.
|
||||
"""
|
||||
@spec push(t, Expr.t()) :: Result.t(t)
|
||||
def push(%List{elements: elements}, expr) when is_expression(expr),
|
||||
do: ok(%List{elements: [expr | elements]})
|
||||
|
||||
def push(%List{}, expr),
|
||||
do: error({:expression_error, "Expected expression, but received #{inspect(expr)}"})
|
||||
end
|
18
lib/gcode/model/expr/list/expr.ex
Normal file
18
lib/gcode/model/expr/list/expr.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.List do
|
||||
alias Gcode.Model.{Expr, Expr.List}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `List`, which will return evaluate to a
|
||||
list.
|
||||
"""
|
||||
|
||||
@spec evaluate(List.t()) :: Expr.result()
|
||||
def evaluate(%List{elements: elements}) do
|
||||
elements =
|
||||
elements
|
||||
|> Enum.map(&Expr.evaluate/1)
|
||||
|
||||
ok(elements)
|
||||
end
|
||||
end
|
12
lib/gcode/model/expr/list/serialise.ex
Normal file
12
lib/gcode/model/expr/list/serialise.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.List do
|
||||
alias Gcode.Model.{Expr.List, Serialise}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `List`. Unfortunately it's impossible
|
||||
to serialise a list into G-code.
|
||||
"""
|
||||
|
||||
@spec serialise(List.t()) :: Serialise.result()
|
||||
def serialise(%List{}), do: error({:serialise_error, "Cannot serialise a list"})
|
||||
end
|
40
lib/gcode/model/expr/string.ex
Normal file
40
lib/gcode/model/expr/string.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
defmodule Gcode.Model.Expr.String do
|
||||
alias Gcode.Model.Expr.String
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct value: Option.none()
|
||||
|
||||
@moduledoc """
|
||||
Represents a string expression in G-code.
|
||||
"""
|
||||
|
||||
@type t :: %String{
|
||||
value: Option.t(String.t())
|
||||
}
|
||||
|
||||
@doc """
|
||||
Initialise a comment.
|
||||
|
||||
## Example
|
||||
|
||||
iex> "Doc, in the carpark, with plutonium"
|
||||
...> |> String.init()
|
||||
{:ok, %String{value: "Doc, in the carpark, with plutonium"}}
|
||||
"""
|
||||
@spec init(Elixir.String.t()) :: Result.t(t)
|
||||
def init(comment) when is_binary(comment) do
|
||||
if Elixir.String.printable?(comment) do
|
||||
ok(%String{value: comment})
|
||||
else
|
||||
error(
|
||||
{:string_error, "String should be a valid UTF-8 string, received #{inspect(comment)}"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def init(comment),
|
||||
do:
|
||||
error(
|
||||
{:string_error, "String should be a valid UTF-8 string, received #{inspect(comment)}"}
|
||||
)
|
||||
end
|
13
lib/gcode/model/expr/string/expr.ex
Normal file
13
lib/gcode/model/expr/string/expr.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.String do
|
||||
alias Gcode.Model.Expr
|
||||
alias Gcode.Model.Expr.String
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `String`, which will return evaluate to an
|
||||
Erlang binary.
|
||||
"""
|
||||
|
||||
@spec evaluate(String.t()) :: Expr.result()
|
||||
def evaluate(%String{value: value}), do: ok(value)
|
||||
end
|
12
lib/gcode/model/expr/string/serialise.ex
Normal file
12
lib/gcode/model/expr/string/serialise.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.String do
|
||||
alias Gcode.Model.Expr.String
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `String`, allowing them to be converted
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(String.t()) :: Result.t([Elixir.String.t()], {:serialise_error, any})
|
||||
def serialise(%String{value: value}), do: ok([inspect(value)])
|
||||
end
|
39
lib/gcode/model/expr/unary.ex
Normal file
39
lib/gcode/model/expr/unary.ex
Normal file
|
@ -0,0 +1,39 @@
|
|||
defmodule Gcode.Model.Expr.Unary do
|
||||
use Gcode.Option
|
||||
defstruct op: none(), expr: none()
|
||||
alias Gcode.Model.{Expr, Expr.Unary}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Represents a unary (or prefix) expression in G-code. A unary consists of a
|
||||
single operand and an operator.
|
||||
"""
|
||||
|
||||
@operators ~w[! + - #]a
|
||||
|
||||
@typedoc "Valid unary operators"
|
||||
@type operator :: :! | :+ | :- | :"#"
|
||||
|
||||
@type t :: %Unary{op: Option.t(atom), expr: Option.t(Expr.t())}
|
||||
|
||||
@doc """
|
||||
Wrap an inner expression and operator in a unary expression.
|
||||
"""
|
||||
@spec init(operator, Expr.t()) :: Result.t(t)
|
||||
def init(operator, expr) when operator in @operators and is_expression(expr),
|
||||
do: ok(%Unary{op: some(operator), expr: some(expr)})
|
||||
|
||||
def init(operator, expr) when operator in @operators,
|
||||
do: error({:expression_error, "Expected expression, but received #{inspect(expr)}"})
|
||||
|
||||
def init(operator, expr) when is_expression(expr),
|
||||
do: error({:expression_error, "Expected unary operator, but received #{inspect(operator)}"})
|
||||
|
||||
def init(operator, expr),
|
||||
do:
|
||||
error(
|
||||
{:expression_error,
|
||||
"Expected unary operator and expression, but received #{inspect(operator: operator, expression: expr)}"}
|
||||
)
|
||||
end
|
78
lib/gcode/model/expr/unary/expr.ex
Normal file
78
lib/gcode/model/expr/unary/expr.ex
Normal file
|
@ -0,0 +1,78 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Unary do
|
||||
alias Gcode.Model.{Expr, Expr.Unary}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Expr` protocol for `Unary`, which will attempt to apply the
|
||||
operator to the operand.
|
||||
"""
|
||||
|
||||
@spec evaluate(Unary.t()) :: Expr.result()
|
||||
def evaluate(%Unary{op: some(:!), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_boolean(result) ->
|
||||
ok(!result)
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaulate to boolean, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(result) ->
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(%Unary{op: some(:-), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_number(result) ->
|
||||
ok(0 - result)
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaluate to a number, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(reason) ->
|
||||
error(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(%Unary{op: some(:+), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_number(result) ->
|
||||
ok(0 + result)
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaluate to a number, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(reason) ->
|
||||
error(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate(%Unary{op: some(:"#"), expr: some(inner)}) do
|
||||
case Expr.evaluate(inner) do
|
||||
ok(result) when is_list(result) ->
|
||||
ok(length(result))
|
||||
|
||||
ok(result) when is_binary(result) ->
|
||||
ok(String.length(result))
|
||||
|
||||
ok(other) ->
|
||||
error(
|
||||
{:program_error,
|
||||
"Expected expression to evaluate to an array or string, but received #{inspect(other)}"}
|
||||
)
|
||||
|
||||
error(reason) ->
|
||||
error(reason)
|
||||
end
|
||||
end
|
||||
end
|
21
lib/gcode/model/expr/unary/serialise.ex
Normal file
21
lib/gcode/model/expr/unary/serialise.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Unary do
|
||||
alias Gcode.Model.{Expr.Unary, Serialise}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implement the `Serialise` protocol for `Unary`, allowing them to be converted
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Unary.t()) :: Serialise.result()
|
||||
def serialise(%Unary{op: some(op), expr: some(inner)}) do
|
||||
case Serialise.serialise(inner) do
|
||||
ok(inner) -> ok([to_string(op) | inner])
|
||||
error(reason) -> error(reason)
|
||||
end
|
||||
end
|
||||
|
||||
def serialise(unary),
|
||||
do: error({:serialise_error, "Invalid unary: #{inspect(unary)}"})
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.Program do
|
||||
defstruct elements: []
|
||||
alias Gcode.Model.{Block, Comment, Program, Skip, Tape}
|
||||
alias Gcode.Model.{Block, Comment, Program, Tape}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
|
@ -20,7 +20,7 @@ defmodule Gcode.Model.Program do
|
|||
elements: [element]
|
||||
}
|
||||
|
||||
@type element :: Block.t() | Comment.t() | Skip.t() | Tape.t()
|
||||
@type element :: Block.t() | Comment.t() | Tape.t()
|
||||
@type error :: {:program_error, String.t()}
|
||||
|
||||
@doc """
|
||||
|
@ -43,8 +43,7 @@ defmodule Gcode.Model.Program do
|
|||
@spec push(t, element) :: Result.t(t, error)
|
||||
def push(%Program{elements: elements} = program, element)
|
||||
when is_list(elements) and
|
||||
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Skip) or
|
||||
is_struct(element, Tape)),
|
||||
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Tape)),
|
||||
do: ok(%Program{program | elements: [element | elements]})
|
||||
|
||||
def push(%Program{elements: elements}, _element) when not is_list(elements),
|
||||
|
|
|
@ -2,14 +2,18 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Program do
|
|||
alias Gcode.Model.{Describe, Program}
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Program`.
|
||||
"""
|
||||
|
||||
@spec describe(Program.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(%Program{elements: elements}, options) do
|
||||
lines =
|
||||
elements
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(&Describe.describe(&1, options))
|
||||
|> Enum.reject(&(&1 == none()))
|
||||
|> Enum.map(fn some(line) -> "#{line}\n" end)
|
||||
|> Stream.map(&Describe.describe(&1, options))
|
||||
|> Stream.reject(&(&1 == none()))
|
||||
|> Stream.map(fn some(line) -> "#{line}\n" end)
|
||||
|> Enum.join("")
|
||||
|
||||
some(lines)
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
defimpl Enumerable, for: Gcode.Model.Program do
|
||||
alias Gcode.Model.Program
|
||||
@moduledoc false
|
||||
use Gcode.Result
|
||||
|
||||
@spec count(Program.t()) :: {:ok, non_neg_integer()} | {:error, module}
|
||||
def count(%Program{elements: elements}), do: {:ok, Enum.count(elements)}
|
||||
@moduledoc """
|
||||
Implements the `Enumerable` protocol for `Program`.
|
||||
"""
|
||||
|
||||
@spec member?(Program.t(), any) :: {:error, module} | {:ok, boolean}
|
||||
def member?(%Program{elements: elements}, element), do: Enumerable.member?(elements, element)
|
||||
@spec count(Program.t()) :: Result.t(non_neg_integer, module)
|
||||
def count(%Program{elements: elements}),
|
||||
do: {:ok, Enum.count(elements)}
|
||||
|
||||
@spec member?(Program.t(), any) :: Result.t(boolean, module)
|
||||
def member?(%Program{elements: elements}, element),
|
||||
do: Enumerable.member?(elements, element)
|
||||
|
||||
@spec reduce(Program.t(), Enumerable.acc(), Enumerable.reducer()) :: Enumerable.result()
|
||||
def reduce(%Program{elements: elements}, acc, fun), do: Enumerable.reduce(elements, acc, fun)
|
||||
def reduce(%Program{elements: elements}, acc, fun),
|
||||
do: Enumerable.reduce(elements, acc, fun)
|
||||
|
||||
@spec slice(Program.t()) ::
|
||||
{:ok, non_neg_integer, Enumerable.slicing_fun()} | {:error, module}
|
||||
{:ok, non_neg_integer, Enumerable.slicing_fun()} | Result.error(module)
|
||||
def slice(%Program{elements: elements}), do: Enumerable.slice(elements)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Program do
|
|||
alias Gcode.{Model.Program, Model.Serialise}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Program`, allowing it to be turned
|
||||
into G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Program.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Program{elements: elements}) do
|
||||
with elements <- Enum.reverse(elements),
|
||||
|
|
|
@ -3,9 +3,11 @@ defprotocol Gcode.Model.Serialise do
|
|||
alias Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
A protocol which is used to serialise the model into a string.
|
||||
A protocol which is used to serialise the model into G-code output.
|
||||
"""
|
||||
|
||||
@type result :: Result.t([String.t()], {:serialise_error, String.t()})
|
||||
|
||||
@spec serialise(Serialise.t()) :: Result.t([String.t()])
|
||||
def serialise(serialisable)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Skip do
|
|||
alias Gcode.Model.Skip
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Skip`.
|
||||
"""
|
||||
|
||||
@spec describe(Skip.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(_skip, _opts \\ []), do: none()
|
||||
end
|
||||
|
|
|
@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Skip do
|
|||
alias Gcode.{Model.Skip, Result}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Skip`, allowing it to be turned into
|
||||
G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Skip.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Skip{number: none()}), do: ok(["/"])
|
||||
|
|
|
@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Tape do
|
|||
alias Gcode.Model.Tape
|
||||
use Gcode.Option
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Describe` protocol for `Tape`.
|
||||
"""
|
||||
|
||||
@spec describe(Tape.t(), options :: []) :: Option.t(String.t())
|
||||
def describe(_tape, _opts \\ []), do: none()
|
||||
end
|
||||
|
|
|
@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Tape do
|
|||
alias Gcode.{Model.Tape, Result}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Tape`, allowing it to be turned into
|
||||
G-code output.
|
||||
"""
|
||||
|
||||
@spec serialise(Tape.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Tape{leader: none()}), do: ok(["%"])
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Gcode.Model.Word do
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Expr, Word}
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
|
@ -11,7 +12,7 @@ defmodule Gcode.Model.Word do
|
|||
|
||||
@type t :: %Word{
|
||||
word: String.t(),
|
||||
address: number
|
||||
address: Expr.t()
|
||||
}
|
||||
|
||||
@doc """
|
||||
|
@ -20,10 +21,10 @@ defmodule Gcode.Model.Word do
|
|||
## Example
|
||||
|
||||
iex> Word.init("G", 0)
|
||||
{:ok, %Word{word: "G", address: 0}}
|
||||
{:ok, %Word{word: "G", address: %Integer{i: 0}}}
|
||||
"""
|
||||
@spec init(String.t(), number) :: Result.t(t)
|
||||
def init(word, address) when is_binary(word) and is_number(address) do
|
||||
@spec init(String.t(), number | Expr.t()) :: Result.t(t)
|
||||
def init(word, address) when is_binary(word) and is_expression(address) do
|
||||
if Regex.match?(~r/^[A-Z]$/, word) do
|
||||
ok(%Word{word: word, address: address})
|
||||
else
|
||||
|
@ -31,9 +32,31 @@ defmodule Gcode.Model.Word do
|
|||
end
|
||||
end
|
||||
|
||||
def init(word, address) when is_number(address),
|
||||
def init(word, address) when is_binary(word) and is_integer(address) do
|
||||
if Regex.match?(~r/^[A-Z]$/, word) do
|
||||
ok(address) = Expr.Integer.init(address)
|
||||
ok(%Word{word: word, address: address})
|
||||
else
|
||||
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
|
||||
end
|
||||
end
|
||||
|
||||
def init(word, address) when is_binary(word) and is_float(address) do
|
||||
if Regex.match?(~r/^[A-Z]$/, word) do
|
||||
ok(address) = Expr.Float.init(address)
|
||||
ok(%Word{word: word, address: address})
|
||||
else
|
||||
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
|
||||
end
|
||||
end
|
||||
|
||||
def init(word, address) when is_expression(address),
|
||||
do: error({:word_error, "Expected word to be a string, received #{inspect(word)}"})
|
||||
|
||||
def init(_word, address),
|
||||
do: error({:word_error, "Expected address to be a number, received #{inspect(address)}"})
|
||||
do:
|
||||
error(
|
||||
{:word_error,
|
||||
"Expected address to be an expression or a number, received #{inspect(address)}"}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
|
||||
import Gcode.Model.Expr.Helpers
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Expr, Word}
|
||||
|
||||
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
|
||||
|
||||
@moduledoc """
|
||||
Describes common/conventional words.
|
||||
|
@ -49,283 +52,560 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
|
|||
do: do_describe(word, Enum.into(options, %{}))
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
|
||||
when axis in ~w[A B C] and angle >= 0,
|
||||
do: some("Rotate #{axis} axis counterclockwise to #{angle}º")
|
||||
when axis in ~w[A B C] do
|
||||
case Expr.evaluate(angle) do
|
||||
ok(angle) when is_number(angle) and angle >= 0 ->
|
||||
some("Rotate #{axis} axis counterclockwise to #{angle}º")
|
||||
|
||||
ok(angle) when is_number(angle) and angle < 0 ->
|
||||
some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
|
||||
|
||||
error(_) ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
|
||||
when axis in ~w[A B C] and angle >= 0,
|
||||
do: some("Rotate #{axis} axis counterclockwise by #{angle}º")
|
||||
when axis in ~w[A B C] do
|
||||
case Expr.evaluate(angle) do
|
||||
ok(angle) when is_number(angle) and angle >= 0 ->
|
||||
some("Rotate #{axis} axis counterclockwise by #{angle}º")
|
||||
|
||||
ok(angle) when is_number(angle) and angle < 0 ->
|
||||
some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
|
||||
|
||||
error(_) ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, _)
|
||||
when axis in ~w[A B C] and angle >= 0,
|
||||
do: some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
|
||||
when axis in ~w[A B C] and angle < 0,
|
||||
do: some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
|
||||
when axis in ~w[A B C] do
|
||||
case Expr.evaluate(angle) do
|
||||
ok(angle) when is_number(angle) and angle >= 0 ->
|
||||
some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
|
||||
|
||||
ok(angle) when is_number(angle) and angle < 0 ->
|
||||
some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
|
||||
|
||||
error(_) ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options) do
|
||||
case distance_with_unit(depth, options) do
|
||||
some(depth) ->
|
||||
some("Depth of cut #{depth}")
|
||||
|
||||
_ ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}) do
|
||||
case Expr.evaluate(aperture) do
|
||||
some(aperture) when is_number(aperture) -> some("Aperture #{aperture}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
some(offset) -> some("Left radial offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
some(offset) -> some("Right radial offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
some(offset) -> some("Radial offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
|
||||
when axis in ~w[A B C] and angle < 0,
|
||||
do: some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options) do
|
||||
case feedrate(feedrate, options) do
|
||||
some(feedrate) -> some("Extruder feedrate #{feedrate}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: angle}, _)
|
||||
when axis in ~w[A B C] and angle < 0,
|
||||
do: some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options) do
|
||||
case feedrate(feedrate, options) do
|
||||
some(feedrate) -> some("Precision feedrate #{feedrate}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options),
|
||||
do: some("Depth of cut #{distance_with_unit(depth, options)}")
|
||||
defp do_describe(%Word{word: "F", address: feedrate}, options) do
|
||||
case feedrate(feedrate, options) do
|
||||
some(feedrate) -> some("Feedrate #{feedrate}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}),
|
||||
do: some("Aperture #{aperture}")
|
||||
defp do_describe(%Word{word: "G", address: address}, options) do
|
||||
case {Expr.evaluate(address), options} do
|
||||
{ok(0), _} ->
|
||||
some("Rapid move")
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options),
|
||||
do: some("Left radial offset #{distance_with_unit(offset, options)}")
|
||||
{ok(1), _} ->
|
||||
some("Linear move")
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options),
|
||||
do: some("Right radial offset #{distance_with_unit(offset, options)}")
|
||||
{ok(2), _} ->
|
||||
some("Clockwise circular move")
|
||||
|
||||
defp do_describe(%Word{word: "D", address: offset}, options),
|
||||
do: some("Radial offset #{distance_with_unit(offset, options)}")
|
||||
{ok(3), _} ->
|
||||
some("Counterclockwise circular move")
|
||||
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options),
|
||||
do: some("Extruder feedrate #{feedrate(feedrate, options)}")
|
||||
{ok(4), _} ->
|
||||
some("Dwell")
|
||||
|
||||
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options),
|
||||
do: some("Precision feedrate #{feedrate(feedrate, options)}")
|
||||
{ok(5), _} ->
|
||||
some("High-precision contour control")
|
||||
|
||||
defp do_describe(%Word{word: "F", address: feedrate}, options),
|
||||
do: some("Feedrate #{feedrate(feedrate, options)}")
|
||||
{ok(5.1), _} ->
|
||||
some("AI advanced preview control")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 0}, _), do: some("Rapid move")
|
||||
defp do_describe(%Word{word: "G", address: 1}, _), do: some("Linear move")
|
||||
defp do_describe(%Word{word: "G", address: 2}, _), do: some("Clockwise circular move")
|
||||
defp do_describe(%Word{word: "G", address: 3}, _), do: some("Counterclockwise circular move")
|
||||
defp do_describe(%Word{word: "G", address: 4}, _), do: some("Dwell")
|
||||
defp do_describe(%Word{word: "G", address: 5}, _), do: some("High-precision contour control")
|
||||
defp do_describe(%Word{word: "G", address: 5.1}, _), do: some("AI advanced preview control")
|
||||
defp do_describe(%Word{word: "G", address: 6.1}, _), do: some("NURBS machining")
|
||||
defp do_describe(%Word{word: "G", address: 7}, _), do: some("Imaginary axis designation")
|
||||
defp do_describe(%Word{word: "G", address: 9}, _), do: some("Exact stop check - non-modal")
|
||||
defp do_describe(%Word{word: "G", address: 10}, _), do: some("Programmable data input")
|
||||
defp do_describe(%Word{word: "G", address: 11}, _), do: some("Data write cancel")
|
||||
defp do_describe(%Word{word: "G", address: 17}, _), do: some("XY plane selection")
|
||||
defp do_describe(%Word{word: "G", address: 18}, _), do: some("ZX plane selection")
|
||||
defp do_describe(%Word{word: "G", address: 19}, _), do: some("YZ plane selection")
|
||||
defp do_describe(%Word{word: "G", address: 20}, _), do: some("Unit is inches")
|
||||
defp do_describe(%Word{word: "G", address: 21}, _), do: some("Unit is mm")
|
||||
defp do_describe(%Word{word: "G", address: 28}, _), do: some("Return to home position")
|
||||
{ok(6.1), _} ->
|
||||
some("NURBS machining")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 30}, _),
|
||||
do: some("Return to secondary home position")
|
||||
{ok(7), _} ->
|
||||
some("Imaginary axis designation")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 31}, _), do: some("Feed until skip function")
|
||||
defp do_describe(%Word{word: "G", address: 32}, _), do: some("Single-point threading")
|
||||
defp do_describe(%Word{word: "G", address: 33}, _), do: some("Variable pitch threading")
|
||||
defp do_describe(%Word{word: "G", address: 40}, _), do: some("Tool radius compensation off")
|
||||
defp do_describe(%Word{word: "G", address: 41}, _), do: some("Tool radius compensation left")
|
||||
defp do_describe(%Word{word: "G", address: 42}, _), do: some("Tool radius compensation right")
|
||||
{ok(9), _} ->
|
||||
some("Exact stop check - non-modal")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 43}, _),
|
||||
do: some("Tool height offset compensation negative")
|
||||
{ok(10), _} ->
|
||||
some("Programmable data input")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 44}, _),
|
||||
do: some("Tool height offset compensation positive")
|
||||
{ok(11), _} ->
|
||||
some("Data write cancel")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 45}, _), do: some("Axis offset single increase")
|
||||
defp do_describe(%Word{word: "G", address: 46}, _), do: some("Axis offset single decrease")
|
||||
defp do_describe(%Word{word: "G", address: 47}, _), do: some("Axis offset double increase")
|
||||
defp do_describe(%Word{word: "G", address: 48}, _), do: some("Axis offset double decrease")
|
||||
{ok(17), _} ->
|
||||
some("XY plane selection")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 49}, _),
|
||||
do: some("Tool length offset compensation cancel")
|
||||
{ok(18), _} ->
|
||||
some("ZX plane selection")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 50}, %{operation: :turning}),
|
||||
do: some("Position register")
|
||||
{ok(19), _} ->
|
||||
some("YZ plane selection")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 50}, _), do: some("Scaling function cancel")
|
||||
defp do_describe(%Word{word: "G", address: 52}, _), do: some("Local coordinate system")
|
||||
defp do_describe(%Word{word: "G", address: 53}, _), do: some("Machine coordinate system")
|
||||
{ok(20), _} ->
|
||||
some("Unit is inches")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: address}, _)
|
||||
when address in [54, 55, 56, 57, 58, 59, 54.1],
|
||||
do: some("Work coordinate system")
|
||||
{ok(21), _} ->
|
||||
some("Unit is mm")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 61}, _), do: some("Exact stop check - modal")
|
||||
defp do_describe(%Word{word: "G", address: 62}, _), do: some("Automatic corner override")
|
||||
defp do_describe(%Word{word: "G", address: 64}, _), do: some("Default cutting mode")
|
||||
defp do_describe(%Word{word: "G", address: 68}, _), do: some("Rotate coordinate system")
|
||||
{ok(28), _} ->
|
||||
some("Return to home position")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 69}, _),
|
||||
do: some("Turn off coordinate system rotation")
|
||||
{ok(30), _} ->
|
||||
some("Return to secondary home position")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 70}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for finishing")
|
||||
{ok(31), _} ->
|
||||
some("Feed until skip function")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 71}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
|
||||
{ok(32), _} ->
|
||||
some("Single-point threading")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 72}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
|
||||
{ok(33), _} ->
|
||||
some("Variable pitch threading")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 73}, %{operation: :turning}),
|
||||
do: some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
|
||||
{ok(40), _} ->
|
||||
some("Tool radius compensation off")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 73}, _), do: some("Peck drilling cycle")
|
||||
{ok(41), _} ->
|
||||
some("Tool radius compensation left")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 74}, %{operation: :turning}),
|
||||
do: some("Peck drilling cycle")
|
||||
{ok(42), _} ->
|
||||
some("Tool radius compensation right")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 74}, _), do: some("Tapping cycle")
|
||||
{ok(43), _} ->
|
||||
some("Tool height offset compensation negative")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 75}, %{operation: :turning}),
|
||||
do: some("Peck grooving cycle")
|
||||
{ok(44), _} ->
|
||||
some("Tool height offset compensation positive")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 76}, %{operation: :turning}),
|
||||
do: some("Threading cycle")
|
||||
{ok(45), _} ->
|
||||
some("Axis offset single increase")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 76}, _), do: some("Fine boring cycle")
|
||||
defp do_describe(%Word{word: "G", address: 80}, _), do: some("Cancel cycle")
|
||||
defp do_describe(%Word{word: "G", address: 81}, _), do: some("Simple drilling cycle")
|
||||
defp do_describe(%Word{word: "G", address: 82}, _), do: some("Drilling cycle with dwell")
|
||||
defp do_describe(%Word{word: "G", address: 83}, _), do: some("Peck drilling cycle")
|
||||
{ok(46), _} ->
|
||||
some("Axis offset single decrease")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 84}, _),
|
||||
do: some("Tapping cycle, righthand thread, M03 spindle direction")
|
||||
{ok(47), _} ->
|
||||
some("Axis offset double increase")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 84.2}, _),
|
||||
do: some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
|
||||
{ok(48), _} ->
|
||||
some("Axis offset double decrease")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 84.3}, _),
|
||||
do: some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
|
||||
{ok(49), _} ->
|
||||
some("Tool length offset compensation cancel")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 85}, _), do: some("Boring cycle, feed in/feed out")
|
||||
{ok(50), %{operation: :turning}} ->
|
||||
some("Position register")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 86}, _),
|
||||
do: some("Boring cycle, feed in/spindle stop/rapid out")
|
||||
{ok(50), _} ->
|
||||
some("Scaling function cancel")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 87}, _), do: some("Boring cycle, backboring")
|
||||
{ok(52), _} ->
|
||||
some("Local coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 88}, _),
|
||||
do: some("Boring cycle, feed in/spindle stop/manual operation")
|
||||
{ok(53), _} ->
|
||||
some("Machine coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 89}, _),
|
||||
do: some("Boring cycle, feed in/dwell/feed out")
|
||||
{ok(address), _} when address in [54, 55, 56, 57, 58, 59, 54.1] ->
|
||||
some("Work coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 90}, _), do: some("Absolute positioning")
|
||||
defp do_describe(%Word{word: "G", address: 91}, _), do: some("Relative positioning")
|
||||
defp do_describe(%Word{word: "G", address: 92}, _), do: some("Position register")
|
||||
defp do_describe(%Word{word: "G", address: 94}, _), do: some("Feedrate per minute")
|
||||
defp do_describe(%Word{word: "G", address: 95}, _), do: some("Feedrate per revolution")
|
||||
defp do_describe(%Word{word: "G", address: 96}, _), do: some("Constant surface speed")
|
||||
defp do_describe(%Word{word: "G", address: 97}, _), do: some("Constant spindle speed")
|
||||
{ok(61), _} ->
|
||||
some("Exact stop check - modal")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 98}, %{operation: :turning}),
|
||||
do: some("Feedrate per minute")
|
||||
{ok(62), _} ->
|
||||
some("Automatic corner override")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 98}, _),
|
||||
do: some("Return to initial Z level in canned cycle")
|
||||
{ok(64), _} ->
|
||||
some("Default cutting mode")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 99}, %{operation: :turning}),
|
||||
do: some("Feedrate per revolution")
|
||||
{ok(68), _} ->
|
||||
some("Rotate coordinate system")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 99}, _),
|
||||
do: some("Return to R level in canned cycle")
|
||||
{ok(69), _} ->
|
||||
some("Turn off coordinate system rotation")
|
||||
|
||||
defp do_describe(%Word{word: "G", address: 100}, _), do: some("Tool length measurement")
|
||||
{ok(70), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for finishing")
|
||||
|
||||
defp do_describe(%Word{word: "H", address: length}, options),
|
||||
do: some("Tool length offset #{distance_with_unit(length, options)}")
|
||||
{ok(71), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
|
||||
|
||||
defp do_describe(%Word{word: "I", address: offset}, options),
|
||||
do: some("X arc center offset #{distance_with_unit(offset, options)}")
|
||||
{ok(72), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
|
||||
|
||||
defp do_describe(%Word{word: "J", address: offset}, options),
|
||||
do: some("Y arc center offset #{distance_with_unit(offset, options)}")
|
||||
{ok(73), %{operation: :turning}} ->
|
||||
some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
|
||||
|
||||
defp do_describe(%Word{word: "K", address: offset}, options),
|
||||
do: some("Z arc center offset #{distance_with_unit(offset, options)}")
|
||||
{ok(73), _} ->
|
||||
some("Peck drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "L", address: count}, _), do: some("Loop count #{count}")
|
||||
{ok(74), %{operation: :turning}} ->
|
||||
some("Peck drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 0}, _), do: some("Compulsory stop")
|
||||
defp do_describe(%Word{word: "M", address: 1}, _), do: some("Optional stop")
|
||||
defp do_describe(%Word{word: "M", address: 2}, _), do: some("End of program")
|
||||
defp do_describe(%Word{word: "M", address: 3}, _), do: some("Spindle on clockwise")
|
||||
defp do_describe(%Word{word: "M", address: 4}, _), do: some("Spindle on counterclockwise")
|
||||
defp do_describe(%Word{word: "M", address: 5}, _), do: some("Spindle stop")
|
||||
defp do_describe(%Word{word: "M", address: 6}, _), do: some("Automatic tool change")
|
||||
defp do_describe(%Word{word: "M", address: 7}, _), do: some("Coolant mist")
|
||||
defp do_describe(%Word{word: "M", address: 8}, _), do: some("Coolant flood")
|
||||
defp do_describe(%Word{word: "M", address: 9}, _), do: some("Coolant off")
|
||||
defp do_describe(%Word{word: "M", address: 10}, _), do: some("Pallet clamp on")
|
||||
defp do_describe(%Word{word: "M", address: 11}, _), do: some("Pallet clamp off")
|
||||
{ok(74), _} ->
|
||||
some("Tapping cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 13}, _),
|
||||
do: some("Spindle on clockwise and coolant flood")
|
||||
{ok(75), %{operation: :turning}} ->
|
||||
some("Peck grooving cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 19}, _), do: some("Spindle orientation")
|
||||
{ok(76), %{operation: :turning}} ->
|
||||
some("Threading cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 21}, %{operation: :turning}),
|
||||
do: some("Tailstock forward")
|
||||
{ok(76), _} ->
|
||||
some("Fine boring cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 21}, _), do: some("Mirror X axis")
|
||||
{ok(80), _} ->
|
||||
some("Cancel cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 22}, %{operation: :turning}),
|
||||
do: some("Tailstock backward")
|
||||
{ok(81), _} ->
|
||||
some("Simple drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 22}, _), do: some("Mirror Y axis")
|
||||
{ok(82), _} ->
|
||||
some("Drilling cycle with dwell")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 23}, %{operation: :turning}),
|
||||
do: some("Thread gradual pullout on")
|
||||
{ok(83), _} ->
|
||||
some("Peck drilling cycle")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 23}, _), do: some("Mirror off")
|
||||
{ok(84), _} ->
|
||||
some("Tapping cycle, righthand thread, M03 spindle direction")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 24}, %{operation: :turning}),
|
||||
do: some("Thread gradual pullout off")
|
||||
{ok(84.2), _} ->
|
||||
some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 30}, _), do: some("End of program")
|
||||
{ok(84.3), _} ->
|
||||
some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: gear}, %{operation: :turning})
|
||||
when is_integer(gear) and gear > 40 and gear < 45,
|
||||
do: some("Gear select #{gear - 40}")
|
||||
{ok(85), _} ->
|
||||
some("Boring cycle, feed in/feed out")
|
||||
|
||||
defp do_describe(%Word{word: "M", address: 48}, _), do: some("Feedrate override allowed")
|
||||
defp do_describe(%Word{word: "M", address: 49}, _), do: some("Feedrate override not allowed")
|
||||
defp do_describe(%Word{word: "M", address: 52}, _), do: some("Unload tool")
|
||||
defp do_describe(%Word{word: "M", address: 60}, _), do: some("Automatic pallet change")
|
||||
defp do_describe(%Word{word: "M", address: 98}, _), do: some("Subprogram call")
|
||||
defp do_describe(%Word{word: "M", address: 99}, _), do: some("Subprogram end")
|
||||
defp do_describe(%Word{word: "M", address: 100}, _), do: some("Clean nozzle")
|
||||
defp do_describe(%Word{word: "N", address: line}, _), do: some("Line #{line}")
|
||||
defp do_describe(%Word{word: "O", address: name}, _), do: some("Program #{name}")
|
||||
defp do_describe(%Word{word: "P", address: param}, _), do: some("Parameter #{param}")
|
||||
{ok(86), _} ->
|
||||
some("Boring cycle, feed in/spindle stop/rapid out")
|
||||
|
||||
defp do_describe(%Word{word: "Q", address: distance}, options),
|
||||
do: some("Peck increment #{distance_with_unit(distance, options)}")
|
||||
{ok(87), _} ->
|
||||
some("Boring cycle, backboring")
|
||||
|
||||
defp do_describe(%Word{word: "R", address: distance}, options),
|
||||
do: some("Radius #{distance_with_unit(distance, options)}")
|
||||
{ok(88), _} ->
|
||||
some("Boring cycle, feed in/spindle stop/manual operation")
|
||||
|
||||
defp do_describe(%Word{word: "S", address: speed}, _), do: some("Speed #{speed}")
|
||||
defp do_describe(%Word{word: "T", address: tool}, _), do: some("Tool #{tool}")
|
||||
{ok(89), _} ->
|
||||
some("Boring cycle, feed in/dwell/feed out")
|
||||
|
||||
defp do_describe(%Word{word: "X", address: position}, options),
|
||||
do: some("X #{distance_with_unit(position, options)}")
|
||||
{ok(90), _} ->
|
||||
some("Absolute positioning")
|
||||
|
||||
defp do_describe(%Word{word: "Y", address: position}, options),
|
||||
do: some("Y #{distance_with_unit(position, options)}")
|
||||
{ok(91), _} ->
|
||||
some("Relative positioning")
|
||||
|
||||
defp do_describe(%Word{word: "Z", address: position}, options),
|
||||
do: some("Z #{distance_with_unit(position, options)}")
|
||||
{ok(92), _} ->
|
||||
some("Position register")
|
||||
|
||||
{ok(94), _} ->
|
||||
some("Feedrate per minute")
|
||||
|
||||
{ok(95), _} ->
|
||||
some("Feedrate per revolution")
|
||||
|
||||
{ok(96), _} ->
|
||||
some("Constant surface speed")
|
||||
|
||||
{ok(97), _} ->
|
||||
some("Constant spindle speed")
|
||||
|
||||
{ok(98), %{operation: :turning}} ->
|
||||
some("Feedrate per minute")
|
||||
|
||||
{ok(98), _} ->
|
||||
some("Return to initial Z level in canned cycle")
|
||||
|
||||
{ok(99), %{operation: :turning}} ->
|
||||
some("Feedrate per revolution")
|
||||
|
||||
{ok(99), _} ->
|
||||
some("Return to R level in canned cycle")
|
||||
|
||||
{ok(100), _} ->
|
||||
some("Tool length measurement")
|
||||
|
||||
_ ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "H", address: length}, options) do
|
||||
case distance_with_unit(length, options) do
|
||||
ok(length) -> some("Tool length offset #{length}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "I", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
ok(offset) -> some("X arc center offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "J", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
ok(offset) -> some("Y arc center offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "K", address: offset}, options) do
|
||||
case distance_with_unit(offset, options) do
|
||||
ok(offset) -> some("Z arc center offset #{offset}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "L", address: count}, _) do
|
||||
case Expr.evaluate(count) do
|
||||
ok(count) -> some("Loop count #{count}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "M", address: address}, options) do
|
||||
case {Expr.evaluate(address), options} do
|
||||
{ok(0), _} ->
|
||||
some("Compulsory stop")
|
||||
|
||||
{ok(1), _} ->
|
||||
some("Optional stop")
|
||||
|
||||
{ok(2), _} ->
|
||||
some("End of program")
|
||||
|
||||
{ok(3), _} ->
|
||||
some("Spindle on clockwise")
|
||||
|
||||
{ok(4), _} ->
|
||||
some("Spindle on counterclockwise")
|
||||
|
||||
{ok(5), _} ->
|
||||
some("Spindle stop")
|
||||
|
||||
{ok(6), _} ->
|
||||
some("Automatic tool change")
|
||||
|
||||
{ok(7), _} ->
|
||||
some("Coolant mist")
|
||||
|
||||
{ok(8), _} ->
|
||||
some("Coolant flood")
|
||||
|
||||
{ok(9), _} ->
|
||||
some("Coolant off")
|
||||
|
||||
{ok(10), _} ->
|
||||
some("Pallet clamp on")
|
||||
|
||||
{ok(11), _} ->
|
||||
some("Pallet clamp off")
|
||||
|
||||
{ok(13), _} ->
|
||||
some("Spindle on clockwise and coolant flood")
|
||||
|
||||
{ok(19), _} ->
|
||||
some("Spindle orientation")
|
||||
|
||||
{ok(21), %{operation: :turning}} ->
|
||||
some("Tailstock forward")
|
||||
|
||||
{ok(21), _} ->
|
||||
some("Mirror X axis")
|
||||
|
||||
{ok(22), %{operation: :turning}} ->
|
||||
some("Tailstock backward")
|
||||
|
||||
{ok(22), _} ->
|
||||
some("Mirror Y axis")
|
||||
|
||||
{ok(23), %{operation: :turning}} ->
|
||||
some("Thread gradual pullout on")
|
||||
|
||||
{ok(23), _} ->
|
||||
some("Mirror off")
|
||||
|
||||
{ok(24), %{operation: :turning}} ->
|
||||
some("Thread gradual pullout off")
|
||||
|
||||
{ok(30), _} ->
|
||||
some("End of program")
|
||||
|
||||
{ok(gear), %{operation: :turning}} when gear > 40 and gear < 45 ->
|
||||
some("Gear select #{gear - 40}")
|
||||
|
||||
{ok(48), _} ->
|
||||
some("Feedrate override allowed")
|
||||
|
||||
{ok(49), _} ->
|
||||
some("Feedrate override not allowed")
|
||||
|
||||
{ok(52), _} ->
|
||||
some("Unload tool")
|
||||
|
||||
{ok(60), _} ->
|
||||
some("Automatic pallet change")
|
||||
|
||||
{ok(98), _} ->
|
||||
some("Subprogram call")
|
||||
|
||||
{ok(99), _} ->
|
||||
some("Subprogram end")
|
||||
|
||||
{ok(100), _} ->
|
||||
some("Clean nozzle")
|
||||
|
||||
_ ->
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "N", address: line}, _) do
|
||||
case Expr.evaluate(line) do
|
||||
ok(line) -> some("Line #{line}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "O", address: name}, _) do
|
||||
case Expr.evaluate(name) do
|
||||
ok(name) -> some("Program #{name}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "P", address: param}, _) do
|
||||
case Expr.evaluate(param) do
|
||||
ok(param) -> some("Parameter #{param}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "Q", address: distance}, options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
ok(distance) -> some("Peck increment #{distance}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "R", address: distance}, options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
ok(distance) -> some("Radius #{distance}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "S", address: speed}, _) do
|
||||
case Expr.evaluate(speed) do
|
||||
ok(speed) -> some("Speed #{speed}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: "T", address: tool}, _) do
|
||||
case Expr.evaluate(tool) do
|
||||
ok(tool) -> some("Tool #{tool}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{word: axis, address: distance}, options) when axis in ~w[X Y Z] do
|
||||
case distance_with_unit(distance, options) do
|
||||
ok(distance) -> some("#{axis} #{distance}")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp do_describe(%Word{}, _options), do: none()
|
||||
|
||||
defp distance_with_unit(distance, %{units: :mm}), do: "#{distance}mm"
|
||||
defp distance_with_unit(distance, %{units: :inches}), do: "#{distance}\""
|
||||
defp distance_with_unit(distance, _), do: distance
|
||||
defp distance_with_unit(distance, %{units: :mm}) when is_number(distance),
|
||||
do: some("#{distance}mm")
|
||||
|
||||
defp feedrate(distance, %{operation: :turning} = options),
|
||||
do: "#{distance_with_unit(distance, options)}/rev"
|
||||
defp distance_with_unit(distance, %{units: :inches}) when is_number(distance),
|
||||
do: some("#{distance}\"")
|
||||
|
||||
defp feedrate(distance, options), do: "#{distance_with_unit(distance, options)}/min"
|
||||
defp distance_with_unit(distance, _) when is_number(distance),
|
||||
do: some(to_string(distance))
|
||||
|
||||
defp distance_with_unit(distance, options) when is_expression(distance) do
|
||||
case Expr.evaluate(distance) do
|
||||
ok(distance) when is_number(distance) -> distance_with_unit(distance, options)
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp distance_with_unit(_, _), do: none()
|
||||
|
||||
defp feedrate(distance, %{operation: :turning} = options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
some(distance) -> some("#{distance}/rev")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
|
||||
defp feedrate(distance, options) do
|
||||
case distance_with_unit(distance, options) do
|
||||
some(distance) -> some("#{distance}/min")
|
||||
_ -> none()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
defimpl Inspect, for: Gcode.Model.Word do
|
||||
alias Gcode.Model.{Describe, Word}
|
||||
alias Gcode.Model.{Describe, Expr, Word}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
import Inspect.Algebra
|
||||
@moduledoc false
|
||||
|
||||
def inspect(%Word{word: letter, address: address} = word, opts) do
|
||||
address =
|
||||
case Expr.evaluate(address) do
|
||||
ok(address) -> address
|
||||
error(reason) -> "Address error: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
case Describe.describe(word) do
|
||||
some(description) ->
|
||||
concat([
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Word, Serialise}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Word{word: word, address: address}) when is_integer(address) do
|
||||
ok(["#{word}#{address}"])
|
||||
end
|
||||
@moduledoc """
|
||||
Implements the `Serialise` protocol for `Word`, allowing it to be turned into
|
||||
G-code output.
|
||||
"""
|
||||
|
||||
def serialise(%Word{word: word, address: address}) when is_float(address) do
|
||||
ok(["#{word}#{address}"])
|
||||
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||
def serialise(%Word{word: word, address: address})
|
||||
when is_binary(word) and byte_size(word) == 1 do
|
||||
with ok(address) <- Serialise.serialise(address),
|
||||
do: ok(["#{word}#{address}"])
|
||||
end
|
||||
|
||||
def serialise(_word), do: error({:serialise_error, "invalid word"})
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Gcode.Option do
|
|||
A helper which represents an optional type.
|
||||
"""
|
||||
|
||||
@spec __using__(any) :: Macro.t()
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
alias Gcode.Option
|
||||
|
@ -16,14 +17,17 @@ defmodule Gcode.Option do
|
|||
@type some(t) :: {:ok, t}
|
||||
@type opt_none :: :error
|
||||
|
||||
@doc "Is the value a none?"
|
||||
@spec none?(t(any)) :: boolean
|
||||
def none?(:error), do: true
|
||||
def none?({:ok, _}), do: false
|
||||
|
||||
@doc "Is the value a some?"
|
||||
@spec some?(t(any)) :: boolean
|
||||
def some?(:error), do: false
|
||||
def some?({:ok, _}), do: true
|
||||
|
||||
@doc "Create or match a none"
|
||||
@spec none :: Macro.t()
|
||||
defmacro none do
|
||||
quote do
|
||||
|
@ -31,6 +35,7 @@ defmodule Gcode.Option do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Create or match a some"
|
||||
@spec some(any) :: Macro.t()
|
||||
defmacro some(pattern) do
|
||||
quote do
|
||||
|
@ -38,6 +43,7 @@ defmodule Gcode.Option do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Attempt to unwrap an option. Raises an error if the option is a none"
|
||||
@spec unwrap!(t) :: any | no_return
|
||||
def unwrap!({:ok, result}), do: result
|
||||
def unwrap!(:error), do: raise("Attempt to unwrap a none")
|
||||
|
|
215
lib/gcode/parser.ex
Normal file
215
lib/gcode/parser.ex
Normal file
|
@ -0,0 +1,215 @@
|
|||
defmodule Gcode.Parser do
|
||||
use Gcode.Result
|
||||
alias Gcode.Model.{Block, Comment, Expr, Program, Word}
|
||||
alias Gcode.Parser.{Engine, Error}
|
||||
|
||||
@moduledoc """
|
||||
A parser for G-code programs.
|
||||
|
||||
This parser converts G-code input (in UTF-8 encoding) into representations
|
||||
with the contents of `Gcode.Model`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Attempt to parse a G-code program from a string.
|
||||
"""
|
||||
@spec parse_string(String.t()) :: Result.t(Program.t(), {:parse_error, String.t()})
|
||||
def parse_string(input) do
|
||||
with ok(tokens) <- Engine.parse(input),
|
||||
ok(program) <- hydrate(tokens) do
|
||||
ok(program)
|
||||
else
|
||||
error(reason) ->
|
||||
error({:parse_error, reason})
|
||||
|
||||
error({message, unexpected, _, {line, _}, col}) ->
|
||||
error(
|
||||
{:parse_error,
|
||||
"Unexpected #{inspect(unexpected)} at line: #{line}:#{col + 1}. #{message}."}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attempt to parse the G-code program at the given path.
|
||||
"""
|
||||
@spec parse_file(Path.t()) :: Result.t(Program.t(), {:parse_error, String.t()})
|
||||
def parse_file(path) do
|
||||
with ok(input) <- File.read(path),
|
||||
ok(tokens) <- Engine.parse(input),
|
||||
ok(program) <- hydrate(tokens) do
|
||||
ok(program)
|
||||
else
|
||||
error(reason) ->
|
||||
error({:parse_error, reason})
|
||||
|
||||
error({message, unexpected, _, {line, _}, col}) ->
|
||||
error(
|
||||
{:parse_error,
|
||||
"Unexpected #{inspect(unexpected)} at line: #{line}:#{col + 1}. #{message}."}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parse and stream the G-code program from a string.
|
||||
|
||||
Note that this function doesn't yield `Program` objects, but blocks, comments,
|
||||
etc.
|
||||
"""
|
||||
@spec stream_string!(String.t()) :: Enumerable.t() | no_return
|
||||
def stream_string!(input) do
|
||||
input
|
||||
|> String.split(~r/\r?\n/)
|
||||
|> Stream.with_index()
|
||||
|> ParallelStream.map(&trim_line/1)
|
||||
|> ParallelStream.reject(&(elem(&1, 0) == ""))
|
||||
|> ParallelStream.map(&parse_line!/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parse and stream the G-code program at the given location.
|
||||
|
||||
Note that this function doesn't yield `Program` objects, but blocks, comments,
|
||||
etc.
|
||||
"""
|
||||
@spec stream_file!(Path.t()) :: Enumerable.t() | no_return
|
||||
def stream_file!(path) do
|
||||
path
|
||||
|> File.stream!()
|
||||
|> Stream.with_index()
|
||||
|> ParallelStream.map(&trim_line/1)
|
||||
|> ParallelStream.reject(&(elem(&1, 0) == ""))
|
||||
|> ParallelStream.map(&parse_line!/1)
|
||||
end
|
||||
|
||||
defp trim_line({input, line_no}), do: {String.trim(input), line_no}
|
||||
|
||||
defp parse_line!({input, line_no}) do
|
||||
with ok(tokens) <- Engine.parse(input),
|
||||
ok(%Program{elements: [element]}) <- hydrate(tokens) do
|
||||
element
|
||||
else
|
||||
ok(%Program{elements: elements}) ->
|
||||
raise Error, "Expected line to result in 1 element, but contained #{length(elements)}"
|
||||
|
||||
ok(%Block{}) ->
|
||||
raise Error, "Expected parser to return a program, but returned a block instead."
|
||||
|
||||
error({:block_error, reason}) ->
|
||||
raise Error, "Block error #{reason}"
|
||||
|
||||
error({:comment_error, reason}) ->
|
||||
raise Error, "Comment error #{reason}"
|
||||
|
||||
error({:expression_error, reason}) ->
|
||||
raise Error, "Expression error #{reason}"
|
||||
|
||||
error({:parse_error, reason}) ->
|
||||
raise Error, "Parse error: #{reason}"
|
||||
|
||||
error({:program_error, reason}) ->
|
||||
raise Error, "Program error: #{reason}"
|
||||
|
||||
error({:string_error, reason}) ->
|
||||
raise Error, "String error: #{reason}"
|
||||
|
||||
error({:word_error, reason}) ->
|
||||
raise Error, "Word error: #{reason}"
|
||||
|
||||
error({message, unexpected, _, _, col}) ->
|
||||
raise Error,
|
||||
"Unexpected #{inspect(unexpected)} at line: #{line_no + 1}:#{col + 1}. #{message}."
|
||||
end
|
||||
end
|
||||
|
||||
defp hydrate(tokens) do
|
||||
with ok(program) <- Program.init(),
|
||||
do: hydrate(tokens, program)
|
||||
end
|
||||
|
||||
defp hydrate([], result), do: ok(result)
|
||||
|
||||
defp hydrate([{:comment, comment} | remaining], %Program{} = program) do
|
||||
with ok(comment) <- Comment.init(List.to_string(comment)),
|
||||
ok(program) <- Program.push(program, comment),
|
||||
do: hydrate(remaining, program)
|
||||
end
|
||||
|
||||
defp hydrate([{:comment, comment} | remaining], %Block{} = block) do
|
||||
with ok(comment) <- Comment.init(List.to_string(comment)),
|
||||
ok(block) <- Block.comment(block, comment),
|
||||
do: hydrate(remaining, block)
|
||||
end
|
||||
|
||||
defp hydrate([{:block, words} | remaining], %Program{} = program) do
|
||||
with ok(block) <- Block.init(),
|
||||
ok(block) <- hydrate(words, block),
|
||||
ok(program) <- Program.push(program, block),
|
||||
do: hydrate(remaining, program)
|
||||
end
|
||||
|
||||
defp hydrate([{:word, contents} | remaining], %Block{} = block) do
|
||||
with ok(command) <- Keyword.fetch(contents, :command),
|
||||
ok(address) <- Keyword.fetch(contents, :address),
|
||||
ok(address) <- expression(address),
|
||||
ok(word) <- Word.init(List.to_string(command), address),
|
||||
ok(block) <- Block.push(block, word),
|
||||
do: hydrate(remaining, block)
|
||||
end
|
||||
|
||||
defp hydrate([{:string, _} = str | remaining], %Block{} = block) do
|
||||
with ok(str) <- expression([str]),
|
||||
ok(block) <- Block.push(block, str),
|
||||
do: hydrate(remaining, block)
|
||||
end
|
||||
|
||||
defp hydrate([{:newline, _} | remaining], %Program{} = program), do: hydrate(remaining, program)
|
||||
defp hydrate([{:newline, _}], %Block{} = block), do: ok(block)
|
||||
|
||||
defp expression([{:-, inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:-, inner)
|
||||
end
|
||||
|
||||
defp expression([{:+, inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:+, inner)
|
||||
end
|
||||
|
||||
defp expression([{:!, inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:!, inner)
|
||||
end
|
||||
|
||||
defp expression([{:"#", inner}]) do
|
||||
with ok(inner) <- expression(inner),
|
||||
do: Expr.Unary.init(:"#", inner)
|
||||
end
|
||||
|
||||
defp expression(integer: value) do
|
||||
value =
|
||||
value
|
||||
|> List.to_string()
|
||||
|> String.to_integer()
|
||||
|
||||
Expr.Integer.init(value)
|
||||
end
|
||||
|
||||
defp expression(float: value) do
|
||||
value =
|
||||
value
|
||||
|> List.to_string()
|
||||
|> String.to_float()
|
||||
|
||||
Expr.Float.init(value)
|
||||
end
|
||||
|
||||
defp expression(string: value) do
|
||||
value =
|
||||
value
|
||||
|> List.to_string()
|
||||
|
||||
Expr.String.init(value)
|
||||
end
|
||||
end
|
238
lib/gcode/parser/engine.ex
Normal file
238
lib/gcode/parser/engine.ex
Normal file
|
@ -0,0 +1,238 @@
|
|||
defmodule Gcode.Parser.Engine do
|
||||
import NimbleParsec
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
A parser for G-code programs using Parsec.
|
||||
"""
|
||||
|
||||
defcombinatorp(
|
||||
:whitespace,
|
||||
[
|
||||
string(" "),
|
||||
string("\t")
|
||||
]
|
||||
|> choice()
|
||||
|> repeat()
|
||||
|> ignore()
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:whitespace?,
|
||||
optional(parsec(:whitespace))
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:newline,
|
||||
[string("\r"), string("\n")] |> choice() |> times(min: 1) |> tag(:newline)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:eol,
|
||||
empty()
|
||||
|> parsec(:whitespace?)
|
||||
|> choice([parsec(:newline), eos()])
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:tape,
|
||||
empty()
|
||||
|> ignore(string("%"))
|
||||
|> optional(
|
||||
parsec(:whitespace?)
|
||||
|> repeat(utf8_char([]))
|
||||
)
|
||||
|> tag(:tape)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:braces_comment,
|
||||
empty()
|
||||
|> ignore(
|
||||
string("(")
|
||||
|> parsec(:whitespace?)
|
||||
)
|
||||
|> repeat(
|
||||
lookahead_not(
|
||||
choice([
|
||||
string(")"),
|
||||
parsec(:whitespace)
|
||||
|> string(")")
|
||||
])
|
||||
)
|
||||
|> utf8_char([])
|
||||
)
|
||||
|> tag(:comment)
|
||||
|> ignore(
|
||||
parsec(:whitespace?)
|
||||
|> string(")")
|
||||
)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:semi_comment,
|
||||
empty()
|
||||
|> ignore(
|
||||
string(";")
|
||||
|> parsec(:whitespace?)
|
||||
)
|
||||
|> repeat(
|
||||
lookahead_not(parsec(:eol))
|
||||
|> utf8_char([])
|
||||
)
|
||||
|> tag(:comment)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:comment,
|
||||
choice([
|
||||
parsec(:braces_comment),
|
||||
parsec(:semi_comment)
|
||||
])
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:integer,
|
||||
times(
|
||||
utf8_char([?0..?9]),
|
||||
min: 1
|
||||
)
|
||||
|> tag(:integer)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:float,
|
||||
times(utf8_char([?0..?9]), min: 1)
|
||||
|> utf8_char([?.])
|
||||
|> times(utf8_char([?0..?9]), min: 1)
|
||||
|> tag(:float)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:number,
|
||||
choice([
|
||||
parsec(:float),
|
||||
parsec(:integer)
|
||||
])
|
||||
)
|
||||
|
||||
defcombinator(
|
||||
:constant,
|
||||
choice([
|
||||
choice([
|
||||
string("true"),
|
||||
string("false")
|
||||
])
|
||||
|> tag(:boolean),
|
||||
string("iterations")
|
||||
|> tag(:iterations),
|
||||
string("line")
|
||||
|> tag(:line),
|
||||
string("pi")
|
||||
|> tag(:pi),
|
||||
string("result")
|
||||
|> tag(:result)
|
||||
])
|
||||
|> tag(:constant)
|
||||
)
|
||||
|
||||
defcombinator(
|
||||
:prefix,
|
||||
choice([
|
||||
ignore(string("!") |> parsec(:whitespace?)) |> tag(parsec(:expression), :!),
|
||||
ignore(string("+") |> parsec(:whitespace?)) |> tag(parsec(:expression), :+),
|
||||
ignore(string("-") |> parsec(:whitespace?)) |> tag(parsec(:expression), :-),
|
||||
ignore(string("#") |> parsec(:whitespace?)) |> tag(parsec(:expression), :"#")
|
||||
])
|
||||
)
|
||||
|
||||
defcombinator(
|
||||
:expression,
|
||||
choice([
|
||||
parsec(:prefix),
|
||||
parsec(:number),
|
||||
parsec(:constant)
|
||||
])
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:bare_string,
|
||||
times(
|
||||
lookahead_not(
|
||||
parsec(:whitespace?)
|
||||
|> choice([
|
||||
parsec(:newline),
|
||||
parsec(:comment)
|
||||
])
|
||||
)
|
||||
|> utf8_char([]),
|
||||
min: 1
|
||||
)
|
||||
|> tag(:string)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:quoted_string,
|
||||
ignore(string("\""))
|
||||
|> repeat(
|
||||
lookahead_not(
|
||||
choice([
|
||||
parsec(:newline),
|
||||
string("\"")
|
||||
])
|
||||
)
|
||||
|> utf8_char([])
|
||||
)
|
||||
|> tag(:string)
|
||||
|> ignore(optional(string("\"")))
|
||||
)
|
||||
|
||||
defcombinatorp(:string, choice([parsec(:quoted_string), parsec(:bare_string)]))
|
||||
|
||||
defcombinatorp(
|
||||
:word,
|
||||
empty()
|
||||
|> utf8_char([?A..?Z])
|
||||
|> tag(:command)
|
||||
|> parsec(:whitespace?)
|
||||
|> tag(
|
||||
parsec(:expression),
|
||||
:address
|
||||
)
|
||||
|> tag(:word)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:block,
|
||||
empty()
|
||||
|> times(
|
||||
choice([
|
||||
parsec(:word),
|
||||
parsec(:string)
|
||||
])
|
||||
|> parsec(:whitespace?),
|
||||
min: 1
|
||||
)
|
||||
|> optional(parsec(:comment))
|
||||
|> tag(:block)
|
||||
)
|
||||
|
||||
defcombinatorp(
|
||||
:line,
|
||||
choice([
|
||||
parsec(:tape),
|
||||
parsec(:comment),
|
||||
parsec(:block)
|
||||
])
|
||||
)
|
||||
|
||||
defparsecp(:program, times(parsec(:line) |> parsec(:eol), min: 1))
|
||||
|
||||
@spec parse(String.t()) :: Result.t(keyword)
|
||||
def parse(program) do
|
||||
case program(program) do
|
||||
{:ok, tokens, _, _, _, _} -> ok(tokens)
|
||||
{:error, a, b, c, d, e} -> error({a, b, c, d, e})
|
||||
end
|
||||
end
|
||||
end
|
8
lib/gcode/parser/error.ex
Normal file
8
lib/gcode/parser/error.ex
Normal file
|
@ -0,0 +1,8 @@
|
|||
defmodule Gcode.Parser.Error do
|
||||
defexception message: nil
|
||||
|
||||
@moduledoc """
|
||||
Parser's streaming outputs have no way to return a result type, so we are
|
||||
forced to rely on exceptions. These are those exceptions.
|
||||
"""
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
defmodule Gcode.Result do
|
||||
@moduledoc """
|
||||
A helper which represents a result type.
|
||||
|
||||
This is really just a wrapper around Erlang's ok/error tuples.
|
||||
"""
|
||||
|
||||
@type t :: t(any, any)
|
||||
|
@ -9,6 +11,7 @@ defmodule Gcode.Result do
|
|||
@type ok(result) :: {:ok, result}
|
||||
@type error(error) :: {:error, error}
|
||||
|
||||
@spec __using__(any) :: Macro.t()
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
alias Gcode.Result
|
||||
|
@ -17,6 +20,7 @@ defmodule Gcode.Result do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Initialise or match an ok value"
|
||||
@spec ok(any) :: Macro.t()
|
||||
defmacro ok(result) do
|
||||
quote do
|
||||
|
@ -24,6 +28,7 @@ defmodule Gcode.Result do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Initialise or match an error value"
|
||||
@spec error(any) :: Macro.t()
|
||||
defmacro error(error) do
|
||||
quote do
|
||||
|
@ -31,18 +36,22 @@ defmodule Gcode.Result do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Is the result ok?"
|
||||
@spec ok?(t) :: boolean
|
||||
def ok?({:ok, _}), do: true
|
||||
def ok?({:error, _}), do: false
|
||||
|
||||
@doc "Is the result an error?"
|
||||
@spec error?(t) :: boolean
|
||||
def error?({:ok, _}), do: false
|
||||
def error?({:error, _}), do: true
|
||||
|
||||
@doc "Attempt to unwrap a result and return the inner value. Raises an exception if the result contains an error."
|
||||
@spec unwrap!(t) :: any | no_return
|
||||
def unwrap!({:ok, result}), do: result
|
||||
def unwrap!({:error, error}), do: raise(error)
|
||||
|
||||
@doc "Convert a successful result another result."
|
||||
@spec map(t, (any -> t)) :: t
|
||||
def map({:ok, value}, mapper) when is_function(mapper, 1) do
|
||||
case mapper.(value) do
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
defmodule Gcode.Result.Enum do
|
||||
@moduledoc false
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
Common enumerableish functions on results.
|
||||
"""
|
||||
|
||||
@type result :: Result.t()
|
||||
@type result(result) :: Result.t(result)
|
||||
@type result(result, error) :: Result.t(result, error)
|
||||
|
@ -13,32 +16,44 @@ defmodule Gcode.Result.Enum do
|
|||
reducer :: (any, any -> result(any))
|
||||
) :: result(any)
|
||||
def reduce_while_ok(elements, acc, reducer) when is_function(reducer, 2) do
|
||||
Enum.reduce_while(elements, {:ok, acc}, fn element, {:ok, acc} ->
|
||||
Enum.reduce_while(elements, ok(acc), fn element, ok(acc) ->
|
||||
case reducer.(element, acc) do
|
||||
{:ok, acc} -> {:cont, {:ok, acc}}
|
||||
{:error, reason} -> {:halt, {:error, reason}}
|
||||
ok(acc) -> {:cont, ok(acc)}
|
||||
error(reason) -> {:halt, error(reason)}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps a collection of results using a mapping function.
|
||||
|
||||
Both the input to the map must be an ok result and the result of each mapping
|
||||
function.
|
||||
"""
|
||||
@spec map(result([any]), mapper :: (any -> result(any))) :: result([any])
|
||||
def map({:ok, enumerable}, mapper) when is_function(mapper, 1) do
|
||||
def map(ok(enumerable), mapper) when is_function(mapper, 1) do
|
||||
reduce_while_ok(enumerable, [], fn element, acc ->
|
||||
case mapper.(element) do
|
||||
{:ok, mapped} -> {:ok, [mapped | acc]}
|
||||
{:error, reason} -> {:error, reason}
|
||||
ok(mapped) -> ok([mapped | acc])
|
||||
error(reason) -> error(reason)
|
||||
end
|
||||
end)
|
||||
|> reverse()
|
||||
end
|
||||
|
||||
def map({:error, reason}, _mapper), do: {:error, reason}
|
||||
def map(error(reason), _mapper), do: error(reason)
|
||||
|
||||
@doc """
|
||||
Reverse the enumerable contents of an ok result.
|
||||
"""
|
||||
@spec reverse(result([any])) :: result([any])
|
||||
def reverse({:ok, enumerable}), do: {:ok, Enum.reverse(enumerable)}
|
||||
def reverse({:error, reason}), do: {:error, reason}
|
||||
def reverse(ok(enumerable)), do: ok(Enum.reverse(enumerable))
|
||||
def reverse(error(reason)), do: error(reason)
|
||||
|
||||
@doc """
|
||||
Join the string contents of an ok result.
|
||||
"""
|
||||
@spec join(result([String.t()]), String.t()) :: result(String.t())
|
||||
def join({:ok, strings}, joiner) when is_binary(joiner), do: {:ok, Enum.join(strings, joiner)}
|
||||
def join({:error, reason}, _joiner), do: {:error, reason}
|
||||
def join(ok(strings), joiner) when is_binary(joiner), do: ok(Enum.join(strings, joiner))
|
||||
def join(error(reason), _joiner), do: error(reason)
|
||||
end
|
||||
|
|
28
mix.exs
28
mix.exs
|
@ -2,7 +2,7 @@ defmodule Gcode.MixProject do
|
|||
use Mix.Project
|
||||
@moduledoc false
|
||||
|
||||
@version "0.3.0"
|
||||
@version "1.0.0"
|
||||
@description """
|
||||
A G-code parser and generator.
|
||||
"""
|
||||
|
@ -17,7 +17,11 @@ defmodule Gcode.MixProject do
|
|||
description: @description,
|
||||
deps: deps(),
|
||||
consolidate_protocols: Mix.env() != :test,
|
||||
elixirc_paths: elixirc_paths(Mix.env())
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
docs: [
|
||||
main: "readme",
|
||||
extras: ["README.md", "CHANGELOG.md"]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -31,9 +35,12 @@ defmodule Gcode.MixProject do
|
|||
def package do
|
||||
[
|
||||
maintainers: ["James Harton <james@harton.nz>"],
|
||||
licenses: ["Hippocratic"],
|
||||
licenses: ["HL3-FULL"],
|
||||
links: %{
|
||||
"Source" => "https://gitlab.com/jimsy/gcode"
|
||||
"Source" => "https://harton.dev/james/gcode",
|
||||
"GitHub" => "https://github.com/jimsynz/gcode",
|
||||
"Changelog" => "https://docs.harton.nz/james/gcode/changelog.html",
|
||||
"Sponsor" => "https://github.com/sponsors/jimsynz"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
@ -41,10 +48,15 @@ defmodule Gcode.MixProject do
|
|||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:ex_doc, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
|
||||
{:earmark, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
|
||||
{:credo, "~> 1.1", only: ~w[dev test]a, runtime: false},
|
||||
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false}
|
||||
{:nimble_parsec, "~> 1.2"},
|
||||
{:parallel_stream, "~> 1.1"},
|
||||
|
||||
# Dev/test
|
||||
{:credo, "~> 1.6", only: ~w[dev test]a, runtime: false},
|
||||
{:ex_check, "~> 0.15", only: ~w[dev test]a, runtime: false},
|
||||
{:ex_doc, "~> 0.30", only: ~w[dev test]a, runtime: false},
|
||||
{:earmark, "~> 1.4", only: ~w[dev test]a, runtime: false},
|
||||
{:git_ops, "~> 2.4", only: ~w[dev test]a, runtime: false}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
19
mix.lock
19
mix.lock
|
@ -1,14 +1,17 @@
|
|||
%{
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"credo": {:hex, :credo, "1.5.4", "9914180105b438e378e94a844ec3a5088ae5875626fc945b7c1462b41afc3198", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cf51af45eadc0a3f39ba13b56fdac415c91b34f7b7533a13dc13550277141bc4"},
|
||||
"earmark": {:hex, :earmark, "1.4.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"},
|
||||
"credo": {:hex, :credo, "1.6.1", "7dc76dcdb764a4316c1596804c48eada9fff44bd4b733a91ccbf0c0f368be61e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "698607fb5993720c7e93d2d8e76f2175bba024de964e160e2f7151ef3ab82ac5"},
|
||||
"earmark": {:hex, :earmark, "1.4.19", "3854a17305c880cc46305af15fb1630568d23a709aba21aaa996ced082fc29d7", [:mix], [{:earmark_parser, ">= 1.4.18", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d5a8c9f9e37159a8fdd3ea8437fb4e229eaf56d5129b9a011dc4780a4872079d"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
|
||||
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.30.0", "ed94bf5183f559d2f825e4f866cc0eab277bbb17da76aff40f8e0f149656943e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "6743fe46704fe27e2f2558faa61f00e5356528768807badb2092d38476d6dac2"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
||||
"git_ops": {:hex, :git_ops, "2.3.0", "a77f91b810d874e1abf5f415f335959a2dfc3613cbcd28c7c05b97c666339fda", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "849bd53b7992963b3fdfebcdd0c946f4aab3f6ffbcfa5668b3e83cd5aeca0a2f"},
|
||||
"git_ops": {:hex, :git_ops, "2.4.5", "185a724dfde3745edd22f7571d59c47a835cf54ded67e9ccbc951920b7eec4c2", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e323a5b01ad53bc8c19c3a444be3e61ed7803ecd2e95530446ae9327d0143ecc"},
|
||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
|
||||
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
|
||||
}
|
||||
|
|
6723
priv/fixtures/cura_marlin.gcode
Normal file
6723
priv/fixtures/cura_marlin.gcode
Normal file
File diff suppressed because it is too large
Load diff
501
priv/fixtures/fusion_360_milling_grbl.nc
Normal file
501
priv/fixtures/fusion_360_milling_grbl.nc
Normal file
|
@ -0,0 +1,501 @@
|
|||
(1001)
|
||||
(Machine)
|
||||
( vendor: Gennmitsu)
|
||||
( model: 3018 Pro)
|
||||
( description: Gennmitsu 3018 Pro)
|
||||
(T1 D=3.175 CR=0 - ZMIN=-6 - flat end mill)
|
||||
G90 G94
|
||||
G17
|
||||
G21
|
||||
(When using Fusion 360 for Personal Use, the feedrate of )
|
||||
(rapid moves is reduced to match the feedrate of cutting )
|
||||
(moves, which can increase machining time. Unrestricted )
|
||||
(rapid moves are available with a Fusion 360 Subscription. )
|
||||
G28 G91 Z0
|
||||
G90
|
||||
|
||||
(2D Contour1)
|
||||
T1
|
||||
S1000 M3
|
||||
G54
|
||||
G0 X160.042 Y80.893
|
||||
Z5
|
||||
G1 Z2 F120
|
||||
Z1.317 F10
|
||||
G19 G3 Y81.21 Z1 J0.317 K0 F120
|
||||
G1 Y81.527
|
||||
G17 G3 X159.724 Y81.845 I-0.318 J0
|
||||
G1 X21.16 Z0.52
|
||||
G3 X21.16 Y78.155 Z0.5 I0 J-1.845
|
||||
G1 X159.724 Z0.02
|
||||
G3 X159.724 Y81.845 Z0 I0 J1.845
|
||||
G1 X21.16 Z-0.48
|
||||
G3 X21.16 Y78.155 Z-0.5 I0 J-1.845
|
||||
G1 X159.724 Z-0.98
|
||||
G3 X159.724 Y81.845 Z-1 I0 J1.845
|
||||
G1 X21.16 Z-1.48
|
||||
G3 X21.16 Y78.155 Z-1.5 I0 J-1.845
|
||||
G1 X159.724 Z-1.98
|
||||
G3 X159.724 Y81.845 Z-2 I0 J1.845
|
||||
G1 X21.16 Z-2.48
|
||||
G3 X21.16 Y78.155 Z-2.5 I0 J-1.845
|
||||
G1 X159.724 Z-2.98
|
||||
G3 X159.724 Y81.845 Z-3 I0 J1.845
|
||||
G1 X21.16 Z-3.48
|
||||
G3 X21.16 Y78.155 Z-3.5 I0 J-1.845
|
||||
G1 X159.724 Z-3.98
|
||||
G3 X159.724 Y81.845 Z-4 I0 J1.845
|
||||
G1 X21.16 Z-4.48
|
||||
G3 X21.16 Y78.155 Z-4.5 I0 J-1.845
|
||||
G1 X159.724 Z-4.98
|
||||
G3 X159.724 Y81.845 Z-5 I0 J1.845
|
||||
G1 X21.16 Z-5.48
|
||||
G3 X21.16 Y78.155 Z-5.5 I0 J-1.845
|
||||
G1 X159.724 Z-5.98
|
||||
G3 X159.724 Y81.845 Z-6 I0 J1.845
|
||||
G1 X21.16
|
||||
G3 X21.16 Y78.155 I0 J-1.845
|
||||
G1 X159.724
|
||||
G3 X159.724 Y81.845 I0 J1.845
|
||||
G2 X159.486 Y82.004 I0 J0.258
|
||||
G3 X159.248 Y82.162 I-0.238 J-0.099
|
||||
G1 X21.16 F90
|
||||
G3 X21.16 Y77.838 I0 J-2.162
|
||||
G1 X159.724
|
||||
G3 X159.724 Y82.162 I0 J2.162
|
||||
G1 X159.248
|
||||
G3 X158.93 Y81.845 I0 J-0.317 F120
|
||||
G1 Y81.527
|
||||
G19 G2 Y81.21 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X151.956 Y94.683
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G18 G2 X152.274 Z1 I0.318 K0 F120
|
||||
G1 X152.592
|
||||
G17 G3 X152.909 Y95 I0 J0.317
|
||||
X151.064 Y96.845 Z0.989 I-1.845 J0
|
||||
G1 X29.82 Z0.511
|
||||
G3 X29.82 Y93.155 Z0.489 I0 J-1.845
|
||||
G1 X151.064 Z0.011
|
||||
G3 X151.064 Y96.845 Z-0.011 I0 J1.845
|
||||
G1 X29.82 Z-0.489
|
||||
G3 X29.82 Y93.155 Z-0.511 I0 J-1.845
|
||||
G1 X151.064 Z-0.989
|
||||
G3 X151.064 Y96.845 Z-1.011 I0 J1.845
|
||||
G1 X29.82 Z-1.489
|
||||
G3 X29.82 Y93.155 Z-1.511 I0 J-1.845
|
||||
G1 X151.064 Z-1.989
|
||||
G3 X151.064 Y96.845 Z-2.011 I0 J1.845
|
||||
G1 X29.82 Z-2.489
|
||||
G3 X29.82 Y93.155 Z-2.511 I0 J-1.845
|
||||
G1 X151.064 Z-2.989
|
||||
G3 X151.064 Y96.845 Z-3.011 I0 J1.845
|
||||
G1 X29.82 Z-3.489
|
||||
G3 X29.82 Y93.155 Z-3.511 I0 J-1.845
|
||||
G1 X151.064 Z-3.989
|
||||
G3 X151.064 Y96.845 Z-4.011 I0 J1.845
|
||||
G1 X29.82 Z-4.489
|
||||
G3 X29.82 Y93.155 Z-4.511 I0 J-1.845
|
||||
G1 X151.064 Z-4.989
|
||||
G3 X151.064 Y96.845 Z-5.011 I0 J1.845
|
||||
G1 X29.82 Z-5.489
|
||||
G3 X29.82 Y93.155 Z-5.511 I0 J-1.845
|
||||
G1 X151.064 Z-5.989
|
||||
G3 X152.909 Y95 Z-6 I0 J1.845
|
||||
X151.064 Y96.845 I-1.845 J0
|
||||
G1 X29.82
|
||||
G3 X29.82 Y93.155 I0 J-1.845
|
||||
G1 X151.064
|
||||
G3 X152.909 Y95 I0 J1.845
|
||||
G2 X153.055 Y95.229 I0.253 J0
|
||||
G3 X153.174 Y95.472 I-0.092 J0.196
|
||||
X151.064 Y97.162 I-2.11 J-0.472 F90
|
||||
G1 X29.82
|
||||
G3 X29.82 Y92.838 I0 J-2.162
|
||||
G1 X151.064
|
||||
G3 X153.174 Y95.472 I0 J2.162
|
||||
X152.795 Y95.713 I-0.31 J-0.069 F120
|
||||
G1 X152.485 Y95.644
|
||||
X152.416 Y95.628 Z-5.992
|
||||
X152.351 Y95.613 Z-5.969
|
||||
X152.292 Y95.6 Z-5.931
|
||||
X152.243 Y95.589 Z-5.88
|
||||
X152.206 Y95.581 Z-5.82
|
||||
X152.183 Y95.576 Z-5.753
|
||||
X152.175 Y95.574 Z-5.683
|
||||
Z3
|
||||
X142.721 Y110.893
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G3 Y111.21 Z1 J0.317 K0 F120
|
||||
G1 Y111.527
|
||||
G17 G3 X142.404 Y111.845 I-0.317 J0
|
||||
G1 X38.481 Z0.526
|
||||
G3 X38.481 Y108.155 Z0.5 I0 J-1.845
|
||||
G1 X142.404 Z0.026
|
||||
G3 X142.404 Y111.845 Z0 I0 J1.845
|
||||
G1 X38.481 Z-0.474
|
||||
G3 X38.481 Y108.155 Z-0.5 I0 J-1.845
|
||||
G1 X142.404 Z-0.974
|
||||
G3 X142.404 Y111.845 Z-1 I0 J1.845
|
||||
G1 X38.481 Z-1.474
|
||||
G3 X38.481 Y108.155 Z-1.5 I0 J-1.845
|
||||
G1 X142.404 Z-1.974
|
||||
G3 X142.404 Y111.845 Z-2 I0 J1.845
|
||||
G1 X38.481 Z-2.474
|
||||
G3 X38.481 Y108.155 Z-2.5 I0 J-1.845
|
||||
G1 X142.404 Z-2.974
|
||||
G3 X142.404 Y111.845 Z-3 I0 J1.845
|
||||
G1 X38.481 Z-3.474
|
||||
G3 X38.481 Y108.155 Z-3.5 I0 J-1.845
|
||||
G1 X142.404 Z-3.974
|
||||
G3 X142.404 Y111.845 Z-4 I0 J1.845
|
||||
G1 X38.481 Z-4.474
|
||||
G3 X38.481 Y108.155 Z-4.5 I0 J-1.845
|
||||
G1 X142.404 Z-4.974
|
||||
G3 X142.404 Y111.845 Z-5 I0 J1.845
|
||||
G1 X38.481 Z-5.474
|
||||
G3 X38.481 Y108.155 Z-5.5 I0 J-1.845
|
||||
G1 X142.404 Z-5.974
|
||||
G3 X142.404 Y111.845 Z-6 I0 J1.845
|
||||
G1 X38.481
|
||||
G3 X38.481 Y108.155 I0 J-1.845
|
||||
G1 X142.404
|
||||
G3 X142.404 Y111.845 I0 J1.845
|
||||
G2 X142.166 Y112.004 I0 J0.258
|
||||
G3 X141.928 Y112.162 I-0.238 J-0.099
|
||||
G1 X38.481 F90
|
||||
G3 X38.481 Y107.838 I0 J-2.162
|
||||
G1 X142.404
|
||||
G3 X142.404 Y112.162 I0 J2.162
|
||||
G1 X141.928
|
||||
G3 X141.61 Y111.845 I0 J-0.317 F120
|
||||
G1 Y111.527
|
||||
G19 G2 Y111.21 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X46.824 Y124.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G2 Y123.79 Z1 J-0.317 K0 F120
|
||||
G1 Y123.473
|
||||
G17 G3 X47.141 Y123.155 I0.317 J0
|
||||
G1 X133.744 Z0.531
|
||||
G3 X133.744 Y126.845 Z0.5 I0 J1.845
|
||||
G1 X47.141 Z0.031
|
||||
G3 X47.141 Y123.155 Z0 I0 J-1.845
|
||||
G1 X133.744 Z-0.469
|
||||
G3 X133.744 Y126.845 Z-0.5 I0 J1.845
|
||||
G1 X47.141 Z-0.969
|
||||
G3 X47.141 Y123.155 Z-1 I0 J-1.845
|
||||
G1 X133.744 Z-1.469
|
||||
G3 X133.744 Y126.845 Z-1.5 I0 J1.845
|
||||
G1 X47.141 Z-1.969
|
||||
G3 X47.141 Y123.155 Z-2 I0 J-1.845
|
||||
G1 X133.744 Z-2.469
|
||||
G3 X133.744 Y126.845 Z-2.5 I0 J1.845
|
||||
G1 X47.141 Z-2.969
|
||||
G3 X47.141 Y123.155 Z-3 I0 J-1.845
|
||||
G1 X133.744 Z-3.469
|
||||
G3 X133.744 Y126.845 Z-3.5 I0 J1.845
|
||||
G1 X47.141 Z-3.969
|
||||
G3 X47.141 Y123.155 Z-4 I0 J-1.845
|
||||
G1 X133.744 Z-4.469
|
||||
G3 X133.744 Y126.845 Z-4.5 I0 J1.845
|
||||
G1 X47.141 Z-4.969
|
||||
G3 X47.141 Y123.155 Z-5 I0 J-1.845
|
||||
G1 X133.744 Z-5.469
|
||||
G3 X133.744 Y126.845 Z-5.5 I0 J1.845
|
||||
G1 X47.141 Z-5.969
|
||||
G3 X47.141 Y123.155 Z-6 I0 J-1.845
|
||||
G1 X133.744
|
||||
G3 X133.744 Y126.845 I0 J1.845
|
||||
G1 X47.141
|
||||
G3 X47.141 Y123.155 I0 J-1.845
|
||||
G2 X47.379 Y122.996 I0 J-0.258
|
||||
G3 X47.617 Y122.838 I0.238 J0.099
|
||||
G1 X133.744 F90
|
||||
G3 X133.744 Y127.162 I0 J2.162
|
||||
G1 X47.141
|
||||
G3 X47.141 Y122.838 I0 J-2.162
|
||||
G1 X47.617
|
||||
G3 X47.935 Y123.155 I0 J0.317 F120
|
||||
G1 Y123.473
|
||||
G19 G3 Y123.79 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X124.766 Y139.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G2 Y138.79 Z1 J-0.318 K0 F120
|
||||
G1 Y138.473
|
||||
G17 G3 X125.083 Y138.155 I0.317 J0
|
||||
X125.083 Y141.845 Z0.961 I0 J1.845
|
||||
G1 X55.801 Z0.5
|
||||
G3 X55.801 Y138.155 Z0.461 I0 J-1.845
|
||||
G1 X125.083 Z0
|
||||
G3 X125.083 Y141.845 Z-0.039 I0 J1.845
|
||||
G1 X55.801 Z-0.5
|
||||
G3 X55.801 Y138.155 Z-0.539 I0 J-1.845
|
||||
G1 X125.083 Z-1
|
||||
G3 X125.083 Y141.845 Z-1.039 I0 J1.845
|
||||
G1 X55.801 Z-1.5
|
||||
G3 X55.801 Y138.155 Z-1.539 I0 J-1.845
|
||||
G1 X125.083 Z-2
|
||||
G3 X125.083 Y141.845 Z-2.039 I0 J1.845
|
||||
G1 X55.801 Z-2.5
|
||||
G3 X55.801 Y138.155 Z-2.539 I0 J-1.845
|
||||
G1 X125.083 Z-3
|
||||
G3 X125.083 Y141.845 Z-3.039 I0 J1.845
|
||||
G1 X55.801 Z-3.5
|
||||
G3 X55.801 Y138.155 Z-3.539 I0 J-1.845
|
||||
G1 X125.083 Z-4
|
||||
G3 X125.083 Y141.845 Z-4.039 I0 J1.845
|
||||
G1 X55.801 Z-4.5
|
||||
G3 X55.801 Y138.155 Z-4.539 I0 J-1.845
|
||||
G1 X125.083 Z-5
|
||||
G3 X125.083 Y141.845 Z-5.039 I0 J1.845
|
||||
G1 X55.801 Z-5.5
|
||||
G3 X55.801 Y138.155 Z-5.539 I0 J-1.845
|
||||
G1 X125.083 Z-6
|
||||
G3 X125.083 Y141.845 I0 J1.845
|
||||
G1 X55.801
|
||||
G3 X55.801 Y138.155 I0 J-1.845
|
||||
G1 X125.083
|
||||
G2 X125.312 Y138.009 I0 J-0.253
|
||||
G3 X125.555 Y137.89 I0.196 J0.092
|
||||
X125.083 Y142.163 I-0.472 J2.11 F90
|
||||
G1 X55.801
|
||||
G3 X55.801 Y137.837 I0 J-2.163
|
||||
G1 X125.083
|
||||
G3 X125.555 Y137.89 I0 J2.163
|
||||
X125.796 Y138.269 I-0.069 J0.31 F120
|
||||
G1 X125.727 Y138.579
|
||||
X125.711 Y138.648 Z-5.992
|
||||
X125.696 Y138.713 Z-5.969
|
||||
X125.683 Y138.772 Z-5.931
|
||||
X125.672 Y138.821 Z-5.88
|
||||
X125.664 Y138.858 Z-5.82
|
||||
X125.659 Y138.881 Z-5.753
|
||||
X125.657 Y138.889 Z-5.683
|
||||
Z3
|
||||
X29.503 Y64.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G2 Y63.79 Z1 J-0.317 K0 F120
|
||||
G1 Y63.472
|
||||
G17 G3 X29.82 Y63.155 I0.317 J0
|
||||
G1 X151.064 Z0.523
|
||||
G3 X151.064 Y66.845 Z0.5 I0 J1.845
|
||||
G1 X29.82 Z0.023
|
||||
G3 X29.82 Y63.155 Z0 I0 J-1.845
|
||||
G1 X151.064 Z-0.477
|
||||
G3 X151.064 Y66.845 Z-0.5 I0 J1.845
|
||||
G1 X29.82 Z-0.977
|
||||
G3 X29.82 Y63.155 Z-1 I0 J-1.845
|
||||
G1 X151.064 Z-1.477
|
||||
G3 X151.064 Y66.845 Z-1.5 I0 J1.845
|
||||
G1 X29.82 Z-1.977
|
||||
G3 X29.82 Y63.155 Z-2 I0 J-1.845
|
||||
G1 X151.064 Z-2.477
|
||||
G3 X151.064 Y66.845 Z-2.5 I0 J1.845
|
||||
G1 X29.82 Z-2.977
|
||||
G3 X29.82 Y63.155 Z-3 I0 J-1.845
|
||||
G1 X151.064 Z-3.477
|
||||
G3 X151.064 Y66.845 Z-3.5 I0 J1.845
|
||||
G1 X29.82 Z-3.977
|
||||
G3 X29.82 Y63.155 Z-4 I0 J-1.845
|
||||
G1 X151.064 Z-4.477
|
||||
G3 X151.064 Y66.845 Z-4.5 I0 J1.845
|
||||
G1 X29.82 Z-4.977
|
||||
G3 X29.82 Y63.155 Z-5 I0 J-1.845
|
||||
G1 X151.064 Z-5.477
|
||||
G3 X151.064 Y66.845 Z-5.5 I0 J1.845
|
||||
G1 X29.82 Z-5.977
|
||||
G3 X29.82 Y63.155 Z-6 I0 J-1.845
|
||||
G1 X151.064
|
||||
G3 X151.064 Y66.845 I0 J1.845
|
||||
G1 X29.82
|
||||
G3 X29.82 Y63.155 I0 J-1.845
|
||||
G2 X30.059 Y62.996 I0 J-0.258
|
||||
G3 X30.297 Y62.838 I0.238 J0.099
|
||||
G1 X151.064 F90
|
||||
G3 X151.064 Y67.162 I0 J2.162
|
||||
G1 X29.82
|
||||
G3 X29.82 Y62.838 I0 J-2.162
|
||||
G1 X30.297
|
||||
G3 X30.614 Y63.155 I0 J0.317 F120
|
||||
G1 Y63.472
|
||||
G19 G3 Y63.79 Z-5.683 J0 K0.318
|
||||
G1 Z3
|
||||
X143.296 Y49.682
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G18 G2 X143.614 Z1 I0.318 K0 F120
|
||||
G1 X143.932
|
||||
G17 G3 X144.249 Y50 I0 J0.318
|
||||
X142.404 Y51.845 Z0.987 I-1.845 J0
|
||||
G1 X38.481 Z0.513
|
||||
G3 X38.481 Y48.155 Z0.487 I0 J-1.845
|
||||
G1 X142.404 Z0.013
|
||||
G3 X142.404 Y51.845 Z-0.013 I0 J1.845
|
||||
G1 X38.481 Z-0.487
|
||||
G3 X38.481 Y48.155 Z-0.513 I0 J-1.845
|
||||
G1 X142.404 Z-0.987
|
||||
G3 X142.404 Y51.845 Z-1.013 I0 J1.845
|
||||
G1 X38.481 Z-1.487
|
||||
G3 X38.481 Y48.155 Z-1.513 I0 J-1.845
|
||||
G1 X142.404 Z-1.987
|
||||
G3 X142.404 Y51.845 Z-2.013 I0 J1.845
|
||||
G1 X38.481 Z-2.487
|
||||
G3 X38.481 Y48.155 Z-2.513 I0 J-1.845
|
||||
G1 X142.404 Z-2.987
|
||||
G3 X142.404 Y51.845 Z-3.013 I0 J1.845
|
||||
G1 X38.481 Z-3.487
|
||||
G3 X38.481 Y48.155 Z-3.513 I0 J-1.845
|
||||
G1 X142.404 Z-3.987
|
||||
G3 X142.404 Y51.845 Z-4.013 I0 J1.845
|
||||
G1 X38.481 Z-4.487
|
||||
G3 X38.481 Y48.155 Z-4.513 I0 J-1.845
|
||||
G1 X142.404 Z-4.987
|
||||
G3 X142.404 Y51.845 Z-5.013 I0 J1.845
|
||||
G1 X38.481 Z-5.487
|
||||
G3 X38.481 Y48.155 Z-5.513 I0 J-1.845
|
||||
G1 X142.404 Z-5.987
|
||||
G3 X144.249 Y50 Z-6 I0 J1.845
|
||||
X142.404 Y51.845 I-1.845 J0
|
||||
G1 X38.481
|
||||
G3 X38.481 Y48.155 I0 J-1.845
|
||||
G1 X142.404
|
||||
G3 X144.249 Y50 I0 J1.845
|
||||
G2 X144.395 Y50.229 I0.253 J0
|
||||
G3 X144.514 Y50.472 I-0.092 J0.196
|
||||
X142.404 Y52.162 I-2.11 J-0.472 F90
|
||||
G1 X38.481
|
||||
G3 X38.481 Y47.838 I0 J-2.162
|
||||
G1 X142.404
|
||||
G3 X144.514 Y50.472 I0 J2.162
|
||||
X144.135 Y50.713 I-0.31 J-0.069 F120
|
||||
G1 X143.825 Y50.644
|
||||
X143.756 Y50.628 Z-5.992
|
||||
X143.691 Y50.613 Z-5.969
|
||||
X143.632 Y50.6 Z-5.931
|
||||
X143.583 Y50.589 Z-5.88
|
||||
X143.546 Y50.581 Z-5.82
|
||||
X143.523 Y50.576 Z-5.753
|
||||
X143.515 Y50.574 Z-5.683
|
||||
Z3
|
||||
X133.426 Y34.107
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G2 Y33.79 Z1 J-0.317 K0 F120
|
||||
G1 Y33.472
|
||||
G17 G3 X133.744 Y33.155 I0.318 J0
|
||||
X133.744 Y36.845 Z0.969 I0 J1.845
|
||||
G1 X47.141 Z0.5
|
||||
G3 X47.141 Y33.155 Z0.469 I0 J-1.845
|
||||
G1 X133.744 Z0
|
||||
G3 X133.744 Y36.845 Z-0.031 I0 J1.845
|
||||
G1 X47.141 Z-0.5
|
||||
G3 X47.141 Y33.155 Z-0.531 I0 J-1.845
|
||||
G1 X133.744 Z-1
|
||||
G3 X133.744 Y36.845 Z-1.031 I0 J1.845
|
||||
G1 X47.141 Z-1.5
|
||||
G3 X47.141 Y33.155 Z-1.531 I0 J-1.845
|
||||
G1 X133.744 Z-2
|
||||
G3 X133.744 Y36.845 Z-2.031 I0 J1.845
|
||||
G1 X47.141 Z-2.5
|
||||
G3 X47.141 Y33.155 Z-2.531 I0 J-1.845
|
||||
G1 X133.744 Z-3
|
||||
G3 X133.744 Y36.845 Z-3.031 I0 J1.845
|
||||
G1 X47.141 Z-3.5
|
||||
G3 X47.141 Y33.155 Z-3.531 I0 J-1.845
|
||||
G1 X133.744 Z-4
|
||||
G3 X133.744 Y36.845 Z-4.031 I0 J1.845
|
||||
G1 X47.141 Z-4.5
|
||||
G3 X47.141 Y33.155 Z-4.531 I0 J-1.845
|
||||
G1 X133.744 Z-5
|
||||
G3 X133.744 Y36.845 Z-5.031 I0 J1.845
|
||||
G1 X47.141 Z-5.5
|
||||
G3 X47.141 Y33.155 Z-5.531 I0 J-1.845
|
||||
G1 X133.744 Z-6
|
||||
G3 X133.744 Y36.845 I0 J1.845
|
||||
G1 X47.141
|
||||
G3 X47.141 Y33.155 I0 J-1.845
|
||||
G1 X133.744
|
||||
G2 X133.973 Y33.009 I0 J-0.253
|
||||
G3 X134.216 Y32.89 I0.196 J0.092
|
||||
X133.744 Y37.162 I-0.472 J2.11 F90
|
||||
G1 X47.141
|
||||
G3 X47.141 Y32.838 I0 J-2.162
|
||||
G1 X133.744
|
||||
G3 X134.216 Y32.89 I0 J2.162
|
||||
X134.457 Y33.269 I-0.069 J0.31 F120
|
||||
G1 X134.388 Y33.579
|
||||
X134.372 Y33.648 Z-5.992
|
||||
X134.357 Y33.713 Z-5.969
|
||||
X134.344 Y33.772 Z-5.931
|
||||
X134.333 Y33.821 Z-5.88
|
||||
X134.325 Y33.858 Z-5.82
|
||||
X134.32 Y33.881 Z-5.753
|
||||
X134.318 Y33.889 Z-5.683
|
||||
Z3
|
||||
X124.766 Y19.108
|
||||
Z2
|
||||
Z1.317 F10
|
||||
G19 G2 Y18.79 Z1 J-0.317 K0 F120
|
||||
G1 Y18.472
|
||||
G17 G3 X125.083 Y18.155 I0.317 J0
|
||||
X125.083 Y21.845 Z0.961 I0 J1.845
|
||||
G1 X55.801 Z0.5
|
||||
G3 X55.801 Y18.155 Z0.461 I0 J-1.845
|
||||
G1 X125.083 Z0
|
||||
G3 X125.083 Y21.845 Z-0.039 I0 J1.845
|
||||
G1 X55.801 Z-0.5
|
||||
G3 X55.801 Y18.155 Z-0.539 I0 J-1.845
|
||||
G1 X125.083 Z-1
|
||||
G3 X125.083 Y21.845 Z-1.039 I0 J1.845
|
||||
G1 X55.801 Z-1.5
|
||||
G3 X55.801 Y18.155 Z-1.539 I0 J-1.845
|
||||
G1 X125.083 Z-2
|
||||
G3 X125.083 Y21.845 Z-2.039 I0 J1.845
|
||||
G1 X55.801 Z-2.5
|
||||
G3 X55.801 Y18.155 Z-2.539 I0 J-1.845
|
||||
G1 X125.083 Z-3
|
||||
G3 X125.083 Y21.845 Z-3.039 I0 J1.845
|
||||
G1 X55.801 Z-3.5
|
||||
G3 X55.801 Y18.155 Z-3.539 I0 J-1.845
|
||||
G1 X125.083 Z-4
|
||||
G3 X125.083 Y21.845 Z-4.039 I0 J1.845
|
||||
G1 X55.801 Z-4.5
|
||||
G3 X55.801 Y18.155 Z-4.539 I0 J-1.845
|
||||
G1 X125.083 Z-5
|
||||
G3 X125.083 Y21.845 Z-5.039 I0 J1.845
|
||||
G1 X55.801 Z-5.5
|
||||
G3 X55.801 Y18.155 Z-5.539 I0 J-1.845
|
||||
G1 X125.083 Z-6
|
||||
G3 X125.083 Y21.845 I0 J1.845
|
||||
G1 X55.801
|
||||
G3 X55.801 Y18.155 I0 J-1.845
|
||||
G1 X125.083
|
||||
G2 X125.312 Y18.009 I0 J-0.253
|
||||
G3 X125.555 Y17.89 I0.196 J0.092
|
||||
X125.083 Y22.163 I-0.472 J2.11 F90
|
||||
G1 X55.801
|
||||
G3 X55.801 Y17.837 I0 J-2.163
|
||||
G1 X125.083
|
||||
G3 X125.555 Y17.89 I0 J2.163
|
||||
X125.796 Y18.269 I-0.069 J0.31 F120
|
||||
G1 X125.727 Y18.579
|
||||
X125.711 Y18.648 Z-5.992
|
||||
X125.696 Y18.713 Z-5.969
|
||||
X125.683 Y18.772 Z-5.931
|
||||
X125.672 Y18.821 Z-5.88
|
||||
X125.664 Y18.858 Z-5.82
|
||||
X125.659 Y18.881 Z-5.753
|
||||
X125.657 Y18.889 Z-5.683
|
||||
Z5
|
||||
G28 G91 Z0
|
||||
G90
|
||||
G53 G0 X0 Y0
|
||||
M5
|
||||
M30
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":semanticCommits",
|
||||
":semanticCommitTypeAll(chore)"
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.Block.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Block, Serialise, Word}
|
||||
alias Gcode.Model.{Block, Expr, Serialise, Word}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
|
@ -8,11 +8,12 @@ defmodule Gcode.Model.Block.SerialiseTest do
|
|||
assert ok(block) =
|
||||
with(
|
||||
ok(block) <- Block.init(),
|
||||
ok(word) <- Word.init("G", 0),
|
||||
ok(address) <- Expr.Integer.init(0),
|
||||
ok(word) <- Word.init("G", address),
|
||||
ok(block) <- Block.push(block, word),
|
||||
ok(word) <- Word.init("N", 100),
|
||||
ok(block) <- Block.push(block, word),
|
||||
do: ok(block)
|
||||
ok(address) <- Expr.Integer.init(100),
|
||||
ok(word) <- Word.init("N", address),
|
||||
do: Block.push(block, word)
|
||||
)
|
||||
|
||||
assert ok([actual]) = Serialise.serialise(block)
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.BlockTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Block, Comment, Word}
|
||||
alias Gcode.Model.{Block, Comment, Expr.Integer, Word}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
doctest Gcode.Model.Block
|
|
@ -1,6 +1,7 @@
|
|||
defmodule Gcode.Model.Comment.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Comment, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "serialise/1" do
|
||||
|
@ -13,8 +14,8 @@ defmodule Gcode.Model.Comment.SerialiseTest do
|
|||
"""
|
||||
|
||||
actual =
|
||||
with {:ok, comment} <- Comment.init(comment),
|
||||
{:ok, comment} <- Serialise.serialise(comment),
|
||||
with ok(comment) <- Comment.init(comment),
|
||||
ok(comment) <- Serialise.serialise(comment),
|
||||
do: comment
|
||||
|
||||
expected = ~w[(This) (is) (a) (test)]
|
53
test/gcode/model/expr/binary/expr_test.exs
Normal file
53
test/gcode/model/expr/binary/expr_test.exs
Normal file
|
@ -0,0 +1,53 @@
|
|||
defmodule Gcode.Model.Expr.Binary.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Gcode.Result
|
||||
import InfixHelper
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
it_evaluates_to(:*, 2, 3, ok(6))
|
||||
it_evaluates_to(:*, 2.0, 3.0, ok(6.0))
|
||||
it_evaluates_to(:*, 2, 3.0, error({:program_error, _}))
|
||||
it_evaluates_to(:/, 3.0, 2.0, ok(1.5))
|
||||
it_evaluates_to(:/, 2, 3, error({:program_error, _}))
|
||||
it_evaluates_to(:+, 2, 3, ok(5))
|
||||
it_evaluates_to(:+, 2.0, 3.0, ok(5.0))
|
||||
it_evaluates_to(:-, 2, 3, ok(-1))
|
||||
it_evaluates_to(:-, 2.0, 3.0, ok(-1.0))
|
||||
it_evaluates_to(:==, 1, 1, ok(true))
|
||||
it_evaluates_to(:==, 1, 2, ok(false))
|
||||
it_evaluates_to(:==, 1.0, 1.0, ok(true))
|
||||
it_evaluates_to(:==, 1.0, 2.0, ok(false))
|
||||
it_evaluates_to(:==, "a", "a", ok(true))
|
||||
it_evaluates_to(:==, "a", "b", ok(false))
|
||||
it_evaluates_to(:!=, 1, 1, ok(false))
|
||||
it_evaluates_to(:!=, 1, 2, ok(true))
|
||||
it_evaluates_to(:!=, 1.0, 1.0, ok(false))
|
||||
it_evaluates_to(:!=, 1.0, 2.0, ok(true))
|
||||
it_evaluates_to(:!=, "a", "a", ok(false))
|
||||
it_evaluates_to(:!=, "a", "b", ok(true))
|
||||
it_evaluates_to(:<, 1, 2, ok(true))
|
||||
it_evaluates_to(:<, 1, 1, ok(false))
|
||||
it_evaluates_to(:<, 1.0, 2.0, ok(true))
|
||||
it_evaluates_to(:<, 1.0, 1.0, ok(false))
|
||||
it_evaluates_to(:<=, 1, 2, ok(true))
|
||||
it_evaluates_to(:<=, 1, 1, ok(true))
|
||||
it_evaluates_to(:<=, 1.0, 2.0, ok(true))
|
||||
it_evaluates_to(:<=, 1.0, 1.0, ok(true))
|
||||
it_evaluates_to(:>, 1, 2, ok(false))
|
||||
it_evaluates_to(:>, 1, 1, ok(false))
|
||||
it_evaluates_to(:>, 1.0, 2.0, ok(false))
|
||||
it_evaluates_to(:>, 1.0, 1.0, ok(false))
|
||||
it_evaluates_to(:>=, 1, 2, ok(false))
|
||||
it_evaluates_to(:>=, 1, 1, ok(true))
|
||||
it_evaluates_to(:>=, 1.0, 2.0, ok(false))
|
||||
it_evaluates_to(:>=, 1.0, 1.0, ok(true))
|
||||
it_evaluates_to(:&&, true, true, ok(true))
|
||||
it_evaluates_to(:&&, true, false, ok(false))
|
||||
it_evaluates_to(:&&, false, false, ok(false))
|
||||
it_evaluates_to(:||, true, true, ok(true))
|
||||
it_evaluates_to(:||, true, false, ok(true))
|
||||
it_evaluates_to(:||, false, false, ok(false))
|
||||
it_evaluates_to(:^, "a", "b", ok("ab"))
|
||||
end
|
||||
end
|
20
test/gcode/model/expr/binary/serialise_test.exs
Normal file
20
test/gcode/model/expr/binary/serialise_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Gcode.Model.Expr.Binary.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Binary, Expr.Integer, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
for op <- ~w[* / + - == != < <= > >= && || ^]a do
|
||||
quote do
|
||||
test "when the operator is `#{unquote(op)}` it serialises correctly" do
|
||||
ok(lhs) = Integer.init(1)
|
||||
ok(rhs) = Integer.init(2)
|
||||
ok(unary) = Binary.init(unquote(op), lhs, rhs)
|
||||
as_s = to_string(unquote(op))
|
||||
assert ok(["1", as_s, "2"]) = Serialise.serialise(unary)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
17
test/gcode/model/expr/binary_test.exs
Normal file
17
test/gcode/model/expr/binary_test.exs
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Gcode.Model.Expr.BinaryTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.{Binary, Integer}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/3" do
|
||||
test "when the operator and expressions are valid, it is ok" do
|
||||
ok(lhs) = Integer.init(1)
|
||||
ok(rhs) = Integer.init(2)
|
||||
|
||||
assert ok(%Binary{op: some(:-), lhs: some(^lhs), rhs: some(^rhs)}) =
|
||||
Binary.init(:-, lhs, rhs)
|
||||
end
|
||||
end
|
||||
end
|
18
test/gcode/model/expr/boolean/expr_test.exs
Normal file
18
test/gcode/model/expr/boolean/expr_test.exs
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Gcode.Model.Expr.Boolean.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Boolean}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the value is `true` it evaluates to `true`" do
|
||||
ok(bool) = Boolean.init(true)
|
||||
assert ok(true) = Expr.evaluate(bool)
|
||||
end
|
||||
|
||||
test "when the value is `false` it evaluates to `false`" do
|
||||
ok(bool) = Boolean.init(false)
|
||||
assert ok(false) = Expr.evaluate(bool)
|
||||
end
|
||||
end
|
||||
end
|
18
test/gcode/model/expr/boolean/serialise_test.exs
Normal file
18
test/gcode/model/expr/boolean/serialise_test.exs
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Gcode.Model.Expr.Boolean.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Boolean, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "when the value is `true` it is serialised to `\"true\"`" do
|
||||
ok(bool) = Boolean.init(true)
|
||||
assert ok(["true"]) = Serialise.serialise(bool)
|
||||
end
|
||||
|
||||
test "when the value is `false` it is serialised to `\"false\"`" do
|
||||
ok(bool) = Boolean.init(false)
|
||||
assert ok(["false"]) = Serialise.serialise(bool)
|
||||
end
|
||||
end
|
||||
end
|
20
test/gcode/model/expr/boolean_test.exs
Normal file
20
test/gcode/model/expr/boolean_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Gcode.Model.Expr.BooleanTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Boolean
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "when the argument is `true` it is ok" do
|
||||
assert ok(%Boolean{}) = Boolean.init(true)
|
||||
end
|
||||
|
||||
test "when the argument is `false` it is ok" do
|
||||
assert ok(%Boolean{}) = Boolean.init(false)
|
||||
end
|
||||
|
||||
test "when passed any other argument, it fails" do
|
||||
assert error({:expression_error, _}) = Boolean.init(nil)
|
||||
end
|
||||
end
|
||||
end
|
29
test/gcode/model/expr/constant/expr_test.exs
Normal file
29
test/gcode/model/expr/constant/expr_test.exs
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule Gcode.Model.Expr.Constant.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Constant}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the constant is `:pi` it returns Pi" do
|
||||
ok(const) = Constant.init(:pi)
|
||||
assert ok(pi) = Expr.evaluate(const)
|
||||
assert_in_delta :math.pi(), pi, 0.1
|
||||
end
|
||||
|
||||
test "when the constant is `line` it returns an error" do
|
||||
ok(const) = Constant.init(:line)
|
||||
assert error(_) = Expr.evaluate(const)
|
||||
end
|
||||
|
||||
test "when the constant is `null` it returns `nil`" do
|
||||
ok(const) = Constant.init(:null)
|
||||
assert ok(nil) = Expr.evaluate(const)
|
||||
end
|
||||
|
||||
test "when the constant is `result` it returns an error" do
|
||||
ok(const) = Constant.init(:result)
|
||||
assert error(_) = Expr.evaluate(const)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/constant/serialise_test.exs
Normal file
13
test/gcode/model/expr/constant/serialise_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Constant.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Constant, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it serialises correctly" do
|
||||
ok(const) = Constant.init(:pi)
|
||||
assert ok(["pi"]) = Serialise.serialise(const)
|
||||
end
|
||||
end
|
||||
end
|
20
test/gcode/model/expr/constant_test.exs
Normal file
20
test/gcode/model/expr/constant_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Gcode.Model.Expr.ConstantTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Constant
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
for name <- ~w[iterations line null pi result]a do
|
||||
quote do
|
||||
test "when the argument is `#{unquote(name)}`, it is ok" do
|
||||
assert ok(%Constant{name: unquote(name)}) = Constant.init(unquote(name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "otherwise, it is an error" do
|
||||
assert error(_) = Constant.init(:wat)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/float/expr_test.exs
Normal file
13
test/gcode/model/expr/float/expr_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Float.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Float}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the value is is a float it evaluates to it" do
|
||||
ok(float) = Float.init(1.23)
|
||||
assert ok(1.23) = Expr.evaluate(float)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/float/serialise_test.exs
Normal file
13
test/gcode/model/expr/float/serialise_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Float.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Float, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it serialises correctly" do
|
||||
ok(float) = Float.init(1.23)
|
||||
assert ok(["1.23"]) = Serialise.serialise(float)
|
||||
end
|
||||
end
|
||||
end
|
16
test/gcode/model/expr/float_test.exs
Normal file
16
test/gcode/model/expr/float_test.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Gcode.Model.Expr.FloatTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Float
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "when the value is a float, it is ok" do
|
||||
assert ok(%Float{}) = Float.init(1.23)
|
||||
end
|
||||
|
||||
test "when the value is not a float, it is an error" do
|
||||
assert error({:expression_error, _}) = Float.init(123)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/integer/expr_test.exs
Normal file
13
test/gcode/model/expr/integer/expr_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Integer.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr, Expr.Integer}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the value is is an integer it evaluates to it" do
|
||||
ok(integer) = Integer.init(123)
|
||||
assert ok(123) = Expr.evaluate(integer)
|
||||
end
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/integer/serialise_test.exs
Normal file
13
test/gcode/model/expr/integer/serialise_test.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Gcode.Model.Expr.Integer.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Integer, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it serialises correctly" do
|
||||
ok(float) = Integer.init(123)
|
||||
assert ok(["123"]) = Serialise.serialise(float)
|
||||
end
|
||||
end
|
||||
end
|
16
test/gcode/model/expr/integer_test.exs
Normal file
16
test/gcode/model/expr/integer_test.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Gcode.Model.Expr.IntegerTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.Integer
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "when the value is an integer, it is ok" do
|
||||
assert ok(%Integer{}) = Integer.init(123)
|
||||
end
|
||||
|
||||
test "when the value is not an integer, it is an error" do
|
||||
assert error({:expression_error, _}) = Integer.init(1.23)
|
||||
end
|
||||
end
|
||||
end
|
14
test/gcode/model/expr/list/expr.ex
Normal file
14
test/gcode/model/expr/list/expr.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.List do
|
||||
alias Gcode.Model.{Expr, Expr.List}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
@spec evaluate(List.t()) :: Expr.result()
|
||||
def evaluate(%List{elements: elements}) do
|
||||
elements =
|
||||
elements
|
||||
|> Enum.map(&Expr.evaluate/1)
|
||||
|
||||
ok(elements)
|
||||
end
|
||||
end
|
13
test/gcode/model/expr/list/serialise.ex
Normal file
13
test/gcode/model/expr/list/serialise.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.List do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.List, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
test "it cannot be serialised" do
|
||||
ok(list) = List.init()
|
||||
assert error(_) = Serialise.serialise(list)
|
||||
end
|
||||
end
|
||||
end
|
25
test/gcode/model/expr/list_test.exs
Normal file
25
test/gcode/model/expr/list_test.exs
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule Gcode.Model.Expr.ListTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.{Integer, List}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/1" do
|
||||
test "it is ok" do
|
||||
assert ok(%List{}) = List.init()
|
||||
end
|
||||
end
|
||||
|
||||
describe "push/2" do
|
||||
test "when the element is an expression, it is ok" do
|
||||
ok(list) = List.init()
|
||||
ok(expr) = Integer.init(123)
|
||||
assert ok(%List{elements: [^expr]}) = List.push(list, expr)
|
||||
end
|
||||
|
||||
test "otherwise it's an error" do
|
||||
ok(list) = List.init()
|
||||
assert error({:expression_error, _}) = List.push(list, :marty)
|
||||
end
|
||||
end
|
||||
end
|
63
test/gcode/model/expr/unary/expr_test.exs
Normal file
63
test/gcode/model/expr/unary/expr_test.exs
Normal file
|
@ -0,0 +1,63 @@
|
|||
defmodule Gcode.Model.Expr.Unary.ExprTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Gcode.Model.{
|
||||
Expr,
|
||||
Expr.Boolean,
|
||||
Expr.Float,
|
||||
Expr.Integer,
|
||||
Expr.List,
|
||||
Expr.String,
|
||||
Expr.Unary
|
||||
}
|
||||
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Expr.evaluate/1" do
|
||||
test "when the operator is `!` and the inner value evaluates to a boolean, it returns it's inverse" do
|
||||
ok(inner) = Boolean.init(true)
|
||||
ok(unary) = Unary.init(:!, inner)
|
||||
assert ok(false) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `!` and the inner value evaluates to a non-boolean, it returns an error" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(unary) = Unary.init(:!, inner)
|
||||
assert error({:program_error, _}) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `+` the inner value is an integer, it evaluates it" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(inner) = Unary.init(:-, inner)
|
||||
ok(unary) = Unary.init(:+, inner)
|
||||
assert ok(-123) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `+` the inner value is an float, it evaluates it" do
|
||||
ok(inner) = Float.init(1.23)
|
||||
ok(unary) = Unary.init(:+, inner)
|
||||
assert ok(1.23) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `#` the inner value evaluates to a list, it returns it's length" do
|
||||
ok(list) = List.init()
|
||||
ok(int) = Integer.init(123)
|
||||
ok(inner) = List.push(list, int)
|
||||
ok(unary) = Unary.init(:"#", inner)
|
||||
assert ok(1) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `#` the inner value evaluates to a string, it returns it's length" do
|
||||
ok(inner) = String.init("Marty McFly")
|
||||
ok(unary) = Unary.init(:"#", inner)
|
||||
assert ok(11) = Expr.evaluate(unary)
|
||||
end
|
||||
|
||||
test "when the operator is `#`, otherwise it returns an error" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(unary) = Unary.init(:"#", inner)
|
||||
assert error({:program_error, _}) = Expr.evaluate(unary)
|
||||
end
|
||||
end
|
||||
end
|
19
test/gcode/model/expr/unary/serialise_test.exs
Normal file
19
test/gcode/model/expr/unary/serialise_test.exs
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Gcode.Model.Expr.Unary.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Expr.Integer, Expr.Unary, Serialise}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "Serialise.serialise/1" do
|
||||
for op <- ~w[! + - #]a do
|
||||
quote do
|
||||
test "when the operator is `#{unquote(op)}` it serialises correctly" do
|
||||
ok(inner) = Integer.init(123)
|
||||
ok(unary) = Unary.init(unquote(op), inner)
|
||||
as_s = to_string(unquote(op))
|
||||
assert ok([as_s, "123"]) = Serialise.serialise(unary)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
27
test/gcode/model/expr/unary_test.exs
Normal file
27
test/gcode/model/expr/unary_test.exs
Normal file
|
@ -0,0 +1,27 @@
|
|||
defmodule Gcode.Model.Expr.UnaryTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Expr.{Integer, Unary}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "init/2" do
|
||||
test "when the operator is valid and the inner value is an expression, it is ok" do
|
||||
ok(inner) = Integer.init(123)
|
||||
assert ok(%Unary{op: some(:-), expr: some(^inner)}) = Unary.init(:-, inner)
|
||||
end
|
||||
|
||||
test "when the operator is valid and the inner value is not an expresion, it is an error" do
|
||||
assert error({:expression_error, _}) = Unary.init(:-, 1.21)
|
||||
end
|
||||
|
||||
test "when the inner value is an expression but the operator is invalid, it an error" do
|
||||
ok(inner) = Integer.init(123)
|
||||
assert error({:expression_error, _}) = Unary.init(:%, inner)
|
||||
end
|
||||
|
||||
test "when both the operator and inner value are invalid, it is an error" do
|
||||
assert error({:expression_error, _}) = Unary.init(:%, 1.21)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,15 @@
|
|||
defmodule Gcode.Model.Skip.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Serialise, Skip}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "serialise/1" do
|
||||
test "when the skip has a number, it formats it correctly" do
|
||||
actual =
|
||||
with {:ok, skip} <- Skip.init(0), {:ok, skip} <- Serialise.serialise(skip), do: skip
|
||||
with ok(skip) <- Skip.init(0),
|
||||
ok(skip) <- Serialise.serialise(skip),
|
||||
do: skip
|
||||
|
||||
expected = ~w[/0]
|
||||
|
||||
|
@ -14,7 +17,10 @@ defmodule Gcode.Model.Skip.SerialiseTest do
|
|||
end
|
||||
|
||||
test "when the skip has no number, it formats it correctly" do
|
||||
actual = with {:ok, skip} <- Skip.init(), {:ok, skip} <- Serialise.serialise(skip), do: skip
|
||||
actual =
|
||||
with ok(skip) <- Skip.init(),
|
||||
ok(skip) <- Serialise.serialise(skip),
|
||||
do: skip
|
||||
|
||||
expected = ~w[/]
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
defmodule Gcode.Model.Word.SerialiseTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Serialise, Word}
|
||||
use Gcode.Result
|
||||
@moduledoc false
|
||||
|
||||
describe "serialise/1" do
|
||||
test "formats the word and the address correctly" do
|
||||
actual =
|
||||
with {:ok, word} <- Word.init("G", 0), {:ok, word} <- Serialise.serialise(word), do: word
|
||||
with ok(word) <- Word.init("G", 0),
|
||||
ok(word) <- Serialise.serialise(word),
|
||||
do: word
|
||||
|
||||
expected = ~w[G0]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Gcode.Model.WordTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.Word
|
||||
alias Gcode.Model.{Expr.Integer, Word}
|
||||
doctest Gcode.Model.Word
|
||||
@moduledoc false
|
||||
end
|
60
test/gcode/parser/engine_test.exs
Normal file
60
test/gcode/parser/engine_test.exs
Normal file
|
@ -0,0 +1,60 @@
|
|||
defmodule Gcode.Parser.EngineTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Gcode.Result
|
||||
use ParserEngineHelper
|
||||
import FixtureHelper
|
||||
@moduledoc false
|
||||
|
||||
it_parses_into("%", tape: ~c"")
|
||||
it_parses_into("% hello", tape: ~c"hello")
|
||||
it_parses_into("()", comment: ~c"")
|
||||
it_parses_into("(hello)", comment: ~c"hello")
|
||||
it_parses_into("( hello )", comment: ~c"hello")
|
||||
it_parses_into("; hello", comment: ~c"hello")
|
||||
it_parses_into("G0", block: [word: [command: ~c"G", address: [integer: ~c"0"]]])
|
||||
it_parses_into("G 0", block: [word: [command: ~c"G", address: [integer: ~c"0"]]])
|
||||
it_parses_into("G54.1", block: [word: [command: ~c"G", address: [float: ~c"54.1"]]])
|
||||
it_parses_into("G 54.1", block: [word: [command: ~c"G", address: [float: ~c"54.1"]]])
|
||||
it_parses_into("G-1", block: [word: [command: ~c"G", address: [{:-, [integer: ~c"1"]}]]])
|
||||
it_parses_into("G+1", block: [word: [command: ~c"G", address: [{:+, [integer: ~c"1"]}]]])
|
||||
it_parses_into("G!1", block: [word: [command: ~c"G", address: [{:!, [integer: ~c"1"]}]]])
|
||||
|
||||
it_parses_into("G1 X112.518 Y131.525 E59.51636 (hello)",
|
||||
block: [
|
||||
word: [command: ~c"G", address: [integer: ~c"1"]],
|
||||
word: [command: ~c"X", address: [float: ~c"112.518"]],
|
||||
word: [command: ~c"Y", address: [float: ~c"131.525"]],
|
||||
word: [command: ~c"E", address: [float: ~c"59.51636"]],
|
||||
comment: ~c"hello"
|
||||
]
|
||||
)
|
||||
|
||||
it_parses_into("M82 ;absolute extrusion mode",
|
||||
block: [
|
||||
word: [command: ~c"M", address: [integer: ~c"82"]],
|
||||
comment: ~c"absolute extrusion mode"
|
||||
]
|
||||
)
|
||||
|
||||
it_parses_into("M117 Hello world",
|
||||
block: [word: [command: ~c"M", address: [integer: ~c"117"]], string: ~c"Hello world"]
|
||||
)
|
||||
|
||||
it_parses_into(read_fixture("fusion_360_milling_grbl.nc"), fn tokens ->
|
||||
lines =
|
||||
tokens
|
||||
|> Enum.reject(&(elem(&1, 0) == :newline))
|
||||
|> Enum.count()
|
||||
|
||||
assert lines == 500
|
||||
end)
|
||||
|
||||
it_parses_into(read_fixture("cura_marlin.gcode"), fn tokens ->
|
||||
lines =
|
||||
tokens
|
||||
|> Enum.reject(&(elem(&1, 0) == :newline))
|
||||
|> Enum.count()
|
||||
|
||||
assert lines == 6723
|
||||
end)
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue