Compare commits

...

73 commits

Author SHA1 Message Date
Jonathan Pallant (Ferrous Systems) 5e6fb4ab06
Clean up slides. 2023-03-22 10:29:07 +00:00
Jonathan Pallant (Ferrous Systems) a4ee38540d
Ensure marp can render slides.
We bundle markdown-it-kroki, which can add mermaid diagrams to marp slides using https://kroki.io.
2023-03-22 10:28:51 +00:00
Jonathan Pallant (Ferrous Systems) 04fc983107
Add notes on closures. 2023-03-22 09:48:35 +00:00
Jonathan Pallant (Ferrous Systems) 33778655e4
Start down-the-stack book.
Has notes on PACs.
2023-03-22 09:45:30 +00:00
Tanks Transfeld 3a4acd3543
Merge pull request #201 from ferrous-systems/fix_serial_term
Add pyserial and change uarte setup
2023-03-21 18:51:55 +01:00
Mirabellensaft 55b40e8b47 change uarte setup 2023-03-21 17:37:11 +01:00
Mirabellensaft 15302d1e38 add pyserial installation to installation chapter 2023-03-21 17:36:53 +01:00
Mirabellensaft c097fcfab5 add pyserial to book 2023-03-21 17:36:25 +01:00
Tanks Transfeld 70ce745930
Merge pull request #196 from ferrous-systems/add_bsc_exercise_text
Add bsc exercise text
2023-03-20 17:37:08 +01:00
Mirabellensaft ee0f7b1145 add links to html doc 2023-03-20 15:48:21 +01:00
Tanks Transfeld b09dd739f8
Update embedded-workshop-book/src/uarte-implementation.md
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 15:21:07 +01:00
Tanks Transfeld 5e5ccb510b
Update embedded-workshop-book/src/uarte-implementation.md
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 15:20:19 +01:00
Tanks Transfeld 7701ae8696
Update embedded-workshop-book/src/uarte-implementation.md
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 15:19:13 +01:00
Mirabellensaft debd7658b7 rm led color comment 2023-03-20 15:17:43 +01:00
Tanks Transfeld f981189222
Update embedded-workshop-book/src/uarte-implementation.md
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:43:38 +01:00
Tanks Transfeld ee29b74c7f
Update embedded-workshop-book/src/bsc-exercise.md
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:42:51 +01:00
Tanks Transfeld 08654a53da
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:42:19 +01:00
Tanks Transfeld 1db5935bbb
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:41:47 +01:00
Tanks Transfeld 8de826d0bc
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:41:28 +01:00
Tanks Transfeld 4fb7415294
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:41:08 +01:00
Tanks Transfeld 81b6657703
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:40:40 +01:00
Tanks Transfeld dc9232f25e
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:40:11 +01:00
Tanks Transfeld d6470363d4
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:39:46 +01:00
Tanks Transfeld 274767bc51
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:39:09 +01:00
Tanks Transfeld 89ee72eb49
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:38:46 +01:00
Tanks Transfeld a3f653de23
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:38:15 +01:00
Tanks Transfeld b5c3480d25
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:37:53 +01:00
Tanks Transfeld 5fc64c53a2
Update embedded-workshop-book/src/uarte-implementation.md
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:37:28 +01:00
Tanks Transfeld f0bf2ec6c3
Update embedded-workshop-book/src/uarte-implementation.md
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:36:25 +01:00
Tanks Transfeld e7eef7aff9
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:36:12 +01:00
Tanks Transfeld 2505e00e32
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:35:43 +01:00
Tanks Transfeld 5f079cdd88
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:35:28 +01:00
Tanks Transfeld ab2d9081bd
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:35:14 +01:00
Tanks Transfeld bfcb572bc1
Update down-the-stack/dk_bsc/src/lib_solution.rs
Co-authored-by: Jonathan Pallant <jonathan.pallant@ferrous-systems.com>
2023-03-20 14:34:58 +01:00
Mirabellensaft 30fe350443 polish 2023-03-17 19:31:07 +01:00
Mirabellensaft 9a354f2c29 last formatting and adding code snippets 2023-03-14 19:39:50 +01:00
Mirabellensaft 3b72b1b530 address issues from testing 2023-03-13 13:29:44 +01:00
Mirabellensaft 38c963e8b0 clarifications after test 2023-03-10 17:13:28 +01:00
Mirabellensaft 5b11a4de97 add generate docs instruction 2023-03-07 18:12:51 +01:00
Mirabellensaft 37930c3913 add uarte tasks 2023-03-07 18:08:33 +01:00
Mirabellensaft 59a08c6854 correct message 2023-03-07 17:53:42 +01:00
Mirabellensaft 71ad8b0b5a clarification 2023-03-07 17:52:41 +01:00
Mirabellensaft 3465441108 add comments and solution file 2023-03-07 17:08:38 +01:00
Mirabellensaft b2adf4a8d1 add todo 2023-03-07 16:59:07 +01:00
Mirabellensaft a1e990b1e9 Intro to testing 2023-03-07 16:58:17 +01:00
Mirabellensaft 373e791693 clean text 2023-03-07 16:46:03 +01:00
Mirabellensaft cd345aad95 mark code 2023-03-07 16:45:34 +01:00
Mirabellensaft 166550667b correct uarte peripheral 2023-02-27 17:06:04 +01:00
Mirabellensaft 16fbd1c39c rm button from example 2023-02-27 17:01:02 +01:00
Mirabellensaft a142d1579d finish draft of uarte implementation 2023-02-27 17:00:21 +01:00
Mirabellensaft 453eb95928 add most of exercise text 2023-02-22 19:11:41 +01:00
Mirabellensaft dffa5d483b add learning goals 2023-02-22 19:11:13 +01:00
Mirabellensaft bf87279d4a add uarte draft 2023-02-21 16:48:10 +01:00
Mirabellensaft c1825d1185 add exercise intro draft 2023-02-21 16:48:10 +01:00
Mirabellensaft 3afc29ca72 add step by step solution 2023-02-21 16:48:10 +01:00
Mirabellensaft 14d76fb10e add pages to book 2023-02-21 16:48:10 +01:00
Tanks Transfeld 25cd0faf70
Merge pull request #197 from ferrous-systems/minor_fixes
Minor_fixes
2023-02-21 16:46:36 +01:00
Mirabellensaft 1891f17253 rm deprecated lines 2023-02-21 16:45:43 +01:00
Mirabellensaft ad7f9db944 rm duplicate line 2023-02-21 16:41:51 +01:00
Mirabellensaft fd5179420f rm deny warnings 2023-02-21 16:39:47 +01:00
Mirabellensaft 8b31518a6f rm dead code, correct comments 2023-02-21 16:39:13 +01:00
Mirabellensaft f21124a304 change setup 2023-02-21 16:38:34 +01:00
Mirabellensaft 5f8e4b1223 correct dir name 2023-02-21 16:38:07 +01:00
Mirabellensaft 6529d5019c rename hal to nrf-hal for clarity 2023-02-21 16:37:40 +01:00
Mirabellensaft aa907e9f20 correct crate name 2023-02-21 13:20:04 +01:00
Mirabellensaft c4cc515666 rename folder 2023-02-20 18:47:54 +01:00
Mirabellensaft 9e46b5732e add template with gaps to fill 2023-02-20 18:25:48 +01:00
Mirabellensaft 923b850102 rn file to lib_solution 2023-02-20 18:25:21 +01:00
Mirabellensaft a9293e68a6 Cleaned up lib 2023-02-20 17:58:48 +01:00
Mirabellensaft 3f56cd66d8 mark expected exercise solution 2023-02-17 13:13:43 +01:00
Mirabellensaft 028a850596 add notes to bsc exercise 2023-02-14 19:04:36 +01:00
Mirabellensaft e98ddd23e5 new code examples for new exercise 2023-02-14 18:48:13 +01:00
Mirabellensaft eaab43590e new book pages 2023-02-14 18:46:54 +01:00
57 changed files with 4224 additions and 0 deletions

1
down-the-stack-book/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
book

View file

@ -0,0 +1,11 @@
# Down the Stack Slides
To read as an mdbook:
1. `cargo install mdbook`
2. `mdbook serve`
To read as a slide-show:
1. Download [marp-cli](https://github.com/marp-team/marp-cli/releases) and unpack into your `$PATH`
2. `marp -c marp.config.js -I . -s`

View file

@ -0,0 +1,16 @@
[book]
authors = ["Jonathan Pallant (Ferrous Systems)"]
language = "en"
multilingual = false
src = "src"
title = "Down the Stack"
[preprocessor]
[preprocessor.mermaid]
command = "mdbook-mermaid"
[output]
[output.html]
additional-js = ["mermaid.min.js", "mermaid-init.js"]

View file

@ -0,0 +1,6 @@
module.exports = {
inputDir: './slides',
engine: ({ marp }) => marp.use(require('@kazumatu981/markdown-it-kroki'), {
entrypoint: "https://kroki.io",
})
}

View file

@ -0,0 +1 @@
mermaid.initialize({startOnLoad:true});

1282
down-the-stack-book/mermaid.min.js vendored Normal file

File diff suppressed because one or more lines are too long

13
down-the-stack-book/node_modules/.package-lock.json generated vendored Normal file
View file

@ -0,0 +1,13 @@
{
"name": "down-the-stack-book",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@kazumatu981/markdown-it-kroki": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kazumatu981/markdown-it-kroki/-/markdown-it-kroki-1.1.1.tgz",
"integrity": "sha512-LDYl+mV2WogLQ5r4olxovm+gphL/MNGfWZ1M1woBO/YhFnfwdn5EAUu9zF/KoVZzytJPq0RNfyeDtMkv+GJihg==",
"dev": true
}
}
}

View file

@ -0,0 +1,17 @@
# CHANGELOG
## v1.1.1
* fix readme, see [this issue](https://github.com/kazumatu981/markdown-it-kroki/issues/1)
## v1.1.0
* Obsolated Option `marpAutoScaling` and detect automatically wether it is nessesury or not.
## v1.0.1
release on npm
## v1.0.0
create new

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Kazuyoshi Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,157 @@
# markdown-it-kroki
> This library was designed to embed [Kori.io](https://kroki.io/) diagram into [Marp](https://marp.app/) Slide-deks!!
This library is a pugin for markdown-it to embed figure is created by textual syntax.
To use this package, You can embed **Software Diagram** (like uml) is written by **code** in Marp Slides-deck.
See marp sample code.
## Sample
---
marp: true
---
## plantuml
```plantuml[platuml image]
@startuml
left to right direction
actor Guest as g
package Professional {
actor Chef as c
actor "Food Critic" as fc
}
package Restaurant {
usecase "Eat Food" as UC1
usecase "Pay for Food" as UC2
usecase "Drink" as UC3
usecase "Review" as UC4
}
fc --> UC4
g --> UC1
g --> UC2
g --> UC3
@enduml
```
![plantuml-sample](img/plantuml-sample.png)
If you want to write daigram, you write Diagram Language (like [plantuml](https://plantuml.com/), [mermaid.js](https://mermaid-js.github.io/mermaid/#/)) with in fenced code block.
## How to install
You can install `npm install` command like bellow.
```bash
npm install -D @kazumatu981/markdown-it-kroki
```
## How to use
Here is the how to use `markdow-it-kroki`.
This section introduce how to create Marp slides-deck project,
and introduce how to create Marp slides-deck server.
You can find deltail info in [here](https://marp.app/),
and you can learn about marp plugin eco-system, [here](https://marpit.marp.app/usage?id=extend-marpit-by-plugins).
### **[1st step]** Create Slides-deck project
First, for create slides-deck, you have to prepair to **Marp Project** directory.
So First, Create slides-deck project, and init npm package.
```bash
mkdir myslides
cd myslides
npm init -y
```
Secondary, Build Marp Environment.
Install [@marp-team/marp-cli](https://github.com/marp-team/marp-cli).
```bash
npm install -D @marp-team/marp-cli
```
> Off-course you can install as **global package** (like `npm install -g @marp-team/marp-cli`), or **run at-once** (like `npx`).
### **[2nd step]** Download this project and install
```bash
git clone https://github.com/kazumatu981/markdown-it-kroki.git
cd myslides
npm install -D path/to/markdown-it-kroki
```
### **[3rd step]** Create `marp.config.js`.
Here is the configuration file for **Marp**.
```javascript
module.exports = {
inputDir: './slides',
engine: ({ marp }) => marp.use(require('@kazumatu981/markdown-it-kroki'), {
entrypoint: "https://kroki.io",
})
}
```
### **[4th step]** Create your slides
On `slies` directory. you create slides-deck. like this.
---
marp: true
---
## mermaid
```mermaid[mermaid image]
flowchart TD
Start --> Stop
```
### **[5th step]** run server
Run marp server.
```bash
marp -s -c marp.config.js
```
## Detail
### Syntax of Markdown
#### Diagram Language
You have to write diagram language by [fenced code block](https://spec.commonmark.org/0.30/#fenced-code-blocks) syntax, start with **triple back quot** and after that the language.
```plantuml
This package depends on kroki.io.
If you want to know which is **supported diagram language**,
you will see in [Kroki.io official web site (https://kroki.io/)](https://kroki.io/).
#### Alt Text
You can write Alt-text attribute to IMG tag in HTML.
Write in `square blacket` after **Diagram Language**.
```mermaid [check your network config..]
### Options of `constructor`
| property-name | type | mean | defaul value |
| ---------------- | -------- | ------------------------------------------------------ | ------------------------- |
| `entrypoint` | `string` | The entry point for Kroki server. | `'https://kroki.io'` |
| `containerClass` | `string` | class name of container (`DIV`-tag `class` attribute). | `'kroki-image-container'` |
| `imageFormat` | `string` | image format of diagram. see [here](https://kroki.io/) | `'svg'` |

View file

@ -0,0 +1,8 @@
module.exports = {
inputDir: './',
engine: ({ marp }) => marp.use(require('../index'), {
entrypoint: "https://kroki.io",
marpAutoScaling: true
})
}

View file

@ -0,0 +1,59 @@
---
marp: true
---
# @kazumatu981/markdown-it-kroki
## Marp Sample
---
## plantuml
```plantuml[platuml image]
@startuml
left to right direction
actor Guest as g
package Professional {
actor Chef as c
actor "Food Critic" as fc
}
package Restaurant {
usecase "Eat Food" as UC1
usecase "Pay for Food" as UC2
usecase "Drink" as UC3
usecase "Review" as UC4
}
fc --> UC4
g --> UC1
g --> UC2
g --> UC3
@enduml
```
---
## mermaid
```mermaid[mermaid image]
graph TD
A[ Anyone ] -->|Can help | B( Go to github.com/yuzutech/kroki )
B --> C{ How to contribute? }
C --> D[ Reporting bugs ]
C --> E[ Sharing ideas ]
C --> F[ Advocating ]
```
---
## normal code
```JavaScript
function testFunc(test) {
let sum = 0;
for(let x = 1; x<=test; x++) {
sum += x;
}
return sum;
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,6 @@
const { MarkdownItKrokiCore } = require('./lib/plugin-core');
module.exports = (md, opt) => {
const plugin = new MarkdownItKrokiCore(md);
plugin.setOptions(opt).use();
};

View file

@ -0,0 +1,37 @@
'use strict';
module.exports = {
/**
* contract `test` to be non-empty string.
* @param {string} test test string
* @param {string} msg message on exception
*/
toNonEmptyString: function (test, msg) {
if (typeof test !== 'string') throw new Error(msg);
if (test === ''
|| test === null
|| test === undefined) throw new Error(msg);
},
/**
* contract `test` to be true.
* @param {boolean} test test boolean.
* @param {sting} msg massage on excetion.
*/
toTrue: function (test, msg) {
if (typeof test !== 'boolean') throw new Error(msg);
if (!test) throw new Error(msg);
},
toBeUrlString: function (test, msg) {
this.toNonEmptyString(test, msg);
try {
require('url').parse(test);
} catch {
throw new Error(msg);
}
},
toBeClassName: function (test, msg) {
if (!/^[A-Za-z0-9]+(-?[A-Za-z0-9]+)*$/.exec(test)) {
throw new Error(msg);
}
}
};

View file

@ -0,0 +1,27 @@
'use strict';
const { deflateSync } = require('zlib');
const contract = require('./contract');
const support = require('./support');
function encode(diagram) {
return deflateSync(diagram, { level: 9 }).toString('base64url');
}
function generateUrl(entrypoint, lang, imgType, diagram) {
contract.toNonEmptyString(entrypoint, '\'entrypoint\' must be non-empty string.');
contract.toNonEmptyString(lang, '\'lang\' must be non-empty string.');
contract.toNonEmptyString(imgType, '\'imgType\' must be non-empty string.');
contract.toNonEmptyString(diagram, '\'diagram\' must be non-empty string.');
contract.toTrue(support.languageSupports(lang), 'Not Supported Diagram Language.');
contract.toTrue(support.imageFormatSupports(imgType), 'Not Supported Image Type.');
const api = `${lang}/${imgType}/${encode(diagram)}`;
return entrypoint.endsWith('/') ?
`${entrypoint}${api}` : `${entrypoint}/${api}`;
}
module.exports = {
encode, generateUrl
};

View file

@ -0,0 +1,52 @@
const { deflateSync } = require('zlib')
const krokiLangs = [
'actdiag',
'blockdiag',
'bpmn',
'bytefield',
'c4plantuml',
'ditaa',
'dot',
'erd',
'excalidraw',
'graphviz',
'mermaid',
'nomnoml',
'nwdiag',
'packetdiag',
'pikchr',
'plantuml',
'rackdiag',
'seqdiag',
'svgbob',
'umlet',
'vega',
'vegalite',
'wavedrom',
]
const entrypoint = 'https://kroki.io/'
const marpKrokiPlugin = (md) => {
const { fence } = md.renderer.rules
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const info = md.utils.unescapeAll(tokens[idx].info).trim()
if (info) {
const [lang] = info.split(/(\s+)/g)
if (krokiLangs.includes(lang)) {
const data = deflateSync(tokens[idx].content).toString('base64url')
// <marp-auto-scaling> is working only with Marp Core v3
return `<p><marp-auto-scaling data-downscale-only><img src="${entrypoint}${lang}/svg/${data}"/></marp-auto-scaling></p>`
}
}
return fence.call(self, tokens, idx, options, env, self)
}
}
module.exports = marpKrokiPlugin

View file

@ -0,0 +1,74 @@
'use strict';
const support = require('./support');
const contract = require('./contract');
const { safeProperty, safeChoice } = require('./safe-property');
const diagramEncoder = require('./diagram-encoder');
class MarkdownItKrokiCore {
constructor(md) {
this._md = md;
}
setOptions(opt) {
this._entrypoint = safeProperty(opt, "entrypoint", "string", 'https://kroki.io');
this._containerClass = safeProperty(opt, "containerClass", "string", "kroki-image-container");
this._imageFormat = safeProperty(opt, "imageFormat", "string", "svg");
this._imageFormat = safeChoice(this._imageFormat, support.imageFormats, "svg");
this._entrypoint = require('url').parse(this._entrypoint).href;
contract.toBeClassName(this._containerClass, "containerClass must be className.");
return this;
}
use() {
// if _md has `marpit` property then use <marp-auto-scaling> tag
this._marpAutoScaling = this._md['marpit'] !== undefined;
this._defaultFence = this._md.renderer.rules.fence;
this._md.renderer.rules.fence
= (tokens, idx, options, env, self) => this.krokiFencePlugin(tokens, idx, options, env, self);
}
static readLanguageAndAltText(info) {
if (!info) return { language: '', alt: '' };
const trimed = info.trim();
const langFound = /[\s|\[]/.exec(trimed);
const altFound = /\[.*?\]/.exec(trimed);
return {
language: langFound ?
trimed.substring(0, langFound.index) : trimed,
alt: altFound ?
altFound[0].replace('[', '').replace(']', '') : ''
};
}
buildEmbedHTML(langAndAlt, diagramCode) {
// alt build url
const url = diagramEncoder.generateUrl(
this._entrypoint, langAndAlt.language, this._imageFormat, diagramCode);
// sanitize alt
const alt = langAndAlt.alt ?
this._md.utils.escapeHtml(langAndAlt.alt) : undefined;
// build img tag
const imgTag = langAndAlt.alt ?
`<img alt="${alt}" src="${url}" />` : `<img src="${url}" />`;
// build container contents
const containerContents = this._marpAutoScaling ?
`<marp-auto-scaling data-downscale-only>${imgTag}</marp-auto-scaling>` : imgTag;
// build embed HTML
return `<p class="${this._containerClass}">${containerContents}</p>`;
}
krokiFencePlugin(tokens, idx, options, env, self) {
const info = this._md.utils.unescapeAll(tokens[idx].info)
const langAndAlt = MarkdownItKrokiCore.readLanguageAndAltText(info);
return support.languageSupports(langAndAlt.language) ?
this.buildEmbedHTML(langAndAlt, tokens[idx].content) :
this._defaultFence.call(self, tokens, idx, options, env, self);
}
}
module.exports = {
MarkdownItKrokiCore
}

View file

@ -0,0 +1,18 @@
'use strict';
function safeProperty(test, name, type, defaultValue) {
if (test == null || test == undefined) return defaultValue;
if (typeof test[name] !== type) return defaultValue;
if (typeof test[name] === "string" && test[name] === '') return defaultValue;
return test[name];
}
function safeChoice(test, candidates, defaultValue) {
return candidates.includes(test) ?
test : defaultValue;
}
function safeUrl(test) {
}
module.exports = { safeProperty, safeChoice };

View file

@ -0,0 +1,54 @@
'use strict';
/**
* Diagram Languages are supported by kroki.io
*/
const LANGUAGES = [
'actdiag',
'blockdiag',
'bpmn',
'bytefield',
'c4plantuml',
'ditaa',
'dot',
'erd',
'excalidraw',
'graphviz',
'mermaid',
'nomnoml',
'nwdiag',
'packetdiag',
'pikchr',
'plantuml',
'rackdiag',
'seqdiag',
'svgbob',
'umlet',
'vega',
'vegalite',
'wavedrom',
];
/**
* Image formats are supported by kroki.io
*/
const IMG_FORMATS = [
'png', 'svg', 'jpeg', 'pdf', 'base64'
];
module.exports = {
lnaguages: LANGUAGES,
imageFormats: IMG_FORMATS,
/**
* test whether `lang` is supported diagram language by kroki.io
* @param {string} lang target language
* @returns is supported
*/
languageSupports: (lang) => LANGUAGES.includes(lang),
/**
* test whether `format` is supported image format by kroki.io
* @param {string} format name of image format like 'png', 'svg', ... etc
* @returns is supported
*/
imageFormatSupports: (format) => IMG_FORMATS.includes(format)
};

View file

@ -0,0 +1,35 @@
{
"name": "@kazumatu981/markdown-it-kroki",
"version": "1.1.1",
"description": "markdown-it kroki plugin.",
"main": "index.js",
"scripts": {
"test": "mocha tests/**/*.test.js",
"demo": "marp -s -c demo/marp.config.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kazumatu981/markdown-it-kroki.git"
},
"keywords": [
"markdown-it",
"markdown-it-plugin",
"kroki",
"marp",
"markdown"
],
"author": "kazumatu981",
"license": "MIT",
"bugs": {
"url": "https://github.com/kazumatu981/markdown-it-kroki/issues"
},
"homepage": "https://github.com/kazumatu981/markdown-it-kroki#readme",
"devDependencies": {
"@marp-team/marp-cli": "^2.2.0",
"chai": "^4.3.6",
"jsdom": "^20.0.1",
"markdown-it": "^13.0.1",
"mocha": "^10.1.0",
"nyc": "^15.1.0"
}
}

View file

@ -0,0 +1,94 @@
const MarkdownIt = require('markdown-it');
const { expect } = require('chai');
const { JSDOM } = require('jsdom');
const MarkdownItKroki = require('../../index');
describe('# [Security-test] anti-injecttion for syntax.', () => {
describe("## for alt", () => {
it('* escape double quote', () => {
const expected = 'this is a "test comment" test';
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "the-container"
});
const result = md.render(
'```graphviz [this is a "test comment" test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(result);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
const actual = imgTag.getAttribute('alt');
expect(actual).to.be.equal(expected);
})
})
});
describe('# [Security-test] anti-injecttion for option.', () => {
describe("## for entrypoint", () => {
it('* deny invalid URL', () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io\"> <script src=\"xxxx.js",
marpAutoScaling: true,
containerClass: "the-container"
});
const html = md.render(
'```graphviz [this is a test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(html);
const scriptTag = dom.window.document.getElementsByTagName("script");
expect(scriptTag.length).to.equal(0);
})
})
describe("## for containerClass", () => {
it('* throw when containerClass are not alpha, digit, or \"-\"', () => {
const testFunction = () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "<container>"
});
const result = md.render(
'```graphviz [this is a test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
};
expect(testFunction).to.throw();
})
})
describe("## for imageFormat", () => {
it('* ignore invalid imageFormat', () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "the-container",
imageFormat: "<injected>"
});
const html = md.render(
'```graphviz [this is a test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
const actual = imgTag.getAttribute('src');
expect(actual).to.includes('/svg/');
})
})
});

View file

@ -0,0 +1,96 @@
const { expect } = require('chai');
const MarkdownIt = require('markdown-it');
const MarkdownItKroki = require('../../index');
const { JSDOM } = require('jsdom');
const testData = [
// graphviz
{
langname: 'graphviz',
data: '```graphviz svg\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
}
];
describe('# [total test] Test pulugin can Render DOM', () => {
for (const test of testData) {
describe(`## Test for ${test.langname}`, () => {
describe('### no option call test', () => {
it('* Not to Throw on no options', () => {
const testFunction = () => {
// render !
const md = new MarkdownIt();
md.use(MarkdownItKroki);
const _ = md.render(test.data);
};
expect(testFunction).to.not.throw();
});
it('* root DOM item is \'p\' on no option', () => {
// render !
const md = new MarkdownIt();
md.use(MarkdownItKroki);
const result = md.render(test.data);
// find p-tag
const dom = new JSDOM(result);
const document = dom.window.document;
const ptags = document.getElementsByTagName("p");
// test p-tag is only one
expect(ptags.length).to.be.equal(1);
// test root item is p
const thePtag = ptags[0];
expect(thePtag.isSameNode(document.body.firstChild)).to.true;
// test embeded default container class
expect(thePtag.getAttribute('class')).to.be.equal('kroki-image-container');
});
it('* has img tag and source is created by this library.', () => {
// render !
const md = new MarkdownIt();
md.use(MarkdownItKroki);
const result = md.render(test.data);
// find p-tag
const dom = new JSDOM(result);
const document = dom.window.document;
const imgTags = document.getElementsByTagName("img");
// test img-tag is only one
expect(imgTags.length).to.be.equal(1);
const imgTag = imgTags[0];
expect(imgTag.getAttribute('src')).not.to.empty;
})
});
})
}
})
describe('[total test] Can Render', () => {
it('* option constainerClass is embeded.', () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "the-container"
});
var result = md.render(
'```graphviz [praphviz-image]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(result);
const document = dom.window.document;
const ptags = document.getElementsByTagName("p");
expect(ptags.length).to.be.equal(1);
const thePtag = ptags[0];
expect(thePtag.isSameNode(document.body.firstChild)).to.true;
expect(thePtag.getAttribute('class')).to.be.equal('the-container');
});
});

View file

@ -0,0 +1,35 @@
const expect = require('chai').expect;
const contract = require('../../lib/contract');
describe('# [unit test]: contract.js', () => {
describe('## toNonEmptyString()', () => {
['abc', ' ', ' abc '].forEach((test) => {
it(`* normal cases. { testcase: ${test} }`, () => {
expect(() => {
contract.toNonEmptyString(test);
}).not.to.throw();
})
});
['', null, undefined, 123, 0.123, true, { test: 123 }].forEach((test) => {
it(`* abnormal cases. { testcase: ${test} }`, () => {
expect(() => {
contract.toNonEmptyString(test);
}).to.throw();
})
});
});
describe('## toTrue', () => {
it('* normal case. {test case: true}', () => {
expect(() => {
contract.toTrue(true);
}).not.to.throw();
});
['', null, undefined, 123, 0.123, false, { test: 123 }].forEach((test) => {
it(`* abnormal cases. { testcase: ${test} }`, () => {
expect(() => {
contract.toTrue(test);
}).to.throw();
})
});
});
})

View file

@ -0,0 +1,75 @@
const expect = require('chai').expect;
const { inflateSync } = require('zlib');
const { encode, generateUrl } = require('../../lib/diagram-encoder');
describe('# [unit-test]: diagram-encoder.js', () => {
describe('## [function]: encode()', () => {
it('* encoded data is able to decode.', () => {
const testFunc = () => {
const expected = '@startuml\nBob -> Alice : hello\n@enduml';
const encoded = encode(expected);
const deflated = Buffer.from(encoded, "base64url");
const actual = inflateSync(deflated).toString();
expect(actual).to.be.equal(expected);
}
expect(testFunc).not.to.Throw();
});
});
describe('## [function]: generateUrl()', () => {
it('* must start format like <entry-point>/<lang>/<format>/', () => {
const actual = generateUrl('https://kroki.io', 'graphviz', 'svg', 'digraph G {Hello->World}');
const expected = 'https://kroki.io/graphviz/svg/';
expect(actual).to.be.a('string');
expect(actual.startsWith(expected)).to.be.true;
});
it('* must endwith <encoded>', () => {
const actual = generateUrl('https://kroki.io', 'graphviz', 'svg', 'digraph G {Hello->World}');
const expected = encode('digraph G {Hello->World}');
expect(actual).to.be.a('string');
expect(actual.endsWith(expected)).to.be.true;
});
[1, '', null, undefined].forEach(test => {
it(`* [exception] throws when entry-point is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl(test, 'graphviz', 'svg', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it(`* [exception] throws when lang is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', test, 'svg', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it(`* [exception] throws when imgType is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz', test, 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it(`* [exception] throws when diagram is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz', 'svg', diagram);
}
expect(testFunction).throw();
});
});
it('* [exception] throws when lang is unsupported lang', () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz123', 'svg', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it('* [exception] throws when imgType is unsupported imgType', () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz', 'svg123', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
});
});

View file

@ -0,0 +1,146 @@
const md = require('markdown-it');
const expect = require('chai').expect;
const { JSDOM } = require('jsdom');
const { MarkdownItKrokiCore } = require('../../lib/plugin-core');
const { encode } = require('../../lib/diagram-encoder');
describe('# [unit-test] plugin-core.js', () => {
describe('## method: buuildEmbedHTML', () => {
describe('### langAndAlt.language', () => {
[null, undefined, ''].forEach((test) => {
it(`* when langAndAlt.language is null or empty, throws error. testcase:${test}`, () => {
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
const testFunc = () => {
plugin.use();
const _ = plugin.buildEmbedHTML(
{ language: test, alt: '' }, diagramCode);
};
expect(testFunc).to.throw();
});
});
it('* language embeded in to url', () => {
const test = 'plantuml';
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// build embed HTML
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: test, alt: '' }, diagramCode);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(/\/plantuml\//.test(url)).to.true;
});
});
describe('### langAndAlt.alt', () => {
[null, undefined, ''].forEach((test) => {
it(`* when langAndAlt.alt is null or empty, no alt attribute. testcase:${test}`, () => {
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// prepair
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
// render
const html = plugin.buildEmbedHTML(
{ language: 'plantuml', alt: test }, diagramCode);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
expect(imgTag.hasAttribute('alt')).to.false;
});
});
it('* embeded altText', () => {
const expected = "this is test Text";
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// prepair
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
// render
const html = plugin.buildEmbedHTML(
{ language: 'plantuml', alt: expected }, diagramCode);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
expect(imgTag.getAttribute('alt')).to.equal(expected);
});
});
describe('### diagramCode', () => {
[null, undefined, ''].forEach((test) => {
it(`* when diagramCode is null or empty, throws error. testcase:${test}`, () => {
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
const testFunc = () => {
plugin.use();
const _ = plugin.buildEmbedHTML(
{ language: 'plantuml', alt: '' }, test);
};
expect(testFunc).to.throw();
});
});
it('* encoded diagram must be embed to url on <img src=\'....\' ', () => {
const test = '@startuml\nBob -> Alice : hello\n @enduml';
const expected = encode(test);
// build embed HTML
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: 'plantuml', alt: '' }, test);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(url.endsWith(expected)).to.true;
});
it('* <img> is surounded by <marp-auto-scaling> on to be used form marp-it', () => {
const test = '@startuml\nBob -> Alice : hello\n @enduml';
const markdownIt = new md()
markdownIt['marpit'] = { someObject: 'is implemented' };
// build embed HTML
const plugin = new MarkdownItKrokiCore(markdownIt).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: 'plantuml', alt: '' }, test);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
const marpAutoScaling = dom.window.document.getElementsByTagName("marp-auto-scaling")[0];
expect(imgTag.isSameNode(marpAutoScaling.firstChild)).to.be.true;
});
it('* <img> is surounded by <marp-auto-scaling> on not to be used form marp-it', () => {
const test = '@startuml\nBob -> Alice : hello\n @enduml';
const markdownIt = new md()
// build embed HTML
const plugin = new MarkdownItKrokiCore(markdownIt).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: 'plantuml', alt: '' }, test);
// parse dom
const dom = new JSDOM(html);
const marpAutoScaling = dom.window.document.getElementsByTagName("marp-auto-scaling");
expect(marpAutoScaling.length).to.equal(0);
})
});
});
});

View file

@ -0,0 +1,178 @@
const md = require('markdown-it');
const expect = require('chai').expect;
const { JSDOM } = require('jsdom');
const { MarkdownItKrokiCore } = require('../../lib/plugin-core');
describe('# [unit-test] plugin-core.js', () => {
describe('## method: setOptions() must be work', () => {
function buildHtmlForTest(options) {
const test = 'plantuml';
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// build embed HTML
const plugin = new MarkdownItKrokiCore(new md()).setOptions(options);
plugin.use();
return plugin.buildEmbedHTML({ language: test }, diagramCode);
}
describe('### entrypoint', () => {
function expectEntryPointToEmbed(htmlString, expected) {
if (!expected) expected = 'https://kroki.io';
// parse dom
const dom = new JSDOM(htmlString);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(url.startsWith(expected)).to.true;
}
it('* no options', () => {
const html = buildHtmlForTest();
expectEntryPointToEmbed(html);
});
it('* option is null', () => {
const html = buildHtmlForTest({ entrypoint: null });
expectEntryPointToEmbed(html);
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ entrypoint: undefined });
expectEntryPointToEmbed(html);
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ entrypoint: '' });
expectEntryPointToEmbed(html);
});
it('* option is 1', () => {
const html = buildHtmlForTest({ entrypoint: 1 });
expectEntryPointToEmbed(html);
});
it('* option is true', () => {
const html = buildHtmlForTest({ entrypoint: true });
expectEntryPointToEmbed(html);
});
it('* option is \'https://localhost:8080\'', () => {
const html = buildHtmlForTest({
entrypoint: 'https://localhost:8080'
});
expectEntryPointToEmbed(html, 'https://localhost:8080');
});
});
describe.skip('### marpAutoScaling', () => {
function expectMarpAutoScalingToEmbed(htmlString, expected) {
// parse dom
const dom = new JSDOM(htmlString);
const tags = dom.window.document.getElementsByTagName("marp-auto-scaling");
if (expected) {
expect(tags).not.to.empty;
} else {
expect(tags).to.empty;
}
}
it('* no options', () => {
const html = buildHtmlForTest();
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is null', () => {
const html = buildHtmlForTest({ marpAutoScaling: null });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ marpAutoScaling: undefined });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ marpAutoScaling: '' });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is 1', () => {
const html = buildHtmlForTest({ marpAutoScaling: 1 });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is \'test\'', () => {
const html = buildHtmlForTest({ marpAutoScaling: 'test' });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is false', () => {
const html = buildHtmlForTest({ marpAutoScaling: false });
expectMarpAutoScalingToEmbed(html, false);
});
})
describe('### containerClass', () => {
function expectContainerClassToEmbed(htmlString, className) {
// parse dom
const dom = new JSDOM(htmlString);
const pTag = dom.window.document.getElementsByTagName("p")[0];
const actualClassName = pTag.getAttribute('class');
expect(actualClassName).to.equal(className);
}
it('* no options', () => {
const html = buildHtmlForTest();
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is null', () => {
const html = buildHtmlForTest({ containerClass: null });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ containerClass: undefined });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ containerClass: '' });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is 1', () => {
const html = buildHtmlForTest({ containerClass: 1 });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is \'containerClass\'', () => {
const html = buildHtmlForTest({ containerClass: 'containerClass' });
expectContainerClassToEmbed(html, 'containerClass');
});
});
describe('### imageFormat', () => {
function expectImageFormatToEmbed(htmlString, expected) {
// parse dom
const dom = new JSDOM(htmlString);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(url).to.includes('/' + expected + '/');
}
it('* no options', () => {
const html = buildHtmlForTest();
expectImageFormatToEmbed(html, 'svg');
});
it('* option is null', () => {
const html = buildHtmlForTest({ imageFormat: null });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ imageFormat: undefined });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ imageFormat: '' });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is 1', () => {
const html = buildHtmlForTest({ imageFormat: 1 });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is \'test\'', () => {
const html = buildHtmlForTest({ imageFormat: 'test' });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is \'png\'', () => {
const html = buildHtmlForTest({ imageFormat: 'png' });
expectImageFormatToEmbed(html, 'png');
});
});
});
});

View file

@ -0,0 +1,49 @@
const expect = require('chai').expect;
const { MarkdownItKrokiCore } = require('../../lib/plugin-core');
describe('# [unit-test] plugin-core.js', () => {
describe('## static method: readLanguageAndAltText() - language', () => {
[
{ test: null, expected: '' },
{ test: undefined, expected: '' },
{ test: '', expected: '' },
{ test: ' ', expected: '' },
{ test: 'plantuml', expected: 'plantuml' },
{ test: ' plantuml', expected: 'plantuml' },
{ test: 'plantuml ', expected: 'plantuml' },
{ test: 'plantuml +++', expected: 'plantuml' },
{ test: 'html+md', expected: 'html+md' },
{ test: 'graphviz[]', expected: 'graphviz' },
{ test: 'graphviz[test]', expected: 'graphviz' },
{ test: 'graphviz [test test]', expected: 'graphviz' },
].forEach(testCase => {
it(`### Can read diagramLanguage. in case \'${testCase.test}\'`, () => {
const actual = MarkdownItKrokiCore.readLanguageAndAltText(testCase.test);
const expected = testCase.expected;
expect(actual.language).to.be.equal(expected);
})
});
});
describe('## static method: readLanguageAndAltText() - alt', () => {
[
{ test: null, expected: '' },
{ test: undefined, expected: '' },
{ test: '', expected: '' },
{ test: ' ', expected: '' },
{ test: 'plantuml', expected: '' },
{ test: ' plantuml', expected: '' },
{ test: 'plantuml ', expected: '' },
{ test: 'plantuml +++', expected: '' },
{ test: 'html+md', expected: '' },
{ test: 'graphviz[]', expected: '' },
{ test: 'graphviz[test]', expected: 'test' },
{ test: 'graphviz [test test]', expected: 'test test' },
].forEach(testCase => {
it(`### Can read diagramLanguage. in case \'${testCase.test}\'`, () => {
const actual = MarkdownItKrokiCore.readLanguageAndAltText(testCase.test);
const expected = testCase.expected;
expect(actual.alt).to.be.equal(expected);
})
});
});
});

View file

@ -0,0 +1,120 @@
const { expect } = require('chai');
const { safeProperty } = require('../../lib/safe-property');
describe('# [unit-test]: safe-property.js', () => {
[
{
testCaseDescription: "standard test - string",
testCase: {
test: {
property1: "hello"
},
name: "property1",
type: "string",
defaultValue: undefined
},
expected: "hello"
},
{
testCaseDescription: "standard test - boolean",
testCase: {
test: {
property1: true
},
name: "property1",
type: "boolean",
defaultValue: undefined
},
expected: true
},
{
testCaseDescription: "standard test - boolean on null",
testCase: {
test: {
property1: null
},
name: "property1",
type: "boolean",
defaultValue: false
},
expected: false
},
{
testCaseDescription: "on null",
testCase: {
test: {
property1: null
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on empty string",
testCase: {
test: {
property1: ''
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on undefined",
testCase: {
test: {
property1: undefined
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on not mutch type",
testCase: {
test: {
property1: 1
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on object is null",
testCase: {
test: null,
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on object is undefined",
testCase: {
test: undefined,
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
].forEach((testItem) => {
it(`* ${testItem.testCaseDescription}`, () => {
const actual = safeProperty(
testItem.testCase.test,
testItem.testCase.name,
testItem.testCase.type,
testItem.testCase.defaultValue);
expect(actual).to.equal(testItem.expected);
});
})
})

18
down-the-stack-book/package-lock.json generated Normal file
View file

@ -0,0 +1,18 @@
{
"name": "down-the-stack-book",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@kazumatu981/markdown-it-kroki": "^1.1.1"
}
},
"node_modules/@kazumatu981/markdown-it-kroki": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kazumatu981/markdown-it-kroki/-/markdown-it-kroki-1.1.1.tgz",
"integrity": "sha512-LDYl+mV2WogLQ5r4olxovm+gphL/MNGfWZ1M1woBO/YhFnfwdn5EAUu9zF/KoVZzytJPq0RNfyeDtMkv+GJihg==",
"dev": true
}
}
}

View file

@ -0,0 +1,5 @@
{
"devDependencies": {
"@kazumatu981/markdown-it-kroki": "^1.1.1"
}
}

View file

@ -0,0 +1,10 @@
# Summary
- [Introduction](./introduction.md)
- [The Peripheral Access Crate](./the_pac.md)
- [The Hardware Abstraction Layer](./the_hal.md)
- [Building Common Abstractions](./building_common_abstractions.md)
- [Creating Portable Drivers](./creating_portable_drivers.md)
- [Supporting your particular board](./supporting_your_particular_board.md)
- [Writing an Application](./writing_an_application.md)

View file

@ -0,0 +1 @@
# Building Common Abstractions

View file

@ -0,0 +1 @@
# Creating Portable Drivers

View file

@ -0,0 +1,84 @@
# Introduction
---
## A Layered Approach
When building Embedded Systems in Rust, we use Rust crates to help us build a modular system.
The elements are:
* The program you are writing
* The MCU are running on
* The PCB (or Board) your MCU is on
* The external devices connected to your MCU
---
## The Layers
To support these elements, we (usually) have these layers.
* Application
* Board Support
* External Drivers (e.g. SPI LCD Driver)
* Hardware Abstraction Layer Traits
* MCU Hardware Abstraction Layer Implementation
* MCU Peripheral Access Crate
* Core Peripherals
* Core Runtime
---
```mermaid
graph TB
app(Application<br/><tt>my_application</tt>)
bsc[Board Support<br/><tt>nrf52840_dk</tt>]
hal[MCU HAL Implementation<br/><tt>nrf52480_hal</tt>]
lcd_driver[SPI LCD Driver<br/><tt>ssd1306</tt>]
hal_traits[[HAL Traits<br/><tt>embedded_hal</tt>]]
pac[MCU PAC<br/><tt>nrf52840</tt>]
rt[Core Runtime<br/><tt>cortex_m_rt</tt>]
cp[Core Peripherals<br/><tt>cortex_m</tt>]
subgraph Key
note1[Embedded Working Group]
note2[nrf-rs]
note3[You]
note4[Others]
end
direction TB
app --> bsc
app & bsc --> hal
app --> lcd_driver
app & lcd_driver --> hal_traits
hal -- Implements --o hal_traits
app & hal --> pac
app & pac --> rt
app & pac & rt --> cp
class app binary;
class bsc library;
class lcd_driver library;
class hal mcu_library;
class pac mcu_library;
class hal_traits ewg_library;
class rt ewg_library;
class cp ewg_library;
class note1 ewg_library;
class note2 mcu_library;
class note3 binary;
class note4 library;
classDef binary fill:#fb8,stroke:#333,stroke-width:4px;
classDef library fill:#cf9,stroke:#333,stroke-width:2px;
classDef ewg_library fill:#f9c,stroke:#333,stroke-width:2px;
classDef mcu_library fill:#9cf,stroke:#333,stroke-width:2px;
```
---
## Don't worry!
There's a lot here. We're going to take it step by step, starting at the bottom.

View file

@ -0,0 +1 @@
# Supporting your particular board

View file

@ -0,0 +1 @@
# The Hardware Abstraction Layer

View file

@ -0,0 +1,206 @@
# The Peripheral Access Crate
---
## Introduction
This crate sits at the bottom of the 'stack'. It provides access to the memory-mapped peripherals in your MCU.
---
## Memory Mapped Peripherals
* e.g. a UART peripheral
* Has registers, represented by a memory address
* Registers are usually consecutive in memory (not always)
* Peripherals can have instances (same layout of registers, different start address)
* UART0, UART1, etc
---
## Registers
* *Registers* are comprised of one or more *fields*.
* Each field is at least 1 bit in length.
* Sometimes fields can only take from a limited set of values
* This is all in your datasheet!
---
## C Code!
Embedded Code in C often uses shifts and bitwise-AND to make up registers from fields.
```c
#define UARTE_INTEN_CTS_SHIFT (0)
#define UARTE_INTEN_CTS_MASK (0x00000001)
#define UARTE_INTEN_RXRDY_SHIFT (2)
#define UARTE_INTEN_RXRDY_MASK (0x00000001)
// The other nine fields are skipped for brevity
uint32_t cts = 0;
uint32_t rxrdy = 1;
uint32_t inten_value = ((cts & UARTE_INTEN_CTS_MASK) << UARTE_INTEN_CTS_SHIFT)
| ((rxrdy & UARTE_INTEN_RXRDY_MASK) << UARTE_INTEN_RXRDY_SHIFT);
*((volatile uint32_t*) 0x40002300) = inten_value;
```
---
## Rust Code
You *could* do this in Rust if you wanted...
```rust
const UARTE0_INTEN: *mut u32 = 0x4000_2300 as *mut u32;
unsafe { UARTE0_INTEN.write_volatile(0x0000_0003); }
```
But this still seems very error-prone. Nothing stops you putting the wrong value at the wrong address.
---
## Adding structure
In C, the various registers for a peripheral can also be grouped into a `struct`
```c
typedef volatile struct uart0_reg_t {
uint32_t tasks_startrx; // @ 0x000
uint32_t tasks_stoprx; // @ 0x004
// ...
uint32_t inten; // @ 0x300
uint32_t _padding[79];
uint32_t baudrate; // @ 0x500
} uart0_reg_t
uart0_reg_t* const p_uart = (uart0_reg_t*) 0x40002000;
```
---
## Structures in Rust
We can do that too (and this is how our PAC works under the hood).
```rust
pub struct RegisterBlock {
pub tasks_startrx: VolatileCell<u32>, // @ 0x000
pub tasks_stoprx: VolatileCell<u32>, // @ 0x004
// ...
pub inten: VolatileCell<u32>, // @ 0x300
_reserved12: [u32; 79],
pub baudrate: VolatileCell<u32>, // @ 0x500
}
let p_uart: &RegisterBlock = unsafe { &*(0x40002000 as *const RegisterBlock) };
```
We use the [`VolatileCell`](https://docs.rs/vcell/0.1.3/vcell/struct.VolatileCell.html) to ensure reads/writes on the structure fields are always performed with volatile pointer read/writes.
---
## CMSIS-SVD Files
A CMSIS-SVD (or just SVD) file is an XML description of all the peripherals, registers and fields on an MCU.
We can use `svd2rust` to turn this into a Peripheral Access Crate.
```mermaid
graph LR
svd[(SVD XML)] --> svd2rust[<tt>svd2rust</tt>] --> rust[(Rust Source)]
```
---
## The `svd2rust` generated API
```mermaid
graph TB
Peripherals --> uarte1[.UARTE1: <b>UARTE1</b>]
uarte1 --> uart1_baudrate[.baudrate: <b>BAUDRATE</b>]
uarte1 --> uart1_inten[.inten: <b>INTEN</b>]
Peripherals --> uarte2[.UARTE2: <b>UARTE2</b>]
uarte2 --> uart2_baudrate[.baudrate: <b>BAUDRATE</b>]
uarte2 --> uart2_inten[.inten: <b>INTEN</b>]
```
* The crate has a top-level `struct Peripherals` with members for each *Peripheral*
* Each *Peripheral* gets a `struct`, like `UARTE0`, `SPI1`, etc.
* Each *Peripheral* `struct` has members for each *Register*
* Each *Register* gets a `struct`, like `BAUDRATE`, `INTEN`, etc.
* Each *Register* `struct` has `read()`, `write()` and `modify()` methods
---
## The `svd2rust` generated API (2)
* The `read()` method returns a special proxy object, with methods for each *Field*
* The `write()` method takes a closure, which is given a special 'proxy' object, with methods for each *Field*
* All the *Field* changes are batched together and written in one go
* Any un-written *Fields* are set to a default value
* The `modify()` method gives you both
* Any un-written *Fields* are left alone
---
## Using a PAC
```rust
// nrf52840 is the PAC
let p = nrf52840::Peripherals::take().unwrap();
// Reading the 'baudrate' field
let current_baud_rate = p.UARTE1.baudrate.read().baudrate();
// Modifying multiple fields in one go
p.UARTE1.inten.modify(|_r, w| {
w.cts().enabled();
w.ncts().enabled();
w.rxrdy().enabled();
w
});
```
---
## Wait, what's a closure?
* It's an anonymous function, declared in-line with your other code
* It can 'capture' local variables (although we don't use that feature here)
* It enables a very powerful Rust idiom, that you can't easily do in C...
---
## Let's take it in turns
- I, the callee, need to set some stuff up
- You, the caller, need to do a bit of work
- I, the callee, need to clean everything up
We can use a closure to insert the caller-provided code in the middle of our function. We see this used [all (1)](https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.map) [over (2)](https://doc.rust-lang.org/core/primitive.str.html#method.matches) [the (3)](https://doc.rust-lang.org/std/thread/fn.spawn.html) Rust standard library!
---
## You tell me...
What are the three steps here?
```rust
p.UARTE1.inten.modify(|_r, w| {
w.cts().enabled();
w.ncts().enabled();
w.rxrdy().enabled();
w
});
```
---
## Documentation
Docs can be generated from the source code.
See <https://docs.rs/nrf52840>
Note that `uarte0` is a *module* and `UARTE0` could mean either a `struct` type, or a field on the `Peripherals` struct.

View file

@ -0,0 +1 @@
# Writing an Application

View file

@ -0,0 +1,19 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# (..)
rustflags = [
"-C", "linker=flip-link", # adds stack overflow protection
"-C", "link-arg=-Tdefmt.x", # defmt support
# (..)
]
[target.thumbv7em-none-eabihf]
# set custom cargo runner to flash & run on embedded target when we call `cargo run`
# for more information, check out https://github.com/knurling-rs/probe-run
runner = "probe-run --chip nRF52840_xxAA"
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
# cross-compile to this target
target = "thumbv7em-none-eabihf" # = ARM Cortex-M4

View file

@ -0,0 +1,48 @@
[package]
authors = ["Tanks Transfeld <tanks.transfeld@ferrous-systems.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
name = "apps"
version = "0.0.0"
[dependencies]
cortex-m = {version = "0.7.6", features = ["critical-section-single-core"]}
cortex-m-rt = "0.7.2"
dk_bsc = { path = "../dk_bsc" }
heapless = "0.7.16"
panic-probe = { version = "0.3.0", features = ["print-defmt"] }
defmt = "0.3.2"
defmt-rtt = "0.3.2"
# optimize code in both profiles
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # !
incremental = false
lto = "fat"
opt-level = 'z' # !
overflow-checks = false
[profile.release]
codegen-units = 1
debug = 1
debug-assertions = false
incremental = false
lto = "fat"
opt-level = 3
overflow-checks = false
[features]
default = [
"other-feature"
]
other-feature = []
# do NOT modify these features
defmt-default = []
defmt-trace = []
defmt-debug = []
defmt-info = []
defmt-warn = []
defmt-error = []

View file

@ -0,0 +1,31 @@
#![no_main]
#![no_std]
use cortex_m_rt::entry;
// this imports `down-the-stack/apps/lib.rs` to retrieve our global logger + panicking-behavior
use apps as _;
// ⚠️ ⚠️ ⚠️ Don't change this file! ⚠️ ⚠️ ⚠️
#[entry]
fn main() -> ! {
// to enable more verbose logs, go to your `Cargo.toml` and set defmt logging levels
// to `defmt-trace` by changing the `default = []` entry in `[features]`
let board = dk_bsc::init().unwrap();
let mut led = board.leds;
let button_1 = board.buttons.b_1;
loop {
if button_1.is_pushed() {
led.led_1.on();
} else {
led.led_1.off();
}
}
// this program does not `exit`; use Ctrl+C to terminate it
}

View file

@ -0,0 +1,28 @@
// this program does not use the standard library to avoid heap allocations.
// only the `core` library functions are available.
#![no_std]
// this program uses a custom entry point instead of `fn main()`
#![no_main]
use cortex_m::asm;
use cortex_m_rt::entry;
// this imports `beginner/apps/lib.rs` to retrieve our global logger + panicking-behavior
use apps as _;
// the custom entry point
// vvvvv
#[entry]
fn main() -> ! {
// ˆˆˆ
// ! is the 'never' type: this function never returns
// initializes the peripherals
dk_bsc::init().unwrap();
defmt::println!("Hello, world!"); // :wave:
loop {
// breakpoint: halts the program's execution
asm::bkpt();
}
}

View file

@ -0,0 +1,28 @@
#![no_main]
#![no_std]
use cortex_m::asm;
use cortex_m_rt::entry;
use core::fmt::Write;
// this imports `down-the-stack/apps/lib.rs` to retrieve our global logger + panicking-behavior
use apps as _;
// ⚠️ ⚠️ ⚠️ Don't change this file! ⚠️ ⚠️ ⚠️
#[entry]
fn main() -> ! {
// to enable more verbose logs, go to your `Cargo.toml` and set defmt logging levels
// to `defmt-trace` by changing the `default = []` entry in `[features]`
let board = dk_bsc::init().unwrap();
let mut uarte = board.uarte;
let tx_buffer = "Hello, World!\n";
uarte.write_str(tx_buffer).unwrap();
// this program does not `exit`; use Ctrl+C to terminate it
loop {
asm::nop();
}
}

View file

@ -0,0 +1,10 @@
#![no_std]
// ⚠️ ⚠️ ⚠️ Don't change this file! ⚠️ ⚠️ ⚠️
use panic_probe as _;
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
#[defmt::panic_handler]
fn panic() -> ! {
cortex_m::asm::udf()
}

View file

@ -0,0 +1,24 @@
[package]
authors = ["Jorge Aparicio <jorge.aparicio@ferrous-systems.com>", "Tanks Transfeld <tanks.transfeld@ferrous-systems.com"]
edition = "2018"
license = "MIT OR Apache-2.0"
name = "dk_bsc"
version = "0.0.0"
[dependencies]
cortex-m = {version = "0.7.6", features = ["critical-section-single-core"]}
cortex-m-rt = "0.7.2"
embedded-hal = "0.2.7"
nrf52840-hal = "0.14.0"
panic-probe = { version = "0.3.0", features = ["print-defmt"] }
defmt = "0.3.2"
defmt-rtt = "0.3.2"
[features]
advanced = []
beginner = []
default = [
"other-feature"
]
other-feature = []

View file

@ -0,0 +1,7 @@
# `dk`
Board Support Crate for the nRF52840 Development Kit (DK)
## Getting familiar with the hardware
TODO

View file

@ -0,0 +1,12 @@
use std::{env, error::Error, fs, path::PathBuf};
fn main() -> Result<(), Box<dyn Error>> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
// put memory layout (linker script) in the linker search path
fs::copy("memory.x", out_dir.join("memory.x"))?;
println!("cargo:rustc-link-search={}", out_dir.display());
Ok(())
}

View file

@ -0,0 +1,5 @@
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 1024K
RAM : ORIGIN = 0x20000000, LENGTH = 256K
}

View file

@ -0,0 +1,259 @@
//! Board Support Crate (BSC) for the nRF52840 Development Kit
#![deny(missing_docs)]
#![no_std]
use core::{
ops,
sync::atomic::{self, Ordering},
time::Duration,
};
use cortex_m::asm;
use embedded_hal::digital::v2::{OutputPin as _, StatefulOutputPin};
use nrf52840_hal as hal;
use hal::{
gpio::{p0, Level, Output, Pin, Port, PushPull},
timer::OneShot,
};
use defmt;
use defmt_rtt as _; // global logger
/// Components on the boarde
// Add structs for the peripheral you want to implement. First for the buttons, later UARTE
pub struct Board {
/// LEDs
pub leds: Leds,
/// Timer
pub timer: Timer,
}
/// All LEDs on the board
pub struct Leds {
/// LED1: pin P0.13, green LED
pub led_1: Led,
/// LED2: pin P0.14, green LED
pub led_2: Led,
/// LED3: pin P0.15, green LED
pub led_3: Led,
/// LED4: pin P0.16, green LED
pub led_4: Led,
}
/// A single LED
pub struct Led {
inner: Pin<Output<PushPull>>,
}
impl Led {
/// Turns on the LED
pub fn on(&mut self) {
defmt::trace!(
"setting P{}.{} low (LED on)",
port_as_char(&self.inner.port()),
self.inner.pin()
);
// NOTE this operations returns a `Result` but never returns the `Err` variant
let _ = self.inner.set_low();
}
/// Turns off the LED
pub fn off(&mut self) {
defmt::trace!(
"setting P{}.{} high (LED off)",
port_as_char(&self.inner.port()),
self.inner.pin()
);
// NOTE this operations returns a `Result` but never returns the `Err` variant
let _ = self.inner.set_high();
}
/// Returns `true` if the LED is in the OFF state
pub fn is_off(&self) -> bool {
self.inner.is_set_high() == Ok(true)
}
/// Returns `true` if the LED is in the ON state
pub fn is_on(&self) -> bool {
!self.is_off()
}
/// Toggles the state (on/off) of the LED
pub fn toggle(&mut self) {
if self.is_off() {
self.on();
} else {
self.off()
}
}
}
/// All buttons on the board
// todo! Add a struct that represents all buttons of the board.
// ...
/// A single button
// todo! Add a struct that represents a single button.
// ...
// Add an impl block for the Button struct
// todo! Add a method that returns true, if the button is pushed.
// ...
/// A timer for creating blocking delays
pub struct Timer {
inner: hal::Timer<hal::pac::TIMER0, OneShot>,
}
impl Timer {
/// Blocks program execution for at least the specified `duration`
pub fn wait(&mut self, duration: Duration) {
defmt::trace!("blocking for {:?} ...", duration);
// 1 cycle = 1 microsecond because the underlying HAL driver
// always sets the timer to 1 MHz.
const NANOS_IN_ONE_MICRO: u32 = 1_000;
let subsec_micros = duration.subsec_nanos() / NANOS_IN_ONE_MICRO;
if subsec_micros != 0 {
self.inner.delay(subsec_micros);
}
const MICROS_IN_ONE_SEC: u32 = 1_000_000;
// maximum number of seconds that fit in a single `delay` call without overflowing the `u32`
// argument
const MAX_SECS: u32 = u32::MAX / MICROS_IN_ONE_SEC;
let mut secs = duration.as_secs();
while secs != 0 {
let cycles = if secs > MAX_SECS as u64 {
secs -= MAX_SECS as u64;
MAX_SECS * MICROS_IN_ONE_SEC
} else {
let cycles = secs as u32 * MICROS_IN_ONE_SEC;
secs = 0;
cycles
};
self.inner.delay(cycles)
}
defmt::trace!("... DONE");
}
}
impl ops::Deref for Timer {
type Target = hal::Timer<hal::pac::TIMER0, OneShot>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl ops::DerefMut for Timer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Uarte peripheral
// todo! Add a struct that represents the Uarte
// ...
// todo! Implement the fmt::Write Trait for Uarte
// ...
/// Initializes the board
///
/// This return an `Err`or if called more than once
pub fn init() -> Result<Board, ()> {
if let Some(periph) = hal::pac::Peripherals::take() {
let pins = p0::Parts::new(periph.P0);
// NOTE LEDs turn on when the pin output level is low
let led_1 = pins.p0_13.degrade().into_push_pull_output(Level::High);
let led_2 = pins.p0_14.degrade().into_push_pull_output(Level::High);
let led_3 = pins.p0_15.degrade().into_push_pull_output(Level::High);
let led_4 = pins.p0_16.degrade().into_push_pull_output(Level::High);
// Buttons
// todo! Assign the pins of the buttons
// ...
defmt::debug!("I/O pins have been configured for digital output");
let timer = hal::Timer::new(periph.TIMER0);
// Uarte
// todo! Assign the pins of the UARTE peripheral
// ...
// todo! Instantiate the UARTE peripheral
// ...
Ok(Board {
leds: Leds {
led_1: Led { inner: led_1 },
led_2: Led { inner: led_2 },
led_3: Led { inner: led_3 },
led_4: Led { inner: led_4 },
},
// todo! Create an instance of the struct that contains all the single buttons.
// ...
timer: Timer { inner: timer },
// todo! Create an instance of the UARTE struct
// ...
})
} else {
Err(())
}
}
/// Exits the application when the program is executed through the `probe-run` Cargo runner
pub fn exit() -> ! {
unsafe {
// turn off the USB D+ pull-up before pausing the device with a breakpoint
// this disconnects the nRF device from the USB host so the USB host won't attempt further
// USB communication (and see an unresponsive device). probe-run will also reset the nRF's
// USBD peripheral when it sees the device in a halted state which has the same effect as
// this line but that can take a while and the USB host may issue a power cycle of the USB
// port / hub / root in the meantime, which can bring down the probe and break probe-run
const USBD_USBPULLUP: *mut u32 = 0x4002_7504 as *mut u32;
USBD_USBPULLUP.write_volatile(0)
}
defmt::println!("`dk::exit()` called; exiting ...");
// force any pending memory operation to complete before the BKPT instruction that follows
atomic::compiler_fence(Ordering::SeqCst);
loop {
asm::bkpt()
}
}
// Helper functions
fn port_as_char(port: &Port) -> char {
match port {
Port::Port0 => '0',
Port::Port1 => '1',
}
}

View file

@ -0,0 +1,303 @@
//! Board Support Crate (BSC) for the nRF52840 Development Kit
#![deny(missing_docs)]
#![no_std]
use core::{
ops,
fmt,
sync::atomic::{self, Ordering},
time::Duration,
};
use cortex_m::asm;
use embedded_hal::digital::v2::{OutputPin as _, StatefulOutputPin};
use nrf52840_hal as hal;
pub use hal::pac::uarte0::{
baudrate::BAUDRATE_A as Baudrate, config::PARITY_A as Parity};
use hal::{
gpio::{p0, Level, Output, Input, PullUp, Pin, Port, PushPull},
timer::OneShot, prelude::InputPin,
uarte,
};
use defmt;
use defmt_rtt as _; // global logger
/// Components on the board
pub struct Board {
/// LEDs
pub leds: Leds,
// 🔽 --- Button Exercise --- 🔽
/// Buttons
pub buttons: Buttons,
// 🔼 --- Button Exercise --- 🔼
/// Timer
pub timer: Timer,
// 🔽 --- UARTE Exercise --- 🔽
/// uarte interface
pub uarte: Uarte,
// 🔼 --- UARTE Exercise --- 🔼
}
/// All LEDs on the board
pub struct Leds {
/// LED1: pin P0.13, green LED
pub led_1: Led,
/// LED2: pin P0.14, green LED
pub led_2: Led,
/// LED3: pin P0.15, green LED
pub led_3: Led,
/// LED4: pin P0.16, green LED
pub led_4: Led,
}
/// A single LED
pub struct Led {
inner: Pin<Output<PushPull>>,
}
impl Led {
/// Turns on the LED
pub fn on(&mut self) {
defmt::trace!(
"setting P{}.{} low (LED on)",
port_as_char(&self.inner.port()),
self.inner.pin()
);
// NOTE this operations returns a `Result` but never returns the `Err` variant
let _ = self.inner.set_low();
}
/// Turns off the LED
pub fn off(&mut self) {
defmt::trace!(
"setting P{}.{} high (LED off)",
port_as_char(&self.inner.port()),
self.inner.pin()
);
// NOTE this operations returns a `Result` but never returns the `Err` variant
let _ = self.inner.set_high();
}
/// Returns `true` if the LED is in the OFF state
pub fn is_off(&self) -> bool {
self.inner.is_set_high() == Ok(true)
}
/// Returns `true` if the LED is in the ON state
pub fn is_on(&self) -> bool {
!self.is_off()
}
/// Toggles the state (on/off) of the LED
pub fn toggle(&mut self) {
if self.is_off() {
self.on();
} else {
self.off()
}
}
}
// 🔽 --- Button Exercise --- 🔽
/// All buttons on the board
pub struct Buttons {
/// BUTTON1: pin P0.11
pub b_1: Button,
/// BUTTON2: pin P0.12
pub b_2: Button,
/// BUTTON3: pin P0.24
pub b_3: Button,
/// BUTTON4: pin P0.25
pub b_4: Button,
}
/// A single button
pub struct Button {
inner: Pin<Input<PullUp>>,
}
impl Button {
/// returns true if button is pushed
pub fn is_pushed(&self) -> bool {
self.inner.is_low() == Ok(true)
}
}
// 🔼 --- Button Exercise --- 🔼
/// A timer for creating blocking delays
pub struct Timer {
inner: hal::Timer<hal::pac::TIMER0, OneShot>,
}
impl Timer {
/// Blocks program execution for at least the specified `duration`
pub fn wait(&mut self, duration: Duration) {
defmt::trace!("blocking for {:?} ...", duration);
// 1 cycle = 1 microsecond because the underlying HAL driver
// always sets the timer to 1 MHz.
const NANOS_IN_ONE_MICRO: u32 = 1_000;
let subsec_micros = duration.subsec_nanos() / NANOS_IN_ONE_MICRO;
if subsec_micros != 0 {
self.inner.delay(subsec_micros);
}
const MICROS_IN_ONE_SEC: u32 = 1_000_000;
// maximum number of seconds that fit in a single `delay` call without overflowing the `u32`
// argument
const MAX_SECS: u32 = u32::MAX / MICROS_IN_ONE_SEC;
let mut secs = duration.as_secs();
while secs != 0 {
let cycles = if secs > MAX_SECS as u64 {
secs -= MAX_SECS as u64;
MAX_SECS * MICROS_IN_ONE_SEC
} else {
let cycles = secs as u32 * MICROS_IN_ONE_SEC;
secs = 0;
cycles
};
self.inner.delay(cycles)
}
defmt::trace!("... DONE");
}
}
impl ops::Deref for Timer {
type Target = hal::Timer<hal::pac::TIMER0, OneShot>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl ops::DerefMut for Timer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
// 🔽 --- UARTE Exercise --- 🔽
/// Uarte peripheral
pub struct Uarte {
inner: hal::Uarte<hal::pac::UARTE1>,
}
impl fmt::Write for Uarte {
fn write_str(&mut self, s: &str) -> fmt::Result {
// Copy all data into an on-stack buffer so we never try to EasyDMA from
// flash.
let mut buf: [u8; 16] = [0; 16];
for block in s.as_bytes().chunks(16) {
buf[..block.len()].copy_from_slice(block);
self.inner.write(&buf[..block.len()]).map_err(|_| fmt::Error)?;
}
Ok(())
}
}
// 🔼 --- UARTE Exercise --- 🔼
/// Initializes the board
///
/// This return an `Err`or if called more than once
pub fn init() -> Result<Board, ()> {
if let Some(periph) = hal::pac::Peripherals::take() {
let pins = p0::Parts::new(periph.P0);
// NOTE LEDs turn on when the pin output level is low
let led_1 = pins.p0_13.degrade().into_push_pull_output(Level::High);
let led_2 = pins.p0_14.degrade().into_push_pull_output(Level::High);
let led_3 = pins.p0_15.degrade().into_push_pull_output(Level::High);
let led_4 = pins.p0_16.degrade().into_push_pull_output(Level::High);
// 🔽 --- Button Exercise --- 🔽
// Buttons
let b_1 = pins.p0_11.degrade().into_pullup_input();
let b_2 = pins.p0_12.degrade().into_pullup_input();
let b_3 = pins.p0_24.degrade().into_pullup_input();
let b_4 = pins.p0_25.degrade().into_pullup_input();
// 🔼 --- Button Exercise --- 🔼
defmt::debug!("I/O pins have been configured for digital output");
let timer = hal::Timer::new(periph.TIMER0);
// 🔽 --- UARTE Exercise --- 🔽
// Uarte
let pins = hal::uarte::Pins {
rxd: pins.p0_08.degrade().into_floating_input(),
txd: pins.p0_06.degrade().into_push_pull_output(Level::High),
cts: None,
rts: None,
};
let uarte = hal::uarte::Uarte::new(periph.UARTE1, pins, Parity::EXCLUDED, Baudrate::BAUD115200);
// 🔼 --- UARTE Exercise --- 🔼
Ok(Board {
leds: Leds {
led_1: Led { inner: led_1 },
led_2: Led { inner: led_2 },
led_3: Led { inner: led_3 },
led_4: Led { inner: led_4 },
},
// 🔽 --- Button Exercise --- 🔽
buttons: Buttons {
b_1: Button { inner: b_1},
b_2: Button { inner: b_2},
b_3: Button { inner: b_3},
b_4: Button { inner: b_4},
},
// 🔼 --- Button Exercise --- 🔼
timer: Timer { inner: timer },
// 🔽 --- UARTE Exercise --- 🔽
uarte: Uarte { inner: uarte },
// 🔼 --- UARTE Exercise --- 🔼
})
} else {
Err(())
}
}
/// Exits the application when the program is executed through the `probe-run` Cargo runner
pub fn exit() -> ! {
unsafe {
// turn off the USB D+ pull-up before pausing the device with a breakpoint
// this disconnects the nRF device from the USB host so the USB host won't attempt further
// USB communication (and see an unresponsive device). probe-run will also reset the nRF's
// USBD peripheral when it sees the device in a halted state which has the same effect as
// this line but that can take a while and the USB host may issue a power cycle of the USB
// port / hub / root in the meantime, which can bring down the probe and break probe-run
const USBD_USBPULLUP: *mut u32 = 0x4002_7504 as *mut u32;
USBD_USBPULLUP.write_volatile(0)
}
defmt::println!("`dk::exit()` called; exiting ...");
// force any pending memory operation to complete before the BKPT instruction that follows
atomic::compiler_fence(Ordering::SeqCst);
loop {
asm::bkpt()
}
}
// Helper functions
fn port_as_char(port: &Port) -> char {
match port {
Port::Port0 => '0',
Port::Port1 => '1',
}
}

View file

@ -24,6 +24,10 @@
- [Collision avoidance](./collision-avoidance.md)
- [Interrupt handling](./interrupt-handling.md)
- [Starting a Project from Scratch](./from-scratch.md)
- [Down the Stack Workbook](./down-the-stack.md)
- [BSC Exercise](./bsc-exercise.md)
- [Button Implementation](./button-implementation.md)
- [UARTE Implementation](./uarte-implementation.md)
- [Advanced Workbook](./advanced-workbook.md)
- [Code Organization](./code-organisation.md)
- [Listing USB Devices](./listing-usb-devices.md)

View file

@ -0,0 +1,67 @@
# BSC Exercise
In this exercise you will learn how to write a *Board Support Crate* (or BSC, also known as a *Board Support Package*) by implementing support for handling button presses, and support for using the UARTE peripheral.
The template `down-the-stack/dk_bsc/src/lib.rs` already contains the LED and Timer implementations. Add your code to the designated lines. You'll find a `//todo!` there.
You can test after each step by running the following command out of `down-the-stack/apps`
```
cargo run --bin hello
```
This program will not call any of the functions you are implementing, so it does not matter if they are incomplete. It will refuse to build if there are errors present in the `lib.rs`!
`down-the-stack/dk_bsc/src/lib_solution.rs` contains the full solution code.
## You will learn how to
* modify the `init()` function that brings up the board's peripherals
* how to configure pins
* how to write a function that checks the state of a pin
* implement functionality on a type
* implement a Trait
* to document and generate docs for your own library!
## Prerequisites
* installation of `pyserial` or other serial terminal that you are familiar with.
* `impl` keyword
* methods and associated functions
* `pub` keyword
* usage of structs to represent registers
* Trait
## Tasks
### Write a button implementation. This entails the following steps:
* Add `struct Buttons` with 4 fields, that represents each of the four buttons.
* Add `struct Button` that is a wrapper for the pin that a single button is connected to.
* Write a method `is_pushed` that checks if a single button is pushed.
* Initialize the pins in `fn init()`.
* Add the `struct Button` to the definition and instantiation of `struct Board`.
* Run `apps/buttons.rs` to test.
* Run `cargo doc` out of the apps folder to find all your doc comments!
### Write a UARTE implementation. This entails the following steps:
* Check the `uarte` module of the `nrf-hal` for requirements of the instantiating method.
* Add `struct Uarte` that serves as wrapper for the `UARTE1` instance.
* Initialize the UARTE1 peripheral in `fn init()` using the following settings:
* parity: included
* baudrate: 115200 baud
* Add `struct Uarte` to the definition and instantiation of `struct Board`.
* Implement the `fmt::Write` trait for `struct Uarte`.
* Connect your computer to the virtual UART port with `screen`.
* Run `apps/uarte_print.rs` to test.
## Knowledge
### Comments
The `lib.rs` has an attribute `#![deny(missing_docs)]`. This means, that missing doc comments for structs are returned as compiler errors, to remind you to document your work properly.
```rust
/// This is a doc comment
// This is a normal comment
```
### Structs represent Registers
[todo!] insert refresher from rust fundamentals
## Hardware documentation for pin configuration
Go to [Nordic Infocenter](https://infocenter.nordicsemi.com/topic/ug_nrf52840_dk/UG/dk/intro.html) to download the User Guide. You can find all the information that is relevant to this exercise in there.

View file

@ -0,0 +1,146 @@
# Write the Button Implementation
## Step-by-Step Solution
### Step 1: Read the docs!
✅ Read the [User Guide section 8.7](https://infocenter.nordicsemi.com/topic/ug_nrf52840_dk/UG/dk/hw_buttons_leds.html?cp=5_0_4_7_6) for info about pins and pin configuration related to the buttons. Note down the pins that the buttons are connected to.
The pins need to be configured as input pins with an internal pull-up. The pins as well as the configurations are defined as types in the `nrf-hal` in the `gpio` peripheral. Add the following imports: `Input` and `PullUp`.
### Step 2: Add the structs that represent the buttons as a group and a generic single button.
✅ Add the struct that represents the single button. It has only one field, `inner`. The type of this button is the pin configuration: `Pin<Input<PullUp>>`
✅ Add the `struct` that represents the group of buttons has four fields, one for each button. The field name contains the number that corresponds to the button numeration on the board. The type of each field is the struct that represents the generic single button.
✅ Add doc comments for every struct and field!
Building this code should return a warning: field `inner` is never read.
<details>
<summary>Solution</summary>
```rust
/// All buttons on the board
pub struct Buttons {
/// BUTTON1: pin P0.11
pub b_1: Button,
/// BUTTON2: pin P0.12
pub b_2: Button,
/// BUTTON3: pin P0.24
pub b_3: Button,
/// BUTTON4: pin P0.25
pub b_4: Button,
}
/// A single button
pub struct Button {
inner: Pin<Input<PullUp>>,
}
```
</details>
### Step 3: Implement the button function.
✅ Add an `impl` block for the `struct Button`. Add a method `is_pushed` that takes in the struct as `&self` and returns a bool, if the button is pushed.
✅ Now remember, the pins the buttons are connected to are configured as active low. For buttons this means, that the pin is pulled low, when the button is pushed.
✅ In the `nrf-hal` you can find a method to check if a single pin is low. To use it, you have to add the following line to your `nrf52840_hal` imports: `prelude::InputPin`.
<details>
<summary>Solution</summary>
```rust
impl Button {
/// returns true if button is pushed
pub fn is_pushed(&self) -> bool {
self.inner.is_low() == Ok(true)
}
}
```
</details>
### Step 4: Bring up the pins!
✅ Go to `pub fn init()`, the function that initializes the board's peripherals.
✅ Configure each pin as degraded, pull-up input pin and bind it to a variable that makes it clear what button number it is connected to.
Building this code brings up warnings about unused variables.
<details>
<summary>Solution</summary>
```rust
// Buttons
let b_1 = pins.p0_11.degrade().into_pullup_input();
let b_2 = pins.p0_12.degrade().into_pullup_input();
let b_3 = pins.p0_24.degrade().into_pullup_input();
let b_4 = pins.p0_25.degrade().into_pullup_input();
```
</details>
### Step 5: Add everything to the board struct.
✅ In the definition of the `struct Board` add a field for the `struct Buttons`.
✅ In the pub `fn init()` function, where `Board` is instantiated, add the button field, assigning the pins you defined earlier to the respective buttons.
<details>
<summary>Solution</summary>
```rust
/// Components on the board
pub struct Board {
/// LEDs
pub leds: Leds,
/// Buttons
pub buttons: Buttons,
/// Timer
pub timer: Timer,
}
// ...
pub fn init() -> Result<Board, ()> {
// ...
Ok(Board {
leds: Leds {
// ...
},
buttons: Buttons {
b_1: Button { inner: b_1},
b_2: Button { inner: b_2},
b_3: Button { inner: b_3},
b_4: Button { inner: b_4},
},
timer: Timer { inner: timer },
})
} else {
Err(())
}
}
```
</details>
### Step 6: Run the example!
✅ Go to `/down-the-stack/apps`.
✅ Run the following command:
```shell
cargo run --bin button
```
### Step 7: Generate the docs!
✅ Out of the apps folder run the following command to build the docs for this crate and to view your written documentation!
```shell
cargo doc
```

View file

@ -0,0 +1,10 @@
# Down the Stack
In this Session you will learn more about Rust's split crate model that includes
* the Board Support Crate
* an implementation of the Hardware Abstraction Layer
* the Peripheral Access Crate
This session consists of lectures and two blocks of exercises:
* BSC Exercise: Implementing further features in a basic BSC
* PAC Exercise: Generating a PAC from an SVD file and writing into registers using the the generated PAC.

View file

@ -157,3 +157,6 @@ $ cargo install nrfdfu
(..)
Installed package `nrfdfu v0.1.3` (..)
```
## pySerial for Down The Stack
If you don't have a serial terminal you're familiar with, we recommend using `pySerial`. Follow the installation instructions [here](https://pythonhosted.org/pyserial/pyserial.html#installation). This may require an update of your python installation. This is needed for the second day of the 3 day embedded training.

View file

@ -0,0 +1,204 @@
# Write the Uarte implementation
## Step-by-Step Solution
### Step 1: Check Documentation.
The UART protocol requires up to four pins, two are mandatory, additional two are optional. they are usually labelled:
* RXD
* TXD
* CTS (optional)
* RTS (optional)
We will only use the mandatory ones.
✅ Check the [User Guide in section 7.2](https://infocenter.nordicsemi.com/topic/ug_nrf52840_dk/UG/dk/vir_com_port.html) to find to find out which pins are reserved for these and what their configuration needs to be.
### Step 2: Explore the `nrf-hal` to find out what needs to be done.
The `nrf52840-hal` is a crate that exports all the `52840` flagged features from the `nrf-hal-common`. Let's take a look at the `nrf52840-hal`'s [Uarte module](https://github.com/nrf-rs/nrf-hal/blob/v0.14.1/nrf-hal-common/src/uarte.rs).
In line 16 we see, that the nRF52840 uses the `hal::pac::UARTE1` peripheral.
In line 44 you find the `struct Uarte<T>(T)`, the interface to a UARTE instance `T`. Besides the instance `T`, the instantiating method takes variables of the following types as arguments: `Pins`, `Parity` and `Baudrate`.
A quick search of the document reveals where to find all of them:
* `Pins`: Line 463
* `Parity` and `Baudrate`: Re-export on line 34
✅ Add the following lines as import:
```
use hal::pac::uarte0::{
baudrate::BAUDRATE_A as Baudrate, config::PARITY_A as Parity};
use hal::uarte;
```
### Step 3: Add `struct Uarte`
✅ Add `struct Uarte` that serves as a wrapper for underlying HAL UART driver.
The struct has one field labelled `inner` which is of type `hal::Uarte` but with the type parameter `T` being set to `hal::pac::UARTE1`.
The main difference between using our `Uarte` object and the underlying HAL `Uarte` object is that *ours* will be pre-configured for the correct pins and baud rate, according to the layout of our nRF52840-DK board and the Virtual COM Port interface chip on the other end of the UART link.
<details>
<summary>Solution</summary>
```rust
pub struct Uarte {
inner: hal::Uarte<hal::pac::UARTE1>,
}
```
</details>
### Step 4: Bring up the peripheral in the `fn init()`
✅ Take a closer look at the definition of the `uarte::Pins` struct in the `nrf-hal`. Compare the pin type configurations with the ones you have already imported in `lib.rs`. Add the ones you're missing.
✅ Create an instance of this struct in `fn init()` with the appropriate pins and configurations. Set the output pin's level to `Level::High`.
Note, that the third and fourth pin are each wrapped in an `Option`: Their type is `None` in this case.
✅ Create a Uarte driver with `hal::uarte::Uarte::new(...)` and bind it to a variable called `uarte` - we will stash this in our own `Uarte` struct later.
Creating the Uarte driver requires four arguments:
* The `UARTE1` instance can be found in the `periph` variable.
* Your instance of the `uarte::Pins` struct.
* Set parity to `Parity::EXCLUDED`
* set the baud rate to `Baudrate::BAUD115200`.
<details>
<summary>Solution</summary>
```rust
let pins = hal::uarte::Pins {
rxd: pins.p0_08.degrade().into_floating_input(),
txd: pins.p0_06.degrade().into_push_pull_output(Level::High),
cts: None,
rts: None,
};
let uarte = hal::uarte::Uarte::new(periph.UARTE1, pins, Parity::EXCLUDED, Baudrate::BAUD115200);
```
</details>
### Step 5: Board struct
✅ Add a field for the `Uarte` struct in the `Board` struct.
add the field to the instance of the `Board` struct in `fn init()`.
<details>
<summary>Solution</summary>
```rust
pub struct Board {
/// LEDs
pub leds: Leds,
/// Buttons
pub buttons: Buttons,
/// Timer
pub timer: Timer,
/// uarte interface
pub uarte: Uarte,
}
// ...
pub fn init() -> Result<Board, ()> {
// ...
Ok(Board {
leds: Leds {
// ...
},
buttons: Buttons {
// ...
},
// 🔼 --- Button Exercise --- 🔼
timer: Timer { inner: timer },
uarte: Uarte { inner: uarte },
})
} else {
Err(())
}
```
</details>
### Step 6: Implementing the `fmt::Write` trait
We want to implement the `fmt::Write` trait so that users can call `write!` on our Uarte object
When implementing this, we can't just write to the `Uarte` instance because a simple write of a string literal would try and read the string literal from flash memory. This does not work because the EasyDMA peripheral in the nRF52 series can only access RAM, not flash.
Instead our implementation must ensure all the strings are copied to a stack allocated buffer and that buffer is passed to the Uarte's `write` method.
✅ Add `use::core::fmt;` to your imports.
✅ Create a public method `write_str`. It takes a mutable reference to self and a `&str` as argument. It returns an `fmt::Result`
✅ Create a buffer. The type is an `array` of 16 u8, set to all 0.
✅ To copy all data into an on-stack buffer, *iterate* over *chunks* of the string and copy them into the buffer (noting that the chunk length may be less than the requested size if you are at the end of the input string).
<details>
<summary>Solution</summary>
```rust
impl fmt::Write for Uarte {
fn write_str(&mut self, s: &str) -> fmt::Result {
// Copy all data into an on-stack buffer so we never try to EasyDMA from
// flash.
let mut buf: [u8; 16] = [0; 16];
for block in s.as_bytes().chunks(16) {
buf[..block.len()].copy_from_slice(block);
self.inner.write(&buf[..block.len()]).map_err(|_| fmt::Error)?;
}
Ok(())
}
}
```
</details>
### Step 7: Connect your computer to the virtual UART
✅ Use the following command to find the address of the nRF52840-DK on your computer.
Linux:
```
$ ls /dev/ttyACM*
```
MacOS:
```
ls /dev/tty.usbmodem*
```
Windows:
a USB Serial Device (COM port) in the Device Manager under the Ports section.
✅ Run the following command to run `miniterm` with the nRF52840-DK with 115200 baud. This includes the default settings of no parity, 8 data bits, 1 stop bit
```
python3 -m serial.tools.miniterm <address> 115200
```
### Step 8: Run the example.
✅ In another terminal window go into the folder `down-the-stack/apps` and use the following command.
```
cargo run --bin uarte_print
```
On your terminal window where `miniterm` runs, "Hello, World" should appear.