Compare commits

...

41 commits
v0.1.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
fa7eb9e961 chore: release version v0.3.0 2021-01-05 02:50:29 +00:00
dd763dc09a feat(descriptions): Add human-readable descriptions of commonly used codes. 2021-01-05 15:45:02 +13:00
875749e09b chore: move files to the correct location. 2021-01-05 09:21:03 +13:00
04fb1c18a8 chore: Update readme. 2021-01-05 09:16:52 +13:00
a566b66f2d chore: release version v0.2.1 2021-01-04 20:04:03 +00:00
5b273e4356 fix(model): Change all model init functions to return a result. 2021-01-05 08:59:22 +13:00
e45c18ea58 chore: second attempt to fix the CD pipeline.
It looks like `$CI_DEFAULT_BRANCH` is not yet available on this version of Gitlab.
2021-01-04 22:25:05 +13:00
9aede327b8 chore: attempt to fix CD pipeline.
For some reason the git_ops job didn't run on my last push to `main` and this is the only thing I can think of that may have affected it.
2021-01-04 22:22:24 +13:00
ca52bdc8cf chore: release version v0.2.0 2021-01-04 22:20:18 +13:00
207c07e0c1 Add renovate.json 2021-01-04 09:16:46 +00:00
ccf4635cca feat(model,serialise): Implement a basic G-Code model and serialiser.
It's not very thorough at the moment, but it should work for now.
2021-01-04 22:14:40 +13:00
109 changed files with 11808 additions and 195 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,141 +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:
- $CI_DEFAULT_BRANCH
git_ops:
image: elixir:latest
dependencies:
- build
stage: release
only:
refs:
- $CI_DEFAULT_BRANCH
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,58 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog -->
## [v1.0.0](https://gitlab.com/jimsy/gcode/compare/v0.4.1...v1.0.0) (2023-01-16)
### Breaking Changes:
* Relicense to HL3-FULL.
## [v0.4.1](https://gitlab.com/jimsy/gcode/compare/v0.4.0...v0.4.1) (2021-01-13)
### Bug Fixes:
* bug in parsing not returning errors
## [v0.4.0](https://gitlab.com/jimsy/gcode/compare/v0.3.0...v0.4.0) (2021-01-10)
### Features:
* Implement (a common subset) G-code parsing.
## [v0.3.0](https://gitlab.com/jimsy/gcode/compare/v0.2.1...v0.3.0) (2021-01-05)
### Features:
* descriptions: Add human-readable descriptions of commonly used codes.
## [v0.2.1](https://gitlab.com/jimsy/gcode/compare/v0.2.0...v0.2.1) (2021-01-04)
### Bug Fixes:
* model: Change all model init functions to return a result.
## [v0.2.0](https://gitlab.com/jimsy/gcode/compare/v0.1.0...v0.2.0) (2021-01-04)
### Features:
* model,serialise: Implement a basic G-Code model and serialiser.
## [v0.1.0](https://gitlab.com/jimsy/gcode/compare/v0.1.0...v0.1.0) (2021-01-02)

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 @@
# Gcode
# G-code
**TODO: Add description**
[![Build Status](https://drone.harton.dev/api/badges/james/gcode/status.svg?ref=refs/heads/main)](https://drone.harton.dev/james/gcode)
[![Hex.pm](https://img.shields.io/hexpm/v/gcode.svg)](https://hex.pm/packages/gcode)
[![Hippocratic License HL3-FULL](https://img.shields.io/static/v1?label=Hippocratic%20License&message=HL3-FULL&labelColor=5e2751&color=bc8c3d)](https://firstdonoharm.dev/version/3/0/full.html)
`gcode` is an Elixir library for parsing and generating [G-code](https://en.wikipedia.org/wiki/G-code), which is a common language for working with CNC machines and 3D printers.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `gcode` to your list of dependencies in `mix.exs`:
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`:
```elixir
def deps do
[
{:gcode, "~> 0.1.0"}
{:gcode, "~> 1.0.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/gcode](https://hexdocs.pm/gcode).
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).
## Github Mirror
This repository is mirrored [on Github](https://github.com/jimsynz/gcode)
from it's primary location [on my Forgejo instance](https://harton.dev/james/gcode).
Feel free to raise issues and open PRs on Github.
## License
This 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,
mix_project: Mix.Project.get!(),
changelog_file: "CHANGELOG.md",
repository_url: "https://gitlab.com/jimsy/gcode",
repository_url: "https://harton.dev/james/gcode",
manage_mix_version?: true,
manage_readme_version: "README.md",
version_tag_prefix: "v"

View file

@ -1,18 +1,30 @@
defmodule Gcode do
alias Gcode.{Model.Program, Model.Serialise}
use Gcode.Result
@moduledoc """
Documentation for `Gcode`.
Gcode - a library for parsing and serialising G-code.
If you haven't heard of G-code before, then you probably don't need this
library, but if you're working with CNC machines or 3D printers then G-code is
the defacto standard for working with these machines. As such it behoves us
to have first class support for working with G-code in Elixir.
You're welcome.
For functions related to parsing G-code files and commands, see the `Parser`
module. For generating your own programs see the contents of `Model`, and for
converting programs back into G-code see the `Model.Serialise` protocol.
"""
@doc """
Hello world.
## Examples
iex> Gcode.hello()
:world
Serialise a program to a String.
"""
def hello do
:world
@spec serialise(Program.t()) :: Result.t(String.t(), {:serialise_error, any})
def serialise(%Program{} = program) do
program
|> Serialise.serialise()
|> Result.Enum.map(&ok("#{&1}\r\n"))
|> Result.Enum.join("")
end
end

105
lib/gcode/model/block.ex Normal file
View file

@ -0,0 +1,105 @@
defmodule Gcode.Model.Block do
use Gcode.Option
use Gcode.Result
defstruct words: [], comment: none()
import Gcode.Model.Expr.Helpers
alias Gcode.Model.{Block, Comment, Expr, Skip, Word}
defguardp is_pushable(value)
when is_struct(value, Block) or is_struct(value, Comment) or is_struct(value, Skip) or
is_struct(value, Word) or is_expression(value)
@moduledoc """
A sequence of G-code words on a single line.
"""
@type t :: %Block{words: [block_contents], comment: Option.t(Comment)}
@typedoc "Any error results in this module will return this type"
@type block_error :: {:block_error, String.t()}
@type block_contents :: Word.t() | Skip.t() | Expr.t()
@doc """
Initialise a new empty G-code program.
## Example
iex> Block.init()
{:ok, %Block{words: [], comment: none()}}
"""
@spec init :: Result.t(t)
def init, do: ok(%Block{words: [], comment: none()})
@doc """
Set a comment on the block (this is just a sugar to make sure that the comment
is rendered on the same line as the block).
*Note:* Once a block has a comment set, it cannot be overwritten.
## Examples
iex> {:ok, comment} = Comment.init("Jen, in the swing seat, with her night terrors")
...> {:ok, block} = Block.init()
...> {:ok, block} = Block.comment(block, comment)
...> Result.ok?(block.comment)
true
"""
@spec comment(t, Comment.t()) :: Result.t(t, block_error)
def comment(%Block{} = block, comment),
do: ok(%Block{block | comment: some(comment)})
@doc """
Pushes a `Word` onto the word list.
*Note:* `Block` stores the words in reverse order because of Erlang list
semantics, you should pretty much always use `words/1` to retrieve them in the
correct order.
## Example
iex> {:ok, block} = Block.init()
...> {:ok, word} = Word.init("G", 0)
...> {:ok, block} = Block.push(block, word)
...> {:ok, word} = Word.init("N", 100)
...> Block.push(block, word)
{:ok, %Block{words: [%Word{word: "N", address: %Integer{i: 100}}, %Word{word: "G", address: %Integer{i: 0}}]}}
"""
@spec push(t, block_contents) :: Result.t(t, block_error)
def push(%Block{words: words} = block, pushable)
when is_pushable(pushable) and is_list(words),
do: ok(%Block{block | words: [pushable | words]})
def push(%Block{words: words}, pushable) when is_pushable(pushable),
do:
error(
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
)
def push(%Block{words: words}, pushable) when is_list(words),
do:
error(
{:block_error,
"Expected element to be pushable, but it is not. Received #{inspect(pushable)}"}
)
@doc """
An accessor which returns the block's words in the correct order.
iex> {:ok, block} = Block.init()
...> {:ok, word} = Word.init("G", 0)
...> {:ok, block} = Block.push(block, word)
...> {:ok, word} = Word.init("N", 100)
...> {:ok, block} = Block.push(block, word)
...> Block.words(block)
{:ok, [%Word{word: "G", address: %Integer{i: 0}}, %Word{word: "N", address: %Integer{i: 100}}]}
"""
@spec words(t) :: Result.t([Word.t()], block_error)
def words(%Block{words: words}) when is_list(words), do: ok(Enum.reverse(words))
def words(%Block{words: words}),
do:
error(
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
)
end

View file

@ -0,0 +1,49 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
alias Gcode.Model.{Block, Describe, Serialise}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Describe` protocol for `Block`, meaning that we can convert
blocks into human-readable strings.
"""
@doc false
@spec describe(Block.t(), options :: []) :: Option.t(String.t())
def describe(%Block{words: words, comment: some(comment)}, options) do
words = describe_words(words, options)
some("#{words} (#{comment.comment})")
end
def describe(%Block{words: words}, options) do
words = describe_words(words, options)
some(words)
end
defp describe_words(words, options) do
words
|> Enum.reverse()
|> Stream.map(&describe_or_serialise(&1, options))
|> Stream.reject(&Option.none?/1)
|> Stream.map(&Option.unwrap!/1)
|> Enum.join(", ")
end
defp describe_or_serialise(word, options) do
case Describe.describe(word, options) do
some(description) ->
some(description)
none() ->
case Serialise.serialise(word) do
ok(serialised) ->
serialised
|> Enum.join(", ")
|> some()
error(_) ->
none()
end
end
end
end

View file

@ -0,0 +1,31 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Block do
alias Gcode.{Model.Block, Model.Serialise, Result}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Block`, meaning that blocks can be
turned into G-code output.
"""
@spec serialise(Block.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Block{words: words, comment: some(comment)}) do
words
|> encode_words()
|> Result.map(fn words -> ok(["#{words} #{comment}"]) end)
end
def serialise(%Block{words: words, comment: none()}) do
words
|> encode_words()
|> Result.map(fn words -> ok([words]) end)
end
defp encode_words(words) when is_list(words) do
words
|> Enum.reverse()
|> ok()
|> Result.Enum.map(&Serialise.serialise/1)
|> Result.Enum.join(" ")
end
end

View file

@ -0,0 +1,44 @@
defmodule Gcode.Model.Comment do
alias Gcode.Model.Comment
use Gcode.Option
use Gcode.Result
defstruct comment: Option.none()
@moduledoc """
A G-code comment.
"""
@type t :: %Comment{
comment: String.t()
}
@type error :: {:comment_error, String.t()}
@doc """
Initialise a comment.
## Example
iex> "Doc, in the carpark, with plutonium"
...> |> Comment.init()
{:ok, %Comment{comment: "Doc, in the carpark, with plutonium"}}
"""
@spec init(String.t()) :: Result.t(t, error)
def init(comment) when is_binary(comment) do
if String.printable?(comment) do
ok(%Comment{comment: comment})
else
error(
{:comment_error,
"Expected comment should be a valid UTF-8 string, received #{inspect(comment)}"}
)
end
end
def init(comment),
do:
error(
{:comment_error,
"Expected comment should be a valid UTF-8 string, received #{inspect(comment)}"}
)
end

View file

@ -0,0 +1,12 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Comment do
alias Gcode.Model.Comment
use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Comment`.
"""
@doc "Stubbornly refuse to describe comments"
@spec describe(Comment.t(), options :: []) :: Option.t(String.t())
def describe(_comment, _opts \\ []), do: none()
end

View file

@ -0,0 +1,21 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
alias Gcode.Model.Comment
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Comment`, allowing it to be turned
into G-code output.
"""
@spec serialise(Comment.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Comment{comment: comment}) when is_binary(comment) do
comment
|> String.split(~r/(\r\n|\r|\n)/)
|> Enum.reject(&(byte_size(&1) == 0))
|> Enum.map(&"(#{&1})")
|> ok()
end
def serialise(_comment), do: error({:serialise_error, "Invalid comment"})
end

View file

@ -0,0 +1,11 @@
defprotocol Gcode.Model.Describe do
alias Gcode.Model.Describe
use Gcode.Option
@moduledoc """
A protocol which is used to describe the model for human consumption.
"""
@spec describe(Describe.t(), options :: []) :: Option.t(String.t())
def describe(describable, opts \\ [])
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

@ -0,0 +1,57 @@
defmodule Gcode.Model.Program do
defstruct elements: []
alias Gcode.Model.{Block, Comment, Program, Tape}
use Gcode.Result
@moduledoc """
A G-code program is the high level object which contains each of the G-code
blocks, comments, etc.
## Example
iex> Program.init()
...> |> Result.unwrap!()
...> |> Enum.count()
0
"""
@typedoc "A G-code program"
@type t :: %Program{
elements: [element]
}
@type element :: Block.t() | Comment.t() | Tape.t()
@type error :: {:program_error, String.t()}
@doc """
Initialise a new, empty G-code program.
iex> Program.init()
{:ok, %Program{elements: []}}
"""
@spec init :: Result.t(t())
def init, do: ok(%Program{})
@doc """
Push a program element onto the end of the program.
iex> {:ok, program} = Program.init()
...> {:ok, tape} = Tape.init()
...> Program.push(program, tape)
{:ok, %Program{elements: [%Tape{}]}}
"""
@spec push(t, element) :: Result.t(t, error)
def push(%Program{elements: elements} = program, element)
when is_list(elements) and
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Tape)),
do: ok(%Program{program | elements: [element | elements]})
def push(%Program{elements: elements}, _element) when not is_list(elements),
do: error({:program_error, "Program elements is not a list"})
def push(%Program{}, element),
do: error({:program_error, "Expected a valid program element, received #{inspect(element)}"})
def push(program, _element),
do: error({:program_error, "Expected a valid program, received #{inspect(program)}"})
end

View file

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

View file

@ -0,0 +1,24 @@
defimpl Enumerable, for: Gcode.Model.Program do
alias Gcode.Model.Program
use Gcode.Result
@moduledoc """
Implements the `Enumerable` protocol for `Program`.
"""
@spec count(Program.t()) :: Result.t(non_neg_integer, module)
def count(%Program{elements: elements}),
do: {:ok, Enum.count(elements)}
@spec member?(Program.t(), any) :: Result.t(boolean, module)
def member?(%Program{elements: elements}, element),
do: Enumerable.member?(elements, element)
@spec reduce(Program.t(), Enumerable.acc(), Enumerable.reducer()) :: Enumerable.result()
def reduce(%Program{elements: elements}, acc, fun),
do: Enumerable.reduce(elements, acc, fun)
@spec slice(Program.t()) ::
{:ok, non_neg_integer, Enumerable.slicing_fun()} | Result.error(module)
def slice(%Program{elements: elements}), do: Enumerable.slice(elements)
end

View file

@ -0,0 +1,17 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Program do
alias Gcode.{Model.Program, Model.Serialise}
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Program`, allowing it to be turned
into G-code output.
"""
@spec serialise(Program.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Program{elements: elements}) do
with elements <- Enum.reverse(elements),
ok(result) <- Result.Enum.map(ok(elements), &Serialise.serialise/1) do
ok(List.flatten(result))
end
end
end

View file

@ -0,0 +1,13 @@
defprotocol Gcode.Model.Serialise do
alias Gcode.Model.Serialise
alias Gcode.Result
@moduledoc """
A protocol which is used to serialise the model into G-code output.
"""
@type result :: Result.t([String.t()], {:serialise_error, String.t()})
@spec serialise(Serialise.t()) :: Result.t([String.t()])
def serialise(serialisable)
end

43
lib/gcode/model/skip.ex Normal file
View file

@ -0,0 +1,43 @@
defmodule Gcode.Model.Skip do
alias Gcode.Model.Skip
use Gcode.Option
use Gcode.Result
defstruct number: Option.none()
@moduledoc """
A G-code skip.
"""
@type t :: %Skip{
number: Option.t(non_neg_integer)
}
@type error :: {:skip_error, String.t()}
@doc """
Initialise a skip with a number.
## Example
iex> 13
...> |> Skip.init()
{:ok, %Skip{number: some(13)}}
"""
@spec init(non_neg_integer) :: Result.t(t, error)
def init(number) when is_integer(number) and number >= 0,
do: ok(%Skip{number: Option.some(number)})
def init(number),
do: error({:skip_error, "Expected a positive integer, received #{inspect(number)}"})
@doc """
Initialise a skip without a number.
## Example
iex> Skip.init()
{:ok, %Skip{number: none()}}
"""
@spec init :: Result.t(t)
def init, do: ok(%Skip{})
end

View file

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

View file

@ -0,0 +1,14 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Skip do
alias Gcode.{Model.Skip, Result}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Skip`, allowing it to be turned into
G-code output.
"""
@spec serialise(Skip.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Skip{number: none()}), do: ok(["/"])
def serialise(%Skip{number: some(number)}), do: ok(["/#{number}"])
end

50
lib/gcode/model/tape.ex Normal file
View file

@ -0,0 +1,50 @@
defmodule Gcode.Model.Tape do
defstruct leader: :error
alias Gcode.{Model.Tape}
use Gcode.Option
use Gcode.Result
@moduledoc """
The tape (`%`) denotes the beginning and end of the program and is not needed
by most controllers. Can optionally contain a comment, called a "leader".
"""
@type t :: %Tape{leader: Option.t(String.t())}
@type error :: {:tape_error, String.t()}
@doc """
Initialises a tape command, with no "leader"
## Example
iex> Tape.init()
{:ok, %Tape{leader: :error}}
"""
@spec init :: Result.t(t)
def init, do: ok(%Tape{leader: Option.none()})
@doc """
Initialises a tape command, with a "leader"
## Example
iex> Tape.init("Marty in the Delorean with the Flux Capacitor")
{:ok, %Tape{leader: {:ok, "Marty in the Delorean with the Flux Capacitor"}}}
"""
@spec init(String.t()) :: Result.t(t, error)
def init(leader) when is_binary(leader) do
if String.printable?(leader) do
ok(%Tape{leader: some(leader)})
else
error(
{:tape_error, "Expected leader to be a valid UTF-8 string, recevied #{inspect(leader)}"}
)
end
end
def init(leader),
do:
error(
{:tape_error, "Expected leader to be a valid UTF-8 string, recevied #{inspect(leader)}"}
)
end

View file

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

View file

@ -0,0 +1,14 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Tape do
alias Gcode.{Model.Tape, Result}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Tape`, allowing it to be turned into
G-code output.
"""
@spec serialise(Tape.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Tape{leader: none()}), do: ok(["%"])
def serialise(%Tape{leader: some(leader)}), do: ok(["% #{leader}"])
end

62
lib/gcode/model/word.ex Normal file
View file

@ -0,0 +1,62 @@
defmodule Gcode.Model.Word do
alias Gcode.Model.{Expr, Word}
import Gcode.Model.Expr.Helpers
use Gcode.Option
use Gcode.Result
defstruct word: none(), address: none()
@moduledoc """
A G-code word.
"""
@type t :: %Word{
word: String.t(),
address: Expr.t()
}
@doc """
Initialise a word with a command and an address.
## Example
iex> Word.init("G", 0)
{:ok, %Word{word: "G", address: %Integer{i: 0}}}
"""
@spec init(String.t(), number | Expr.t()) :: Result.t(t)
def init(word, address) when is_binary(word) and is_expression(address) do
if Regex.match?(~r/^[A-Z]$/, word) do
ok(%Word{word: word, address: address})
else
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_integer(address) do
if Regex.match?(~r/^[A-Z]$/, word) do
ok(address) = Expr.Integer.init(address)
ok(%Word{word: word, address: address})
else
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
end
end
def init(word, address) when is_binary(word) and is_float(address) do
if Regex.match?(~r/^[A-Z]$/, word) do
ok(address) = Expr.Float.init(address)
ok(%Word{word: word, address: address})
else
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
end
end
def init(word, address) when is_expression(address),
do: error({:word_error, "Expected word to be a string, received #{inspect(word)}"})
def init(_word, address),
do:
error(
{:word_error,
"Expected address to be an expression or a number, received #{inspect(address)}"}
)
end

View file

@ -0,0 +1,611 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
import Gcode.Model.Expr.Helpers
use Gcode.Option
use Gcode.Result
alias Gcode.Model.{Expr, Word}
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
@moduledoc """
Describes common/conventional words.
"""
@type options :: [option]
@type option :: operation | positioning | compensation | units
@type operation :: {:operation, :milling | :turning | :printing | :plotting}
@type positioning :: {:positioning, :absolute | :relative}
@type compensation :: {:compensation, :left | :right}
@type units :: {:units, :mm | :inches}
@doc "Refer `describe/2`"
@spec describe(Word.t()) :: Option.t(String.t())
def describe(word), do: do_describe(word, %{})
@doc """
Describe a word for human consumption.
*Note:* Many words have different meanings depending on the operation, machine
state or program state. Use can use the `options` argument to provide a hint,
otherwise a more-generic response will be shown.
## Examples
iex> {:ok, word} = Word.init("N", 100)
...> Word.Describe.describe(word)
{:ok, "Line/block number 100"}
iex> {:ok, word} = Word.init("G", 0)
...> Word.Describe.describe(word)
{:ok, "Rapid move"}
iex> {:ok, word} = Word.init("A", 15)
{:ok, "Rotate A axis counterclockwise by/to 15º"}
iex> {:ok, word} = Word.init("A", -15, positioning: :absolute)
{:ok, "Rotate A axis clockwise to 15º"}
iex> {:ok, word} = Word.init("G", 8)
:error
"""
@spec describe(Word.t(), options) :: Option.t(String.t())
def describe(%Word{} = word, options) when is_list(options),
do: do_describe(word, Enum.into(options, %{}))
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
when axis in ~w[A B C] do
case Expr.evaluate(angle) do
ok(angle) when is_number(angle) and angle >= 0 ->
some("Rotate #{axis} axis counterclockwise to #{angle}º")
ok(angle) when is_number(angle) and angle < 0 ->
some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
error(_) ->
none()
end
end
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
when axis in ~w[A B C] do
case Expr.evaluate(angle) do
ok(angle) when is_number(angle) and angle >= 0 ->
some("Rotate #{axis} axis counterclockwise by #{angle}º")
ok(angle) when is_number(angle) and angle < 0 ->
some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
error(_) ->
none()
end
end
defp do_describe(%Word{word: axis, address: angle}, _)
when axis in ~w[A B C] do
case Expr.evaluate(angle) do
ok(angle) when is_number(angle) and angle >= 0 ->
some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
ok(angle) when is_number(angle) and angle < 0 ->
some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
error(_) ->
none()
end
end
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options) do
case distance_with_unit(depth, options) do
some(depth) ->
some("Depth of cut #{depth}")
_ ->
none()
end
end
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}) do
case Expr.evaluate(aperture) do
some(aperture) when is_number(aperture) -> some("Aperture #{aperture}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options) do
case distance_with_unit(offset, options) do
some(offset) -> some("Left radial offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options) do
case distance_with_unit(offset, options) do
some(offset) -> some("Right radial offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: offset}, options) do
case distance_with_unit(offset, options) do
some(offset) -> some("Radial offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options) do
case feedrate(feedrate, options) do
some(feedrate) -> some("Extruder feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options) do
case feedrate(feedrate, options) do
some(feedrate) -> some("Precision feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: "F", address: feedrate}, options) do
case feedrate(feedrate, options) do
some(feedrate) -> some("Feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: "G", address: address}, options) do
case {Expr.evaluate(address), options} do
{ok(0), _} ->
some("Rapid move")
{ok(1), _} ->
some("Linear move")
{ok(2), _} ->
some("Clockwise circular move")
{ok(3), _} ->
some("Counterclockwise circular move")
{ok(4), _} ->
some("Dwell")
{ok(5), _} ->
some("High-precision contour control")
{ok(5.1), _} ->
some("AI advanced preview control")
{ok(6.1), _} ->
some("NURBS machining")
{ok(7), _} ->
some("Imaginary axis designation")
{ok(9), _} ->
some("Exact stop check - non-modal")
{ok(10), _} ->
some("Programmable data input")
{ok(11), _} ->
some("Data write cancel")
{ok(17), _} ->
some("XY plane selection")
{ok(18), _} ->
some("ZX plane selection")
{ok(19), _} ->
some("YZ plane selection")
{ok(20), _} ->
some("Unit is inches")
{ok(21), _} ->
some("Unit is mm")
{ok(28), _} ->
some("Return to home position")
{ok(30), _} ->
some("Return to secondary home position")
{ok(31), _} ->
some("Feed until skip function")
{ok(32), _} ->
some("Single-point threading")
{ok(33), _} ->
some("Variable pitch threading")
{ok(40), _} ->
some("Tool radius compensation off")
{ok(41), _} ->
some("Tool radius compensation left")
{ok(42), _} ->
some("Tool radius compensation right")
{ok(43), _} ->
some("Tool height offset compensation negative")
{ok(44), _} ->
some("Tool height offset compensation positive")
{ok(45), _} ->
some("Axis offset single increase")
{ok(46), _} ->
some("Axis offset single decrease")
{ok(47), _} ->
some("Axis offset double increase")
{ok(48), _} ->
some("Axis offset double decrease")
{ok(49), _} ->
some("Tool length offset compensation cancel")
{ok(50), %{operation: :turning}} ->
some("Position register")
{ok(50), _} ->
some("Scaling function cancel")
{ok(52), _} ->
some("Local coordinate system")
{ok(53), _} ->
some("Machine coordinate system")
{ok(address), _} when address in [54, 55, 56, 57, 58, 59, 54.1] ->
some("Work coordinate system")
{ok(61), _} ->
some("Exact stop check - modal")
{ok(62), _} ->
some("Automatic corner override")
{ok(64), _} ->
some("Default cutting mode")
{ok(68), _} ->
some("Rotate coordinate system")
{ok(69), _} ->
some("Turn off coordinate system rotation")
{ok(70), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for finishing")
{ok(71), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
{ok(72), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
{ok(73), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
{ok(73), _} ->
some("Peck drilling cycle")
{ok(74), %{operation: :turning}} ->
some("Peck drilling cycle")
{ok(74), _} ->
some("Tapping cycle")
{ok(75), %{operation: :turning}} ->
some("Peck grooving cycle")
{ok(76), %{operation: :turning}} ->
some("Threading cycle")
{ok(76), _} ->
some("Fine boring cycle")
{ok(80), _} ->
some("Cancel cycle")
{ok(81), _} ->
some("Simple drilling cycle")
{ok(82), _} ->
some("Drilling cycle with dwell")
{ok(83), _} ->
some("Peck drilling cycle")
{ok(84), _} ->
some("Tapping cycle, righthand thread, M03 spindle direction")
{ok(84.2), _} ->
some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
{ok(84.3), _} ->
some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
{ok(85), _} ->
some("Boring cycle, feed in/feed out")
{ok(86), _} ->
some("Boring cycle, feed in/spindle stop/rapid out")
{ok(87), _} ->
some("Boring cycle, backboring")
{ok(88), _} ->
some("Boring cycle, feed in/spindle stop/manual operation")
{ok(89), _} ->
some("Boring cycle, feed in/dwell/feed out")
{ok(90), _} ->
some("Absolute positioning")
{ok(91), _} ->
some("Relative positioning")
{ok(92), _} ->
some("Position register")
{ok(94), _} ->
some("Feedrate per minute")
{ok(95), _} ->
some("Feedrate per revolution")
{ok(96), _} ->
some("Constant surface speed")
{ok(97), _} ->
some("Constant spindle speed")
{ok(98), %{operation: :turning}} ->
some("Feedrate per minute")
{ok(98), _} ->
some("Return to initial Z level in canned cycle")
{ok(99), %{operation: :turning}} ->
some("Feedrate per revolution")
{ok(99), _} ->
some("Return to R level in canned cycle")
{ok(100), _} ->
some("Tool length measurement")
_ ->
none()
end
end
defp do_describe(%Word{word: "H", address: length}, options) do
case distance_with_unit(length, options) do
ok(length) -> some("Tool length offset #{length}")
_ -> none()
end
end
defp do_describe(%Word{word: "I", address: offset}, options) do
case distance_with_unit(offset, options) do
ok(offset) -> some("X arc center offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "J", address: offset}, options) do
case distance_with_unit(offset, options) do
ok(offset) -> some("Y arc center offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "K", address: offset}, options) do
case distance_with_unit(offset, options) do
ok(offset) -> some("Z arc center offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "L", address: count}, _) do
case Expr.evaluate(count) do
ok(count) -> some("Loop count #{count}")
_ -> none()
end
end
defp do_describe(%Word{word: "M", address: address}, options) do
case {Expr.evaluate(address), options} do
{ok(0), _} ->
some("Compulsory stop")
{ok(1), _} ->
some("Optional stop")
{ok(2), _} ->
some("End of program")
{ok(3), _} ->
some("Spindle on clockwise")
{ok(4), _} ->
some("Spindle on counterclockwise")
{ok(5), _} ->
some("Spindle stop")
{ok(6), _} ->
some("Automatic tool change")
{ok(7), _} ->
some("Coolant mist")
{ok(8), _} ->
some("Coolant flood")
{ok(9), _} ->
some("Coolant off")
{ok(10), _} ->
some("Pallet clamp on")
{ok(11), _} ->
some("Pallet clamp off")
{ok(13), _} ->
some("Spindle on clockwise and coolant flood")
{ok(19), _} ->
some("Spindle orientation")
{ok(21), %{operation: :turning}} ->
some("Tailstock forward")
{ok(21), _} ->
some("Mirror X axis")
{ok(22), %{operation: :turning}} ->
some("Tailstock backward")
{ok(22), _} ->
some("Mirror Y axis")
{ok(23), %{operation: :turning}} ->
some("Thread gradual pullout on")
{ok(23), _} ->
some("Mirror off")
{ok(24), %{operation: :turning}} ->
some("Thread gradual pullout off")
{ok(30), _} ->
some("End of program")
{ok(gear), %{operation: :turning}} when gear > 40 and gear < 45 ->
some("Gear select #{gear - 40}")
{ok(48), _} ->
some("Feedrate override allowed")
{ok(49), _} ->
some("Feedrate override not allowed")
{ok(52), _} ->
some("Unload tool")
{ok(60), _} ->
some("Automatic pallet change")
{ok(98), _} ->
some("Subprogram call")
{ok(99), _} ->
some("Subprogram end")
{ok(100), _} ->
some("Clean nozzle")
_ ->
none()
end
end
defp do_describe(%Word{word: "N", address: line}, _) do
case Expr.evaluate(line) do
ok(line) -> some("Line #{line}")
_ -> none()
end
end
defp do_describe(%Word{word: "O", address: name}, _) do
case Expr.evaluate(name) do
ok(name) -> some("Program #{name}")
_ -> none()
end
end
defp do_describe(%Word{word: "P", address: param}, _) do
case Expr.evaluate(param) do
ok(param) -> some("Parameter #{param}")
_ -> none()
end
end
defp do_describe(%Word{word: "Q", address: distance}, options) do
case distance_with_unit(distance, options) do
ok(distance) -> some("Peck increment #{distance}")
_ -> none()
end
end
defp do_describe(%Word{word: "R", address: distance}, options) do
case distance_with_unit(distance, options) do
ok(distance) -> some("Radius #{distance}")
_ -> none()
end
end
defp do_describe(%Word{word: "S", address: speed}, _) do
case Expr.evaluate(speed) do
ok(speed) -> some("Speed #{speed}")
_ -> none()
end
end
defp do_describe(%Word{word: "T", address: tool}, _) do
case Expr.evaluate(tool) do
ok(tool) -> some("Tool #{tool}")
_ -> none()
end
end
defp do_describe(%Word{word: axis, address: distance}, options) when axis in ~w[X Y Z] do
case distance_with_unit(distance, options) do
ok(distance) -> some("#{axis} #{distance}")
_ -> none()
end
end
defp do_describe(%Word{}, _options), do: none()
defp distance_with_unit(distance, %{units: :mm}) when is_number(distance),
do: some("#{distance}mm")
defp distance_with_unit(distance, %{units: :inches}) when is_number(distance),
do: some("#{distance}\"")
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

View file

@ -0,0 +1,27 @@
defimpl Inspect, for: Gcode.Model.Word do
alias Gcode.Model.{Describe, Expr, Word}
use Gcode.Option
use Gcode.Result
import Inspect.Algebra
@moduledoc false
def inspect(%Word{word: letter, address: address} = word, opts) do
address =
case Expr.evaluate(address) do
ok(address) -> address
error(reason) -> "Address error: #{inspect(reason)}"
end
case Describe.describe(word) do
some(description) ->
concat([
"#Gcode.Word<",
to_doc([word: letter, address: address], opts),
" (#{description})>"
])
none() ->
concat(["#Gcode.Word<", to_doc([word: letter, address: address], opts), ">"])
end
end
end

View file

@ -0,0 +1,19 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do
alias Gcode.Model.{Word, Serialise}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Word`, allowing it to be turned into
G-code output.
"""
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Word{word: word, address: address})
when is_binary(word) and byte_size(word) == 1 do
with ok(address) <- Serialise.serialise(address),
do: ok(["#{word}#{address}"])
end
def serialise(_word), do: error({:serialise_error, "invalid word"})
end

50
lib/gcode/option.ex Normal file
View file

@ -0,0 +1,50 @@
defmodule Gcode.Option do
@moduledoc """
A helper which represents an optional type.
"""
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do
quote do
alias Gcode.Option
require Gcode.Option
import Gcode.Option, only: [some: 1, none: 0]
end
end
@type t :: t(any)
@type t(value) :: some(value) | opt_none
@type some(t) :: {:ok, t}
@type opt_none :: :error
@doc "Is the value a none?"
@spec none?(t(any)) :: boolean
def none?(:error), do: true
def none?({:ok, _}), do: false
@doc "Is the value a some?"
@spec some?(t(any)) :: boolean
def some?(:error), do: false
def some?({:ok, _}), do: true
@doc "Create or match a none"
@spec none :: Macro.t()
defmacro none do
quote do
:error
end
end
@doc "Create or match a some"
@spec some(any) :: Macro.t()
defmacro some(pattern) do
quote do
{:ok, unquote(pattern)}
end
end
@doc "Attempt to unwrap an option. Raises an error if the option is a none"
@spec unwrap!(t) :: any | no_return
def unwrap!({:ok, result}), do: result
def unwrap!(:error), do: raise("Attempt to unwrap a none")
end

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

64
lib/gcode/result.ex Normal file
View file

@ -0,0 +1,64 @@
defmodule Gcode.Result do
@moduledoc """
A helper which represents a result type.
This is really just a wrapper around Erlang's ok/error tuples.
"""
@type t :: t(any, any)
@type t(result) :: t(result, any)
@type t(result, error) :: ok(result) | error(error)
@type ok(result) :: {:ok, result}
@type error(error) :: {:error, error}
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do
quote do
alias Gcode.Result
require Gcode.Result
import Gcode.Result, only: [ok: 1, error: 1]
end
end
@doc "Initialise or match an ok value"
@spec ok(any) :: Macro.t()
defmacro ok(result) do
quote do
{:ok, unquote(result)}
end
end
@doc "Initialise or match an error value"
@spec error(any) :: Macro.t()
defmacro error(error) do
quote do
{:error, unquote(error)}
end
end
@doc "Is the result ok?"
@spec ok?(t) :: boolean
def ok?({:ok, _}), do: true
def ok?({:error, _}), do: false
@doc "Is the result an error?"
@spec error?(t) :: boolean
def error?({:ok, _}), do: false
def error?({:error, _}), do: true
@doc "Attempt to unwrap a result and return the inner value. Raises an exception if the result contains an error."
@spec unwrap!(t) :: any | no_return
def unwrap!({:ok, result}), do: result
def unwrap!({:error, error}), do: raise(error)
@doc "Convert a successful result another result."
@spec map(t, (any -> t)) :: t
def map({:ok, value}, mapper) when is_function(mapper, 1) do
case mapper.(value) do
{:ok, value} -> {:ok, value}
{:error, error} -> {:error, error}
end
end
def map({:error, value}, mapper) when is_function(mapper, 1), do: {:error, value}
end

59
lib/gcode/result/enum.ex Normal file
View file

@ -0,0 +1,59 @@
defmodule Gcode.Result.Enum do
use Gcode.Result
@moduledoc """
Common enumerableish functions on results.
"""
@type result :: Result.t()
@type result(result) :: Result.t(result)
@type result(result, error) :: Result.t(result, error)
@doc "As long as the result of the reducer is ok, continue reducing, otherwise short circuit"
@spec reduce_while_ok(
enumerable :: any,
accumulator :: any,
reducer :: (any, any -> result(any))
) :: result(any)
def reduce_while_ok(elements, acc, reducer) when is_function(reducer, 2) do
Enum.reduce_while(elements, ok(acc), fn element, ok(acc) ->
case reducer.(element, acc) do
ok(acc) -> {:cont, ok(acc)}
error(reason) -> {:halt, error(reason)}
end
end)
end
@doc """
Maps a collection of results using a mapping function.
Both the input to the map must be an ok result and the result of each mapping
function.
"""
@spec map(result([any]), mapper :: (any -> result(any))) :: result([any])
def map(ok(enumerable), mapper) when is_function(mapper, 1) do
reduce_while_ok(enumerable, [], fn element, acc ->
case mapper.(element) do
ok(mapped) -> ok([mapped | acc])
error(reason) -> error(reason)
end
end)
|> reverse()
end
def map(error(reason), _mapper), do: error(reason)
@doc """
Reverse the enumerable contents of an ok result.
"""
@spec reverse(result([any])) :: result([any])
def reverse(ok(enumerable)), do: ok(Enum.reverse(enumerable))
def reverse(error(reason)), do: error(reason)
@doc """
Join the string contents of an ok result.
"""
@spec join(result([String.t()]), String.t()) :: result(String.t())
def join(ok(strings), joiner) when is_binary(joiner), do: ok(Enum.join(strings, joiner))
def join(error(reason), _joiner), do: error(reason)
end

34
mix.exs
View file

@ -1,7 +1,8 @@
defmodule Gcode.MixProject do
use Mix.Project
@moduledoc false
@version "0.1.0"
@version "1.0.0"
@description """
A G-code parser and generator.
"""
@ -14,7 +15,13 @@ defmodule Gcode.MixProject do
start_permanent: Mix.env() == :prod,
package: package(),
description: @description,
deps: deps()
deps: deps(),
consolidate_protocols: Mix.env() != :test,
elixirc_paths: elixirc_paths(Mix.env()),
docs: [
main: "readme",
extras: ["README.md", "CHANGELOG.md"]
]
]
end
@ -28,9 +35,12 @@ defmodule Gcode.MixProject do
def package do
[
maintainers: ["James Harton <james@harton.nz>"],
licenses: ["Hippocratic"],
licenses: ["HL3-FULL"],
links: %{
"Source" => "https://gitlab.com/jimsy/gcode"
"Source" => "https://harton.dev/james/gcode",
"GitHub" => "https://github.com/jimsynz/gcode",
"Changelog" => "https://docs.harton.nz/james/gcode/changelog.html",
"Sponsor" => "https://github.com/sponsors/jimsynz"
}
]
end
@ -38,10 +48,18 @@ defmodule Gcode.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ex_doc, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
{:earmark, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
{:credo, "~> 1.1", only: ~w[dev test]a, runtime: false},
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false}
{:nimble_parsec, "~> 1.2"},
{:parallel_stream, "~> 1.1"},
# Dev/test
{:credo, "~> 1.6", only: ~w[dev test]a, runtime: false},
{:ex_check, "~> 0.15", only: ~w[dev test]a, runtime: false},
{:ex_doc, "~> 0.30", only: ~w[dev test]a, runtime: false},
{:earmark, "~> 1.4", only: ~w[dev test]a, runtime: false},
{:git_ops, "~> 2.4", only: ~w[dev test]a, runtime: false}
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
end

View file

@ -1,14 +1,17 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"credo": {:hex, :credo, "1.5.4", "9914180105b438e378e94a844ec3a5088ae5875626fc945b7c1462b41afc3198", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cf51af45eadc0a3f39ba13b56fdac415c91b34f7b7533a13dc13550277141bc4"},
"earmark": {:hex, :earmark, "1.4.13", "2c6ce9768fc9fdbf4046f457e207df6360ee6c91ee1ecb8e9a139f96a4289d91", [:mix], [{:earmark_parser, ">= 1.4.12", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "a0cf3ed88ef2b1964df408889b5ecb886d1a048edde53497fc935ccd15af3403"},
"earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},
"ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"},
"credo": {:hex, :credo, "1.6.1", "7dc76dcdb764a4316c1596804c48eada9fff44bd4b733a91ccbf0c0f368be61e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "698607fb5993720c7e93d2d8e76f2175bba024de964e160e2f7151ef3ab82ac5"},
"earmark": {:hex, :earmark, "1.4.19", "3854a17305c880cc46305af15fb1630568d23a709aba21aaa996ced082fc29d7", [:mix], [{:earmark_parser, ">= 1.4.18", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d5a8c9f9e37159a8fdd3ea8437fb4e229eaf56d5129b9a011dc4780a4872079d"},
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
"ex_doc": {:hex, :ex_doc, "0.30.0", "ed94bf5183f559d2f825e4f866cc0eab277bbb17da76aff40f8e0f149656943e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "6743fe46704fe27e2f2558faa61f00e5356528768807badb2092d38476d6dac2"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.3.0", "a77f91b810d874e1abf5f415f335959a2dfc3613cbcd28c7c05b97c666339fda", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "849bd53b7992963b3fdfebcdd0c946f4aab3f6ffbcfa5668b3e83cd5aeca0a2f"},
"git_ops": {:hex, :git_ops, "2.4.5", "185a724dfde3745edd22f7571d59c47a835cf54ded67e9ccbc951920b7eec4c2", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e323a5b01ad53bc8c19c3a444be3e61ed7803ecd2e95530446ae9327d0143ecc"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
}

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

@ -0,0 +1,11 @@
defmodule Gcode.Model.Block.DescribeTest do
use DescribeCase, async: true
@moduledoc false
describes_block("A13 B-15.2",
with: [positioning: :absolute],
as: "Rotate A axis counterclockwise to 13º, Rotate B axis clockwise to 15.2º"
)
describes_block("G90 M213", as: "Absolute positioning, M213")
end

View file

@ -0,0 +1,25 @@
defmodule Gcode.Model.Block.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Block, Expr, Serialise, Word}
use Gcode.Result
@moduledoc false
describe "serialise/1" do
assert ok(block) =
with(
ok(block) <- Block.init(),
ok(address) <- Expr.Integer.init(0),
ok(word) <- Word.init("G", address),
ok(block) <- Block.push(block, word),
ok(address) <- Expr.Integer.init(100),
ok(word) <- Word.init("N", address),
do: Block.push(block, word)
)
assert ok([actual]) = Serialise.serialise(block)
expected = "G0 N100"
assert actual == expected
end
end

View file

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

View file

@ -0,0 +1,26 @@
defmodule Gcode.Model.Comment.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Comment, Serialise}
use Gcode.Result
@moduledoc false
describe "serialise/1" do
test "each line of the comment is wrapped in brackets" do
comment = """
This
is
a
test
"""
actual =
with ok(comment) <- Comment.init(comment),
ok(comment) <- Serialise.serialise(comment),
do: comment
expected = ~w[(This) (is) (a) (test)]
assert actual == expected
end
end
end

View file

@ -0,0 +1,7 @@
defmodule Gcode.Model.CommentTest do
use ExUnit.Case, async: true
alias Gcode.Model.Comment
use Gcode.Option
doctest Gcode.Model.Comment
@moduledoc false
end

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

@ -0,0 +1,46 @@
defmodule Gcode.Model.Program.DescribeTest do
use DescribeCase, async: true
@moduledoc false
@program """
O4968
N01 M216
N02 G20 G90 G54 D200 G40
N03 G50 S2000
N04 T0300
N05 G96 S854 M03
N06 G41 G00 X1.1 Z1.1 T0303 M08
N07 G01 Z1.0 F.05
N08 X-0.016
N09 G00 Z1.1
N10 X1.0
N11 G01 Z0.0 F.05
N12 G00 X1.1 M05 M09
N13 G91 G28 X0
N14 G91 G28 Z0
N15 G90
N16 M30
"""
@expected """
Program 4968
Line 1, M216
Line 2, Unit is inches, Absolute positioning, Work coordinate system, Radial offset 200, Tool radius compensation off
Line 3, Scaling function cancel, Speed 2000
Line 4, Tool 300
Line 5, Constant surface speed, Speed 854, Spindle on clockwise
Line 6, Tool radius compensation left, Rapid move, X 1.1, Z 1.1, Tool 303, Coolant flood
Line 7, Linear move, Z 1.0
Line 8, X -0.016
Line 9, Rapid move, Z 1.1
Line 10, X 1.0
Line 11, Linear move, Z 0.0
Line 12, Rapid move, X 1.1, Spindle stop, Coolant off
Line 13, Relative positioning, Return to home position, X 0
Line 14, Relative positioning, Return to home position, Z 0
Line 15, Absolute positioning
Line 16, End of program
"""
describes_program(@program, as: @expected)
end

View file

@ -0,0 +1,51 @@
defmodule Gcode.Model.Program.SerialiseTest do
use ExUnit.Case, async: true
use Gcode.Result
alias Gcode.Model.{Block, Comment, Program, Serialise, Skip, Tape, Word}
@moduledoc false
describe "serialise/1" do
test "it formats correctly" do
actual =
with ok(program) <- Program.init(),
ok(tape) <- Tape.init("The beginning"),
ok(program) <- Program.push(program, tape),
ok(comment) <- Comment.init("I am a single line comment"),
ok(program) <- Program.push(program, comment),
ok(block) <- Block.init(),
ok(word) <- Word.init("G", 0),
ok(block) <- Block.push(block, word),
ok(word) <- Word.init("N", 100),
ok(block) <- Block.push(block, word),
ok(program) <- Program.push(program, block),
ok(comment) <- Comment.init("I\nam\na\nmultiline\ncomment"),
ok(program) <- Program.push(program, comment),
ok(block) <- Block.init(),
ok(skip) <- Skip.init(),
ok(block) <- Block.push(block, skip),
ok(word) <- Word.init("N", 200),
ok(block) <- Block.push(block, word),
ok(program) <- Program.push(program, block),
ok(tape) <- Tape.init("The end"),
ok(program) <- Program.push(program, tape),
ok(result) <- Serialise.serialise(program) do
result
end
expected = [
"% The beginning",
"(I am a single line comment)",
"G0 N100",
"(I)",
"(am)",
"(a)",
"(multiline)",
"(comment)",
"/ N200",
"% The end"
]
assert actual == expected
end
end
end

View file

@ -0,0 +1,7 @@
defmodule Gcode.Model.ProgramTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Program, Tape}
use Gcode.Result
doctest Gcode.Model.Program
@moduledoc false
end

View file

@ -0,0 +1,30 @@
defmodule Gcode.Model.Skip.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Serialise, Skip}
use Gcode.Result
@moduledoc false
describe "serialise/1" do
test "when the skip has a number, it formats it correctly" do
actual =
with ok(skip) <- Skip.init(0),
ok(skip) <- Serialise.serialise(skip),
do: skip
expected = ~w[/0]
assert actual == expected
end
test "when the skip has no number, it formats it correctly" do
actual =
with ok(skip) <- Skip.init(),
ok(skip) <- Serialise.serialise(skip),
do: skip
expected = ~w[/]
assert actual == expected
end
end
end

View file

@ -0,0 +1,7 @@
defmodule Gcode.Model.SkipTest do
use ExUnit.Case, async: true
alias Gcode.Model.Skip
use Gcode.Option
doctest Gcode.Model.Skip
@moduledoc false
end

View file

@ -0,0 +1,5 @@
defmodule Gcode.Model.TapeTest do
use ExUnit.Case, async: true
alias Gcode.Model.Tape
doctest Gcode.Model.Tape
end

View file

@ -0,0 +1,192 @@
defmodule Gcode.Model.Word.DescribeTest do
use DescribeCase, async: true
@moduledoc false
describes_word("A13", as: "Rotate A axis counterclockwise by/to 13º")
describes_word("B-15.2", with: [positioning: :absolute], as: "Rotate B axis clockwise to 15.2º")
describes_word("C99",
with: [positioning: :relative],
as: "Rotate C axis counterclockwise by 99º"
)
describes_word("D22", as: "Radial offset 22")
describes_word("D22", with: [operation: :turning], as: "Depth of cut 22")
describes_word("D22", with: [operation: :plotting], as: "Aperture 22")
describes_word("D22", with: [compensation: :left, units: :mm], as: "Left radial offset 22mm")
describes_word("D22",
with: [compensation: :right, units: :inches],
as: "Right radial offset 22\""
)
describes_word("E123",
with: [operation: :turning, units: :mm],
as: "Precision feedrate 123mm/rev"
)
describes_word("E123",
with: [operation: :printing, units: :inches],
as: "Extruder feedrate 123\"/min"
)
describes_word("E123", with: [operation: :turning], as: "Precision feedrate 123/rev")
describes_word("F100", with: [operation: :turning, units: :mm], as: "Feedrate 100mm/rev")
describes_word("F100", with: [units: :inches], as: "Feedrate 100\"/min")
describes_word("F100", as: "Feedrate 100/min")
describes_word("G0", as: "Rapid move")
describes_word("G1", as: "Linear move")
describes_word("G2", as: "Clockwise circular move")
describes_word("G3", as: "Counterclockwise circular move")
describes_word("G4", as: "Dwell")
describes_word("G5", as: "High-precision contour control")
describes_word("G5.1", as: "AI advanced preview control")
describes_word("G6.1", as: "NURBS machining")
describes_word("G7", as: "Imaginary axis designation")
describes_word("G9", as: "Exact stop check - non-modal")
describes_word("G10", as: "Programmable data input")
describes_word("G11", as: "Data write cancel")
describes_word("G17", as: "XY plane selection")
describes_word("G18", as: "ZX plane selection")
describes_word("G19", as: "YZ plane selection")
describes_word("G20", as: "Unit is inches")
describes_word("G21", as: "Unit is mm")
describes_word("G28", as: "Return to home position")
describes_word("G30", as: "Return to secondary home position")
describes_word("G31", as: "Feed until skip function")
describes_word("G32", as: "Single-point threading")
describes_word("G33", as: "Variable pitch threading")
describes_word("G40", as: "Tool radius compensation off")
describes_word("G41", as: "Tool radius compensation left")
describes_word("G42", as: "Tool radius compensation right")
describes_word("G43", as: "Tool height offset compensation negative")
describes_word("G44", as: "Tool height offset compensation positive")
describes_word("G45", as: "Axis offset single increase")
describes_word("G46", as: "Axis offset single decrease")
describes_word("G47", as: "Axis offset double increase")
describes_word("G48", as: "Axis offset double decrease")
describes_word("G49", as: "Tool length offset compensation cancel")
describes_word("G50", as: "Scaling function cancel")
describes_word("G50", with: [operation: :turning], as: "Position register")
describes_word("G52", as: "Local coordinate system")
describes_word("G53", as: "Machine coordinate system")
describes_word("G54", as: "Work coordinate system")
describes_word("G55", as: "Work coordinate system")
describes_word("G56", as: "Work coordinate system")
describes_word("G57", as: "Work coordinate system")
describes_word("G58", as: "Work coordinate system")
describes_word("G59", as: "Work coordinate system")
describes_word("G54.1", as: "Work coordinate system")
describes_word("G61", as: "Exact stop check - modal")
describes_word("G62", as: "Automatic corner override")
describes_word("G64", as: "Default cutting mode")
describes_word("G68", as: "Rotate coordinate system")
describes_word("G69", as: "Turn off coordinate system rotation")
describes_word("G70",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for finishing"
)
describes_word("G71",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis"
)
describes_word("G72",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis"
)
describes_word("G73",
with: [operation: :turning],
as: "Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition"
)
describes_word("G73", as: "Peck drilling cycle")
describes_word("G74", with: [operation: :turning], as: "Peck drilling cycle")
describes_word("G74", as: "Tapping cycle")
describes_word("G75", with: [operation: :turning], as: "Peck grooving cycle")
describes_word("G76", as: "Fine boring cycle")
describes_word("G76", with: [operation: :turning], as: "Threading cycle")
describes_word("G80", as: "Cancel cycle")
describes_word("G81", as: "Simple drilling cycle")
describes_word("G82", as: "Drilling cycle with dwell")
describes_word("G83", as: "Peck drilling cycle")
describes_word("G84", as: "Tapping cycle, righthand thread, M03 spindle direction")
describes_word("G84.2",
as: "Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder"
)
describes_word("G84.3",
as: "Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder"
)
describes_word("G85", as: "Boring cycle, feed in/feed out")
describes_word("G86", as: "Boring cycle, feed in/spindle stop/rapid out")
describes_word("G87", as: "Boring cycle, backboring")
describes_word("G88", as: "Boring cycle, feed in/spindle stop/manual operation")
describes_word("G89", as: "Boring cycle, feed in/dwell/feed out")
describes_word("G90", as: "Absolute positioning")
describes_word("G91", as: "Relative positioning")
describes_word("G92", as: "Position register")
describes_word("G94", as: "Feedrate per minute")
describes_word("G95", as: "Feedrate per revolution")
describes_word("G96", as: "Constant surface speed")
describes_word("G97", as: "Constant spindle speed")
describes_word("G98", as: "Return to initial Z level in canned cycle")
describes_word("G98", with: [operation: :turning], as: "Feedrate per minute")
describes_word("G99", as: "Return to R level in canned cycle")
describes_word("G99", with: [operation: :turning], as: "Feedrate per revolution")
describes_word("G100", as: "Tool length measurement")
describes_word("H76", with: [units: :mm], as: "Tool length offset 76mm")
describes_word("I1.21", with: [units: :mm], as: "X arc center offset 1.21mm")
describes_word("J1.21", with: [units: :inches], as: "Y arc center offset 1.21\"")
describes_word("K1.21", as: "Z arc center offset 1.21")
describes_word("L87", as: "Loop count 87")
describes_word("M0", as: "Compulsory stop")
describes_word("M1", as: "Optional stop")
describes_word("M2", as: "End of program")
describes_word("M3", as: "Spindle on clockwise")
describes_word("M4", as: "Spindle on counterclockwise")
describes_word("M5", as: "Spindle stop")
describes_word("M6", as: "Automatic tool change")
describes_word("M7", as: "Coolant mist")
describes_word("M8", as: "Coolant flood")
describes_word("M9", as: "Coolant off")
describes_word("M10", as: "Pallet clamp on")
describes_word("M11", as: "Pallet clamp off")
describes_word("M13", as: "Spindle on clockwise and coolant flood")
describes_word("M19", as: "Spindle orientation")
describes_word("M21", as: "Mirror X axis")
describes_word("M21", with: [operation: :turning], as: "Tailstock forward")
describes_word("M22", as: "Mirror Y axis")
describes_word("M22", with: [operation: :turning], as: "Tailstock backward")
describes_word("M23", as: "Mirror off")
describes_word("M23", with: [operation: :turning], as: "Thread gradual pullout on")
describes_word("M24", with: [operation: :turning], as: "Thread gradual pullout off")
describes_word("M30", as: "End of program")
describes_word("M41", with: [operation: :turning], as: "Gear select 1")
describes_word("M42", with: [operation: :turning], as: "Gear select 2")
describes_word("M43", with: [operation: :turning], as: "Gear select 3")
describes_word("M44", with: [operation: :turning], as: "Gear select 4")
describes_word("M48", as: "Feedrate override allowed")
describes_word("M49", as: "Feedrate override not allowed")
describes_word("M52", as: "Unload tool")
describes_word("M60", as: "Automatic pallet change")
describes_word("M98", as: "Subprogram call")
describes_word("M99", as: "Subprogram end")
describes_word("M100", as: "Clean nozzle")
describes_word("N100", as: "Line 100")
describes_word("O200", as: "Program 200")
describes_word("P12", as: "Parameter 12")
describes_word("Q34", as: "Peck increment 34")
describes_word("R37", with: [units: :mm], as: "Radius 37mm")
describes_word("S12", as: "Speed 12")
describes_word("T4", as: "Tool 4")
describes_word("X2.34", as: "X 2.34")
describes_word("Y3.45", as: "Y 3.45")
describes_word("Z4.56", as: "Z 4.56")
end

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