Compare commits

...

30 commits
v0.3.0 ... main

Author SHA1 Message Date
6bc6a38531
chore: fix docs release.
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-14 19:22:24 +13:00
b855a8c388
chore: fix typo in readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-08 14:44:52 +13:00
24e93521f9
chore: Update docs and package links.
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-07 19:47:54 +13:00
fd043e41b5
chore: Simplify CI configuration. 2024-03-07 19:47:54 +13:00
8400584ebb chore: remove old renovate configuration
Some checks failed
continuous-integration/drone/push Build is failing
2024-02-14 15:01:31 +13:00
69dec00654 chore: Update forgejo hostname.
Some checks failed
continuous-integration/drone/push Build is failing
2024-02-05 15:52:27 +13:00
1e382f069f
chore: add Drone CI configuration
Some checks failed
continuous-integration/drone/push Build is failing
2023-07-21 19:39:52 +12:00
93c3165678
chore: run formatter. 2023-07-09 14:04:30 +12:00
681c4e3bd8 chore(deps): update dependency ex_doc to ~> 0.30 2023-07-07 23:05:51 +12:00
494122a037 chore: release version v1.0.0 2023-01-16 22:15:25 +00:00
7ffb351379 chore!: Relicense to HL3-FULL. 2023-01-17 11:11:14 +13:00
3a8d98f8f5 chore(deps): update dependency ex_doc to ~> 0.29 2022-10-20 05:57:54 +13:00
a6da6c0a4f chore(deps): update dependency ex_doc to >= 0.28.1 2022-02-23 10:58:51 +13:00
dac0b85e9f chore(deps): update dependency earmark to >= 1.4.20 2022-01-31 12:34:22 +13:00
83f21036b2 chore(deps): update dependency parallel_stream to ~> 1.1 2021-12-09 19:09:02 +13:00
363e623214 chore(deps): update dependency nimble_parsec to ~> 1.2 2021-12-09 18:20:54 +13:00
9f511e3a0a chore(deps): update dependency git_ops to ~> 2.4 2021-12-09 17:29:44 +13:00
70a0bd179b chore(deps): update dependency ex_doc to >= 0.26.0 2021-12-09 16:37:37 +13:00
7e2eb9e9bb chore(deps): update dependency earmark to v1 2021-12-09 15:25:55 +13:00
43e486c911 chore(deps): update dependency earmark to >= 0.2.1 2021-12-09 13:47:52 +13:00
636b683ccc chore(credo): fix linting issues. 2021-12-09 13:38:40 +13:00
d4c8001bf5 chore(deps): update dependency credo to ~> 1.6 2021-12-09 12:03:39 +13:00
73abf75886 chore: update renovate.json 2021-12-09 11:13:34 +13:00
ef4fb943f1 chore: release version v0.4.1 2021-01-13 00:29:59 +00:00
0ae33a44ff fix: bug in parsing not returning errors 2021-01-13 13:26:50 +13:00
6edee4b073 refactor: Ensure that we're using options and results as much as possible. 2021-01-13 13:25:18 +13:00
6146f60574 ci: fix asset upload token. 2021-01-13 13:17:01 +13:00
e08c2d4c55 chore: Turn on hex releases. 2021-01-10 18:44:04 +13:00
f52d85be3e chore: release version v0.4.0 2021-01-10 04:48:51 +00:00
ad8f6c44ef feat: Implement (a common subset) G-code parsing.
It turns out that G-code is a turing complete programming language. That was a surprise! I've implemented enough of the parser to be able to parse the output of Fusion 360 and Cura, both of which I use on a daily basis.  It is not a complete implementation.
2021-01-10 17:43:25 +13:00
105 changed files with 10435 additions and 490 deletions

351
.drone.yml Normal file
View 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

View file

@ -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
View file

@ -0,0 +1,2 @@
erlang 26.0.2
elixir 1.15.4

View file

@ -5,6 +5,31 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog --> <!-- 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) ## [v0.3.0](https://gitlab.com/jimsy/gcode/compare/v0.2.1...v0.3.0) (2021-01-05)

16
LICENSE
View file

@ -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
View 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 Licensees 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 Licensees 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 persons 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 persons 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 Networks 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 Licensees 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 Licensees website and/or, to the extent Licensee is a representative, agent, affiliate, successor, attorney, subsidiary, or assign, on Licensees principals or parents 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 Softwares 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 LICENSEES 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, Licensees 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 Softwares 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 Licensors and Licensees respective heirs, successors, and assigns.

View file

@ -1,21 +1,42 @@
# G-code # 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. `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 ## 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 repository is mirrored [on Github](https://github.com/jimsynz/gcode)
from it's primary location [on my Forgejo instance](https://harton.dev/james/gcode).
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/). Feel free to raise issues and open PRs on Github.
## License ## 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.

View file

@ -3,7 +3,7 @@ import Config
config :git_ops, config :git_ops,
mix_project: Mix.Project.get!(), mix_project: Mix.Project.get!(),
changelog_file: "CHANGELOG.md", changelog_file: "CHANGELOG.md",
repository_url: "https://gitlab.com/jimsy/gcode", repository_url: "https://harton.dev/james/gcode",
manage_mix_version?: true, manage_mix_version?: true,
manage_readme_version: "README.md", manage_readme_version: "README.md",
version_tag_prefix: "v" version_tag_prefix: "v"

View file

@ -1,8 +1,20 @@
defmodule Gcode do defmodule Gcode do
alias Gcode.{Model.Program, Model.Serialise, Result} alias Gcode.{Model.Program, Model.Serialise}
use Gcode.Result
@moduledoc """ @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 """ @doc """
@ -12,9 +24,7 @@ defmodule Gcode do
def serialise(%Program{} = program) do def serialise(%Program{} = program) do
program program
|> Serialise.serialise() |> Serialise.serialise()
|> Result.Enum.map(fn block -> |> Result.Enum.map(&ok("#{&1}\r\n"))
{:ok, "#{block}\r\n"}
end)
|> Result.Enum.join("") |> Result.Enum.join("")
end end
end end

View file

@ -2,16 +2,21 @@ defmodule Gcode.Model.Block do
use Gcode.Option use Gcode.Option
use Gcode.Result use Gcode.Result
defstruct words: [], comment: none() 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 """ @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)} @type t :: %Block{words: [block_contents], comment: Option.t(Comment)}
@typedoc "Any error results in this module will return this type" @typedoc "Any error results in this module will return this type"
@type block_error :: {:block_error, String.t()} @type block_error :: {:block_error, String.t()}
@type block_contents :: Word.t() | Skip.t() @type block_contents :: Word.t() | Skip.t() | Expr.t()
@doc """ @doc """
Initialise a new empty G-code program. Initialise a new empty G-code program.
@ -37,20 +42,10 @@ defmodule Gcode.Model.Block do
...> {:ok, block} = Block.comment(block, comment) ...> {:ok, block} = Block.comment(block, comment)
...> Result.ok?(block.comment) ...> Result.ok?(block.comment)
true 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) @spec comment(t, Comment.t()) :: Result.t(t, block_error)
def comment(%Block{comment: none()} = block, %Comment{} = comment), def comment(%Block{} = block, comment),
do: {:ok, %Block{block | comment: some(comment)}} do: ok(%Block{block | comment: some(comment)})
def comment(%Block{comment: some(_)}, _comment),
do: {:error, {:block_error, "Block already contains a comment"}}
@doc """ @doc """
Pushes a `Word` onto the word list. Pushes a `Word` onto the word list.
@ -66,19 +61,26 @@ defmodule Gcode.Model.Block do
...> {:ok, block} = Block.push(block, word) ...> {:ok, block} = Block.push(block, word)
...> {:ok, word} = Word.init("N", 100) ...> {:ok, word} = Word.init("N", 100)
...> Block.push(block, word) ...> 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) @spec push(t, block_contents) :: Result.t(t, block_error)
def push(%Block{words: words} = block, word) def push(%Block{words: words} = block, pushable)
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words), when is_pushable(pushable) and is_list(words),
do: {:ok, %Block{block | words: [word | words]}} do: ok(%Block{block | words: [pushable | words]})
def push(%Block{words: words}, word) def push(%Block{words: words}, pushable) when is_pushable(pushable),
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
do: do:
{:error, error(
{:block_error, {:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}} "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 """ @doc """
An accessor which returns the block's words in the correct order. 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, word} = Word.init("N", 100)
...> {:ok, block} = Block.push(block, word) ...> {:ok, block} = Block.push(block, word)
...> Block.words(block) ...> 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) @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}), def words(%Block{words: words}),
do: do:
{:error, error(
{:block_error, {:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}} "Expected block to contain a list of words, but it contains #{inspect(words)}"}
)
end end

View file

@ -3,6 +3,12 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
use Gcode.Option use Gcode.Option
use Gcode.Result 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()) @spec describe(Block.t(), options :: []) :: Option.t(String.t())
def describe(%Block{words: words, comment: some(comment)}, options) do def describe(%Block{words: words, comment: some(comment)}, options) do
words = describe_words(words, options) words = describe_words(words, options)
@ -17,9 +23,9 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
defp describe_words(words, options) do defp describe_words(words, options) do
words words
|> Enum.reverse() |> Enum.reverse()
|> Enum.map(&describe_or_serialise(&1, options)) |> Stream.map(&describe_or_serialise(&1, options))
|> Enum.reject(&Option.none?/1) |> Stream.reject(&Option.none?/1)
|> Enum.map(&Option.unwrap!/1) |> Stream.map(&Option.unwrap!/1)
|> Enum.join(", ") |> Enum.join(", ")
end end

View file

@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Block do
use Gcode.Option use Gcode.Option
use Gcode.Result 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}) @spec serialise(Block.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Block{words: words, comment: some(comment)}) do def serialise(%Block{words: words, comment: some(comment)}) do
words words

View file

@ -2,6 +2,11 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Comment do
alias Gcode.Model.Comment alias Gcode.Model.Comment
use Gcode.Option 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()) @spec describe(Comment.t(), options :: []) :: Option.t(String.t())
def describe(_comment, _opts \\ []), do: none() def describe(_comment, _opts \\ []), do: none()
end end

View file

@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
use Gcode.Option use Gcode.Option
use Gcode.Result 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}) @spec serialise(Comment.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Comment{comment: comment}) when is_binary(comment) do def serialise(%Comment{comment: comment}) when is_binary(comment) do
comment comment
@ -12,5 +17,5 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
|> ok() |> ok()
end end
def serialise(_comment), do: {:error, {:serialise_error, "Invalid comment"}} def serialise(_comment), do: error({:serialise_error, "Invalid comment"})
end end

15
lib/gcode/model/expr.ex Normal file
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.Program do defmodule Gcode.Model.Program do
defstruct elements: [] defstruct elements: []
alias Gcode.Model.{Block, Comment, Program, Skip, Tape} alias Gcode.Model.{Block, Comment, Program, Tape}
use Gcode.Result use Gcode.Result
@moduledoc """ @moduledoc """
@ -20,7 +20,7 @@ defmodule Gcode.Model.Program do
elements: [element] 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()} @type error :: {:program_error, String.t()}
@doc """ @doc """
@ -43,8 +43,7 @@ defmodule Gcode.Model.Program do
@spec push(t, element) :: Result.t(t, error) @spec push(t, element) :: Result.t(t, error)
def push(%Program{elements: elements} = program, element) def push(%Program{elements: elements} = program, element)
when is_list(elements) and when is_list(elements) and
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Skip) or (is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Tape)),
is_struct(element, Tape)),
do: ok(%Program{program | elements: [element | elements]}) do: ok(%Program{program | elements: [element | elements]})
def push(%Program{elements: elements}, _element) when not is_list(elements), def push(%Program{elements: elements}, _element) when not is_list(elements),

View file

@ -2,14 +2,18 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Program do
alias Gcode.Model.{Describe, Program} alias Gcode.Model.{Describe, Program}
use Gcode.Option use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Program`.
"""
@spec describe(Program.t(), options :: []) :: Option.t(String.t()) @spec describe(Program.t(), options :: []) :: Option.t(String.t())
def describe(%Program{elements: elements}, options) do def describe(%Program{elements: elements}, options) do
lines = lines =
elements elements
|> Enum.reverse() |> Enum.reverse()
|> Enum.map(&Describe.describe(&1, options)) |> Stream.map(&Describe.describe(&1, options))
|> Enum.reject(&(&1 == none())) |> Stream.reject(&(&1 == none()))
|> Enum.map(fn some(line) -> "#{line}\n" end) |> Stream.map(fn some(line) -> "#{line}\n" end)
|> Enum.join("") |> Enum.join("")
some(lines) some(lines)

View file

@ -1,17 +1,24 @@
defimpl Enumerable, for: Gcode.Model.Program do defimpl Enumerable, for: Gcode.Model.Program do
alias Gcode.Model.Program alias Gcode.Model.Program
@moduledoc false use Gcode.Result
@spec count(Program.t()) :: {:ok, non_neg_integer()} | {:error, module} @moduledoc """
def count(%Program{elements: elements}), do: {:ok, Enum.count(elements)} Implements the `Enumerable` protocol for `Program`.
"""
@spec member?(Program.t(), any) :: {:error, module} | {:ok, boolean} @spec count(Program.t()) :: Result.t(non_neg_integer, module)
def member?(%Program{elements: elements}, element), do: Enumerable.member?(elements, element) 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() @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()) :: @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) def slice(%Program{elements: elements}), do: Enumerable.slice(elements)
end end

View file

@ -2,6 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Program do
alias Gcode.{Model.Program, Model.Serialise} alias Gcode.{Model.Program, Model.Serialise}
use Gcode.Result 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}) @spec serialise(Program.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Program{elements: elements}) do def serialise(%Program{elements: elements}) do
with elements <- Enum.reverse(elements), with elements <- Enum.reverse(elements),

View file

@ -3,9 +3,11 @@ defprotocol Gcode.Model.Serialise do
alias Gcode.Result alias Gcode.Result
@moduledoc """ @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()]) @spec serialise(Serialise.t()) :: Result.t([String.t()])
def serialise(serialisable) def serialise(serialisable)
end end

View file

@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Skip do
alias Gcode.Model.Skip alias Gcode.Model.Skip
use Gcode.Option use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Skip`.
"""
@spec describe(Skip.t(), options :: []) :: Option.t(String.t()) @spec describe(Skip.t(), options :: []) :: Option.t(String.t())
def describe(_skip, _opts \\ []), do: none() def describe(_skip, _opts \\ []), do: none()
end end

View file

@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Skip do
alias Gcode.{Model.Skip, Result} alias Gcode.{Model.Skip, Result}
use Gcode.Option use Gcode.Option
use Gcode.Result 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}) @spec serialise(Skip.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Skip{number: none()}), do: ok(["/"]) def serialise(%Skip{number: none()}), do: ok(["/"])

View file

@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Tape do
alias Gcode.Model.Tape alias Gcode.Model.Tape
use Gcode.Option use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Tape`.
"""
@spec describe(Tape.t(), options :: []) :: Option.t(String.t()) @spec describe(Tape.t(), options :: []) :: Option.t(String.t())
def describe(_tape, _opts \\ []), do: none() def describe(_tape, _opts \\ []), do: none()
end end

View file

@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Tape do
alias Gcode.{Model.Tape, Result} alias Gcode.{Model.Tape, Result}
use Gcode.Option use Gcode.Option
use Gcode.Result 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}) @spec serialise(Tape.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Tape{leader: none()}), do: ok(["%"]) def serialise(%Tape{leader: none()}), do: ok(["%"])

View file

@ -1,5 +1,6 @@
defmodule Gcode.Model.Word do defmodule Gcode.Model.Word do
alias Gcode.Model.Word alias Gcode.Model.{Expr, Word}
import Gcode.Model.Expr.Helpers
use Gcode.Option use Gcode.Option
use Gcode.Result use Gcode.Result
@ -11,7 +12,7 @@ defmodule Gcode.Model.Word do
@type t :: %Word{ @type t :: %Word{
word: String.t(), word: String.t(),
address: number address: Expr.t()
} }
@doc """ @doc """
@ -20,10 +21,10 @@ defmodule Gcode.Model.Word do
## Example ## Example
iex> Word.init("G", 0) 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) @spec init(String.t(), number | Expr.t()) :: Result.t(t)
def init(word, address) when is_binary(word) and is_number(address) do def init(word, address) when is_binary(word) and is_expression(address) do
if Regex.match?(~r/^[A-Z]$/, word) do if Regex.match?(~r/^[A-Z]$/, word) do
ok(%Word{word: word, address: address}) ok(%Word{word: word, address: address})
else else
@ -31,9 +32,31 @@ defmodule Gcode.Model.Word do
end end
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)}"}) do: error({:word_error, "Expected word to be a string, received #{inspect(word)}"})
def init(_word, address), 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 end

View file

@ -1,7 +1,10 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Word do defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
import Gcode.Model.Expr.Helpers
use Gcode.Option use Gcode.Option
use Gcode.Result use Gcode.Result
alias Gcode.Model.Word alias Gcode.Model.{Expr, Word}
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
@moduledoc """ @moduledoc """
Describes common/conventional words. Describes common/conventional words.
@ -49,283 +52,560 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
do: do_describe(word, Enum.into(options, %{})) do: do_describe(word, Enum.into(options, %{}))
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute}) defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
when axis in ~w[A B C] and angle >= 0, when axis in ~w[A B C] do
do: some("Rotate #{axis} axis counterclockwise to #{angle}º") 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}) defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
when axis in ~w[A B C] and angle >= 0, when axis in ~w[A B C] do
do: some("Rotate #{axis} axis counterclockwise by #{angle}º") 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}, _) defp do_describe(%Word{word: axis, address: angle}, _)
when axis in ~w[A B C] and angle >= 0, when axis in ~w[A B C] do
do: some("Rotate #{axis} axis counterclockwise by/to #{angle}º") case Expr.evaluate(angle) do
ok(angle) when is_number(angle) and angle >= 0 ->
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute}) some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
when axis in ~w[A B C] and angle < 0,
do: some("Rotate #{axis} axis clockwise to #{abs(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}) defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options) do
when axis in ~w[A B C] and angle < 0, case feedrate(feedrate, options) do
do: some("Rotate #{axis} axis clockwise by #{abs(angle)}º") some(feedrate) -> some("Extruder feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: axis, address: angle}, _) defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options) do
when axis in ~w[A B C] and angle < 0, case feedrate(feedrate, options) do
do: some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º") some(feedrate) -> some("Precision feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options), defp do_describe(%Word{word: "F", address: feedrate}, options) do
do: some("Depth of cut #{distance_with_unit(depth, options)}") case feedrate(feedrate, options) do
some(feedrate) -> some("Feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}), defp do_describe(%Word{word: "G", address: address}, options) do
do: some("Aperture #{aperture}") case {Expr.evaluate(address), options} do
{ok(0), _} ->
some("Rapid move")
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options), {ok(1), _} ->
do: some("Left radial offset #{distance_with_unit(offset, options)}") some("Linear move")
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options), {ok(2), _} ->
do: some("Right radial offset #{distance_with_unit(offset, options)}") some("Clockwise circular move")
defp do_describe(%Word{word: "D", address: offset}, options), {ok(3), _} ->
do: some("Radial offset #{distance_with_unit(offset, options)}") some("Counterclockwise circular move")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options), {ok(4), _} ->
do: some("Extruder feedrate #{feedrate(feedrate, options)}") some("Dwell")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options), {ok(5), _} ->
do: some("Precision feedrate #{feedrate(feedrate, options)}") some("High-precision contour control")
defp do_describe(%Word{word: "F", address: feedrate}, options), {ok(5.1), _} ->
do: some("Feedrate #{feedrate(feedrate, options)}") some("AI advanced preview control")
defp do_describe(%Word{word: "G", address: 0}, _), do: some("Rapid move") {ok(6.1), _} ->
defp do_describe(%Word{word: "G", address: 1}, _), do: some("Linear move") some("NURBS machining")
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")
defp do_describe(%Word{word: "G", address: 30}, _), {ok(7), _} ->
do: some("Return to secondary home position") some("Imaginary axis designation")
defp do_describe(%Word{word: "G", address: 31}, _), do: some("Feed until skip function") {ok(9), _} ->
defp do_describe(%Word{word: "G", address: 32}, _), do: some("Single-point threading") some("Exact stop check - non-modal")
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")
defp do_describe(%Word{word: "G", address: 43}, _), {ok(10), _} ->
do: some("Tool height offset compensation negative") some("Programmable data input")
defp do_describe(%Word{word: "G", address: 44}, _), {ok(11), _} ->
do: some("Tool height offset compensation positive") some("Data write cancel")
defp do_describe(%Word{word: "G", address: 45}, _), do: some("Axis offset single increase") {ok(17), _} ->
defp do_describe(%Word{word: "G", address: 46}, _), do: some("Axis offset single decrease") some("XY plane selection")
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")
defp do_describe(%Word{word: "G", address: 49}, _), {ok(18), _} ->
do: some("Tool length offset compensation cancel") some("ZX plane selection")
defp do_describe(%Word{word: "G", address: 50}, %{operation: :turning}), {ok(19), _} ->
do: some("Position register") some("YZ plane selection")
defp do_describe(%Word{word: "G", address: 50}, _), do: some("Scaling function cancel") {ok(20), _} ->
defp do_describe(%Word{word: "G", address: 52}, _), do: some("Local coordinate system") some("Unit is inches")
defp do_describe(%Word{word: "G", address: 53}, _), do: some("Machine coordinate system")
defp do_describe(%Word{word: "G", address: address}, _) {ok(21), _} ->
when address in [54, 55, 56, 57, 58, 59, 54.1], some("Unit is mm")
do: some("Work coordinate system")
defp do_describe(%Word{word: "G", address: 61}, _), do: some("Exact stop check - modal") {ok(28), _} ->
defp do_describe(%Word{word: "G", address: 62}, _), do: some("Automatic corner override") some("Return to home position")
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")
defp do_describe(%Word{word: "G", address: 69}, _), {ok(30), _} ->
do: some("Turn off coordinate system rotation") some("Return to secondary home position")
defp do_describe(%Word{word: "G", address: 70}, %{operation: :turning}), {ok(31), _} ->
do: some("Fixed cycle, multiple repetitive cycle - for finishing") some("Feed until skip function")
defp do_describe(%Word{word: "G", address: 71}, %{operation: :turning}), {ok(32), _} ->
do: some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis") some("Single-point threading")
defp do_describe(%Word{word: "G", address: 72}, %{operation: :turning}), {ok(33), _} ->
do: some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis") some("Variable pitch threading")
defp do_describe(%Word{word: "G", address: 73}, %{operation: :turning}), {ok(40), _} ->
do: some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition") 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}), {ok(42), _} ->
do: some("Peck drilling cycle") 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}), {ok(44), _} ->
do: some("Peck grooving cycle") some("Tool height offset compensation positive")
defp do_describe(%Word{word: "G", address: 76}, %{operation: :turning}), {ok(45), _} ->
do: some("Threading cycle") some("Axis offset single increase")
defp do_describe(%Word{word: "G", address: 76}, _), do: some("Fine boring cycle") {ok(46), _} ->
defp do_describe(%Word{word: "G", address: 80}, _), do: some("Cancel cycle") some("Axis offset single decrease")
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")
defp do_describe(%Word{word: "G", address: 84}, _), {ok(47), _} ->
do: some("Tapping cycle, righthand thread, M03 spindle direction") some("Axis offset double increase")
defp do_describe(%Word{word: "G", address: 84.2}, _), {ok(48), _} ->
do: some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder") some("Axis offset double decrease")
defp do_describe(%Word{word: "G", address: 84.3}, _), {ok(49), _} ->
do: some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder") 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}, _), {ok(50), _} ->
do: some("Boring cycle, feed in/spindle stop/rapid out") 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}, _), {ok(53), _} ->
do: some("Boring cycle, feed in/spindle stop/manual operation") some("Machine coordinate system")
defp do_describe(%Word{word: "G", address: 89}, _), {ok(address), _} when address in [54, 55, 56, 57, 58, 59, 54.1] ->
do: some("Boring cycle, feed in/dwell/feed out") some("Work coordinate system")
defp do_describe(%Word{word: "G", address: 90}, _), do: some("Absolute positioning") {ok(61), _} ->
defp do_describe(%Word{word: "G", address: 91}, _), do: some("Relative positioning") some("Exact stop check - modal")
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")
defp do_describe(%Word{word: "G", address: 98}, %{operation: :turning}), {ok(62), _} ->
do: some("Feedrate per minute") some("Automatic corner override")
defp do_describe(%Word{word: "G", address: 98}, _), {ok(64), _} ->
do: some("Return to initial Z level in canned cycle") some("Default cutting mode")
defp do_describe(%Word{word: "G", address: 99}, %{operation: :turning}), {ok(68), _} ->
do: some("Feedrate per revolution") some("Rotate coordinate system")
defp do_describe(%Word{word: "G", address: 99}, _), {ok(69), _} ->
do: some("Return to R level in canned cycle") 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), {ok(71), %{operation: :turning}} ->
do: some("Tool length offset #{distance_with_unit(length, options)}") some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
defp do_describe(%Word{word: "I", address: offset}, options), {ok(72), %{operation: :turning}} ->
do: some("X arc center offset #{distance_with_unit(offset, options)}") some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
defp do_describe(%Word{word: "J", address: offset}, options), {ok(73), %{operation: :turning}} ->
do: some("Y arc center offset #{distance_with_unit(offset, options)}") some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
defp do_describe(%Word{word: "K", address: offset}, options), {ok(73), _} ->
do: some("Z arc center offset #{distance_with_unit(offset, options)}") 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") {ok(74), _} ->
defp do_describe(%Word{word: "M", address: 1}, _), do: some("Optional stop") some("Tapping cycle")
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")
defp do_describe(%Word{word: "M", address: 13}, _), {ok(75), %{operation: :turning}} ->
do: some("Spindle on clockwise and coolant flood") 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}), {ok(76), _} ->
do: some("Tailstock forward") 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}), {ok(81), _} ->
do: some("Tailstock backward") 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}), {ok(83), _} ->
do: some("Thread gradual pullout on") 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}), {ok(84.2), _} ->
do: some("Thread gradual pullout off") 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}) {ok(85), _} ->
when is_integer(gear) and gear > 40 and gear < 45, some("Boring cycle, feed in/feed out")
do: some("Gear select #{gear - 40}")
defp do_describe(%Word{word: "M", address: 48}, _), do: some("Feedrate override allowed") {ok(86), _} ->
defp do_describe(%Word{word: "M", address: 49}, _), do: some("Feedrate override not allowed") some("Boring cycle, feed in/spindle stop/rapid out")
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}")
defp do_describe(%Word{word: "Q", address: distance}, options), {ok(87), _} ->
do: some("Peck increment #{distance_with_unit(distance, options)}") some("Boring cycle, backboring")
defp do_describe(%Word{word: "R", address: distance}, options), {ok(88), _} ->
do: some("Radius #{distance_with_unit(distance, options)}") some("Boring cycle, feed in/spindle stop/manual operation")
defp do_describe(%Word{word: "S", address: speed}, _), do: some("Speed #{speed}") {ok(89), _} ->
defp do_describe(%Word{word: "T", address: tool}, _), do: some("Tool #{tool}") some("Boring cycle, feed in/dwell/feed out")
defp do_describe(%Word{word: "X", address: position}, options), {ok(90), _} ->
do: some("X #{distance_with_unit(position, options)}") some("Absolute positioning")
defp do_describe(%Word{word: "Y", address: position}, options), {ok(91), _} ->
do: some("Y #{distance_with_unit(position, options)}") some("Relative positioning")
defp do_describe(%Word{word: "Z", address: position}, options), {ok(92), _} ->
do: some("Z #{distance_with_unit(position, options)}") 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 do_describe(%Word{}, _options), do: none()
defp distance_with_unit(distance, %{units: :mm}), do: "#{distance}mm" defp distance_with_unit(distance, %{units: :mm}) when is_number(distance),
defp distance_with_unit(distance, %{units: :inches}), do: "#{distance}\"" do: some("#{distance}mm")
defp distance_with_unit(distance, _), do: distance
defp feedrate(distance, %{operation: :turning} = options), defp distance_with_unit(distance, %{units: :inches}) when is_number(distance),
do: "#{distance_with_unit(distance, options)}/rev" 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 end

View file

@ -1,10 +1,17 @@
defimpl Inspect, for: Gcode.Model.Word do defimpl Inspect, for: Gcode.Model.Word do
alias Gcode.Model.{Describe, Word} alias Gcode.Model.{Describe, Expr, Word}
use Gcode.Option use Gcode.Option
use Gcode.Result
import Inspect.Algebra import Inspect.Algebra
@moduledoc false @moduledoc false
def inspect(%Word{word: letter, address: address} = word, opts) do 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 case Describe.describe(word) do
some(description) -> some(description) ->
concat([ concat([

View file

@ -1,15 +1,18 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do
alias Gcode.Model.Word alias Gcode.Model.{Word, Serialise}
use Gcode.Option use Gcode.Option
use Gcode.Result use Gcode.Result
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any}) @moduledoc """
def serialise(%Word{word: word, address: address}) when is_integer(address) do Implements the `Serialise` protocol for `Word`, allowing it to be turned into
ok(["#{word}#{address}"]) G-code output.
end """
def serialise(%Word{word: word, address: address}) when is_float(address) do @spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
ok(["#{word}#{address}"]) 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 end
def serialise(_word), do: error({:serialise_error, "invalid word"}) def serialise(_word), do: error({:serialise_error, "invalid word"})

View file

@ -3,6 +3,7 @@ defmodule Gcode.Option do
A helper which represents an optional type. A helper which represents an optional type.
""" """
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do defmacro __using__(_) do
quote do quote do
alias Gcode.Option alias Gcode.Option
@ -16,14 +17,17 @@ defmodule Gcode.Option do
@type some(t) :: {:ok, t} @type some(t) :: {:ok, t}
@type opt_none :: :error @type opt_none :: :error
@doc "Is the value a none?"
@spec none?(t(any)) :: boolean @spec none?(t(any)) :: boolean
def none?(:error), do: true def none?(:error), do: true
def none?({:ok, _}), do: false def none?({:ok, _}), do: false
@doc "Is the value a some?"
@spec some?(t(any)) :: boolean @spec some?(t(any)) :: boolean
def some?(:error), do: false def some?(:error), do: false
def some?({:ok, _}), do: true def some?({:ok, _}), do: true
@doc "Create or match a none"
@spec none :: Macro.t() @spec none :: Macro.t()
defmacro none do defmacro none do
quote do quote do
@ -31,6 +35,7 @@ defmodule Gcode.Option do
end end
end end
@doc "Create or match a some"
@spec some(any) :: Macro.t() @spec some(any) :: Macro.t()
defmacro some(pattern) do defmacro some(pattern) do
quote do quote do
@ -38,6 +43,7 @@ defmodule Gcode.Option do
end end
end end
@doc "Attempt to unwrap an option. Raises an error if the option is a none"
@spec unwrap!(t) :: any | no_return @spec unwrap!(t) :: any | no_return
def unwrap!({:ok, result}), do: result def unwrap!({:ok, result}), do: result
def unwrap!(:error), do: raise("Attempt to unwrap a none") def unwrap!(:error), do: raise("Attempt to unwrap a none")

215
lib/gcode/parser.ex Normal file
View 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
View 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

View 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

View file

@ -1,6 +1,8 @@
defmodule Gcode.Result do defmodule Gcode.Result do
@moduledoc """ @moduledoc """
A helper which represents a result type. A helper which represents a result type.
This is really just a wrapper around Erlang's ok/error tuples.
""" """
@type t :: t(any, any) @type t :: t(any, any)
@ -9,6 +11,7 @@ defmodule Gcode.Result do
@type ok(result) :: {:ok, result} @type ok(result) :: {:ok, result}
@type error(error) :: {:error, error} @type error(error) :: {:error, error}
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do defmacro __using__(_) do
quote do quote do
alias Gcode.Result alias Gcode.Result
@ -17,6 +20,7 @@ defmodule Gcode.Result do
end end
end end
@doc "Initialise or match an ok value"
@spec ok(any) :: Macro.t() @spec ok(any) :: Macro.t()
defmacro ok(result) do defmacro ok(result) do
quote do quote do
@ -24,6 +28,7 @@ defmodule Gcode.Result do
end end
end end
@doc "Initialise or match an error value"
@spec error(any) :: Macro.t() @spec error(any) :: Macro.t()
defmacro error(error) do defmacro error(error) do
quote do quote do
@ -31,18 +36,22 @@ defmodule Gcode.Result do
end end
end end
@doc "Is the result ok?"
@spec ok?(t) :: boolean @spec ok?(t) :: boolean
def ok?({:ok, _}), do: true def ok?({:ok, _}), do: true
def ok?({:error, _}), do: false def ok?({:error, _}), do: false
@doc "Is the result an error?"
@spec error?(t) :: boolean @spec error?(t) :: boolean
def error?({:ok, _}), do: false def error?({:ok, _}), do: false
def error?({:error, _}), do: true 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 @spec unwrap!(t) :: any | no_return
def unwrap!({:ok, result}), do: result def unwrap!({:ok, result}), do: result
def unwrap!({:error, error}), do: raise(error) def unwrap!({:error, error}), do: raise(error)
@doc "Convert a successful result another result."
@spec map(t, (any -> t)) :: t @spec map(t, (any -> t)) :: t
def map({:ok, value}, mapper) when is_function(mapper, 1) do def map({:ok, value}, mapper) when is_function(mapper, 1) do
case mapper.(value) do case mapper.(value) do

View file

@ -1,7 +1,10 @@
defmodule Gcode.Result.Enum do defmodule Gcode.Result.Enum do
@moduledoc false
use Gcode.Result use Gcode.Result
@moduledoc """
Common enumerableish functions on results.
"""
@type result :: Result.t() @type result :: Result.t()
@type result(result) :: Result.t(result) @type result(result) :: Result.t(result)
@type result(result, error) :: Result.t(result, error) @type result(result, error) :: Result.t(result, error)
@ -13,32 +16,44 @@ defmodule Gcode.Result.Enum do
reducer :: (any, any -> result(any)) reducer :: (any, any -> result(any))
) :: result(any) ) :: result(any)
def reduce_while_ok(elements, acc, reducer) when is_function(reducer, 2) do 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 case reducer.(element, acc) do
{:ok, acc} -> {:cont, {:ok, acc}} ok(acc) -> {:cont, ok(acc)}
{:error, reason} -> {:halt, {:error, reason}} error(reason) -> {:halt, error(reason)}
end end
end) 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]) @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 -> reduce_while_ok(enumerable, [], fn element, acc ->
case mapper.(element) do case mapper.(element) do
{:ok, mapped} -> {:ok, [mapped | acc]} ok(mapped) -> ok([mapped | acc])
{:error, reason} -> {:error, reason} error(reason) -> error(reason)
end end
end) end)
|> reverse() |> reverse()
end 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]) @spec reverse(result([any])) :: result([any])
def reverse({:ok, enumerable}), do: {:ok, Enum.reverse(enumerable)} def reverse(ok(enumerable)), do: ok(Enum.reverse(enumerable))
def reverse({:error, reason}), do: {:error, reason} 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()) @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(ok(strings), joiner) when is_binary(joiner), do: ok(Enum.join(strings, joiner))
def join({:error, reason}, _joiner), do: {:error, reason} def join(error(reason), _joiner), do: error(reason)
end end

28
mix.exs
View file

@ -2,7 +2,7 @@ defmodule Gcode.MixProject do
use Mix.Project use Mix.Project
@moduledoc false @moduledoc false
@version "0.3.0" @version "1.0.0"
@description """ @description """
A G-code parser and generator. A G-code parser and generator.
""" """
@ -17,7 +17,11 @@ defmodule Gcode.MixProject do
description: @description, description: @description,
deps: deps(), deps: deps(),
consolidate_protocols: Mix.env() != :test, 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 end
@ -31,9 +35,12 @@ defmodule Gcode.MixProject do
def package do def package do
[ [
maintainers: ["James Harton <james@harton.nz>"], maintainers: ["James Harton <james@harton.nz>"],
licenses: ["Hippocratic"], licenses: ["HL3-FULL"],
links: %{ 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 end
@ -41,10 +48,15 @@ defmodule Gcode.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:ex_doc, ">= 0.0.0", only: ~w[dev test]a, runtime: false}, {:nimble_parsec, "~> 1.2"},
{:earmark, ">= 0.0.0", only: ~w[dev test]a, runtime: false}, {:parallel_stream, "~> 1.1"},
{:credo, "~> 1.1", only: ~w[dev test]a, runtime: false},
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false} # 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 end

View file

@ -1,14 +1,17 @@
%{ %{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "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"}, "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.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"}, "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.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"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"}, "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"}, "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_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"}, "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": {: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.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"}, "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"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "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"},
} }

File diff suppressed because it is too large Load diff

View 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

View file

@ -1,7 +0,0 @@
{
"extends": [
"config:base",
":semanticCommits",
":semanticCommitTypeAll(chore)"
]
}

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.Block.SerialiseTest do defmodule Gcode.Model.Block.SerialiseTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Gcode.Model.{Block, Serialise, Word} alias Gcode.Model.{Block, Expr, Serialise, Word}
use Gcode.Result use Gcode.Result
@moduledoc false @moduledoc false
@ -8,11 +8,12 @@ defmodule Gcode.Model.Block.SerialiseTest do
assert ok(block) = assert ok(block) =
with( with(
ok(block) <- Block.init(), 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(block) <- Block.push(block, word),
ok(word) <- Word.init("N", 100), ok(address) <- Expr.Integer.init(100),
ok(block) <- Block.push(block, word), ok(word) <- Word.init("N", address),
do: ok(block) do: Block.push(block, word)
) )
assert ok([actual]) = Serialise.serialise(block) assert ok([actual]) = Serialise.serialise(block)

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.BlockTest do defmodule Gcode.Model.BlockTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Gcode.Model.{Block, Comment, Word} alias Gcode.Model.{Block, Comment, Expr.Integer, Word}
use Gcode.Option use Gcode.Option
use Gcode.Result use Gcode.Result
doctest Gcode.Model.Block doctest Gcode.Model.Block

View file

@ -1,6 +1,7 @@
defmodule Gcode.Model.Comment.SerialiseTest do defmodule Gcode.Model.Comment.SerialiseTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Gcode.Model.{Comment, Serialise} alias Gcode.Model.{Comment, Serialise}
use Gcode.Result
@moduledoc false @moduledoc false
describe "serialise/1" do describe "serialise/1" do
@ -13,8 +14,8 @@ defmodule Gcode.Model.Comment.SerialiseTest do
""" """
actual = actual =
with {:ok, comment} <- Comment.init(comment), with ok(comment) <- Comment.init(comment),
{:ok, comment} <- Serialise.serialise(comment), ok(comment) <- Serialise.serialise(comment),
do: comment do: comment
expected = ~w[(This) (is) (a) (test)] expected = ~w[(This) (is) (a) (test)]

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -1,12 +1,15 @@
defmodule Gcode.Model.Skip.SerialiseTest do defmodule Gcode.Model.Skip.SerialiseTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Gcode.Model.{Serialise, Skip} alias Gcode.Model.{Serialise, Skip}
use Gcode.Result
@moduledoc false @moduledoc false
describe "serialise/1" do describe "serialise/1" do
test "when the skip has a number, it formats it correctly" do test "when the skip has a number, it formats it correctly" do
actual = 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] expected = ~w[/0]
@ -14,7 +17,10 @@ defmodule Gcode.Model.Skip.SerialiseTest do
end end
test "when the skip has no number, it formats it correctly" do 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[/] expected = ~w[/]

View file

@ -1,12 +1,15 @@
defmodule Gcode.Model.Word.SerialiseTest do defmodule Gcode.Model.Word.SerialiseTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Gcode.Model.{Serialise, Word} alias Gcode.Model.{Serialise, Word}
use Gcode.Result
@moduledoc false @moduledoc false
describe "serialise/1" do describe "serialise/1" do
test "formats the word and the address correctly" do test "formats the word and the address correctly" do
actual = 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] expected = ~w[G0]

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.WordTest do defmodule Gcode.Model.WordTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Gcode.Model.Word alias Gcode.Model.{Expr.Integer, Word}
doctest Gcode.Model.Word doctest Gcode.Model.Word
@moduledoc false @moduledoc false
end end

View 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