Compare commits

..
18 Commits
Author SHA1 Message Date
Meysam Parvizi b6dec3d394 feat(site): add Display & Camera cluster (MIPI CSI-2, MIPI DSI) to the map 2026-10-10 04:57:24 +02:00
Meysam Parvizi da88157952 fix(site): soften cluster header hover and selected highlight 2026-10-10 04:38:48 +02:00
Meysam Parvizi 3150cc358d docs: add free soft-skill resources 2026-10-10 04:36:03 +02:00
Meysam Parvizi b08f3a9699 docs: add resources for UWB, Profinet, LIN, MOST, FlexRay and the six soft skills 2026-10-10 04:16:23 +02:00
Meysam Parvizi 8cf40b4993 fix(site): make cluster headers clickable with pill feedback instead of link-style underline; name the hovered box in the tooltip 2026-10-10 04:05:19 +02:00
Meysam Parvizi 95aff195b4 Move README heading names into map.json, warn about unmapped README sections, document the README-explorer workflow 2026-10-10 03:55:37 +02:00
Meysam Parvizi 2c4faffbd8 feat(site): pinch to zoom and two-finger pan on the map
Two touches zoom around their midpoint and pan with it; lifting them does not open a topic. Safari's trackpad pinch gesture events are handled too.
2026-10-10 03:46:13 +02:00
Meysam Parvizi 558cc859f5 fix(site): render the map without transitions when printing
The box strokes fade from screen to print colors over 0.15 s, and the PNG/PDF render sometimes captured them mid-fade. That made the output differ slightly between runs and the map fingerprint flip, which created an unnecessary release (v2.0.2). Print styles now disable transitions and animations, so 12 of 12 test renders are byte-identical.
2026-10-10 03:10:24 +02:00
Meysam Parvizi 71adde583f fix(site): highlight group labels by their pill on hover instead of underlining 2026-10-10 02:56:48 +02:00
Meysam Parvizi 334e7d0456 docs(readme): introduce the interactive roadmap and link the image to it
Add a short description of the interactive explorer above the roadmap image and make the image open the site.
2026-10-10 02:56:48 +02:00
Meysam Parvizi 4e0ba15dc9 style(site): match the logo height to the license badge in the map footer 2026-10-10 02:41:39 +02:00
Meysam Parvizi 4c52895477 feat(site): use curated connections between topics
Connections come from the links list in map.json instead of only from shared or mentioned resources. Curated ones are solid lines with a reason in the details panel; the text-based ones stay as faint dashed evidence. Tests check names, duplicates and that every technical topic is connected.
2026-10-10 02:39:06 +02:00
Meysam Parvizi 2e2ca453c9 docs(map): draft the connections between topics
A first set of 196 hand-picked connections, each with a one-line reason, for review by the maintainer.
2026-10-10 02:39:06 +02:00
Meysam Parvizi b83bdda42f style(site): make the hover tooltip semi-transparent
The popup background is 72% opaque with a slight blur, so the map stays visible behind it.
2026-10-10 02:37:25 +02:00
Meysam Parvizi 8704cf6234 fix(site): allow zooming in after "Show the whole page"
The whole-page view sits below the minimum zoom, so every zoom-in step was rejected as out of range. Limits now only stop steps that go further out of range, and zooming out can reach the whole-page view.
2026-10-10 02:35:06 +02:00
Meysam Parvizi ad4a5406d9 fix(site): make the tooltip hint look like a hint, not a link
The tooltip ignores the mouse and follows the cursor, so its hint is now plain muted text about the topic instead of an accent-colored link-like line.
2026-10-10 01:58:55 +02:00
Meysam Parvizi 2c70f347de fix(site): no scrollbar over the chip rows in Firefox
Chip rows wrap on desktop; on phones they swipe with a hidden scrollbar and an edge fade.
2026-10-10 01:52:43 +02:00
Meysam Parvizi 752f36a921 ci: give the release job a static name 2026-10-10 01:41:51 +02:00
12 changed files with 587 additions and 101 deletions
+4 -1
View File
@@ -50,6 +50,9 @@ jobs:
- name: Test the README parser and map layout
run: node --test site/parser.test.mjs
- name: Warn about README sections that are not on the map
run: node site/tools/coverage.mjs
- name: Assemble the site
run: |
mkdir _site
@@ -99,7 +102,7 @@ jobs:
path: _site
release:
name: Release ${{ needs.build.outputs.version }}
name: Publish release
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && needs.build.outputs.release == 'true'
needs: build
runs-on: ubuntu-24.04
+11 -1
View File
@@ -30,8 +30,18 @@ Contributions from everyone are welcomed. To keep the roadmap practical, accessi
- Contributions should match the topics and structure of the roadmap.
- Areas where contributors have experience or genuine interest should be focused on.
- If a new topic is thought to make the roadmap more complete, it may be suggested. New topics should be proposed thoughtfully, considering their usefulness and relevance for other learners.
- Connections between topics on the map are listed in `site/map.json` under `links` as `["Topic", "Other topic", "why they are related"]`, using the names shown on the map. Connect topics that a learner should study together or that depend on each other, and keep the reason to one short sentence. Tests fail if a name is misspelled, a pair is listed twice or a topic has no connection.
## 6. Versioning and Releases
## 6. How the README Feeds the Interactive Roadmap
The [interactive roadmap](https://m3y54m.github.io/Embedded-Engineering-Roadmap/) reads `README.md` directly; there is no other copy of the content. Most contributions need no change outside the README.
- **Topics** come from heading depth: `##` is a group, `###` a main topic, `####` and deeper are subtopics. The emojis in headings (✳️ 🔵 🔶 🔸) are decoration only. Renaming a heading changes its link (`#/topic-name`).
- **Resources** are list items in the form `- [📘👶💎 Title](https://url)`, optionally followed by a dash and a short note. Symbols: 📘 book, 🎞️ video, 📝 article, 🔗 link, 🎧 audio, 👶 beginner, 💎 essential. `python3 .github/scripts/check_readme.py` checks the format and duplicates.
- **The map** (`site/map.json`) draws only the main topics. Each label on it opens the README heading with the same name. If the wording differs, or the label has no section of its own, add `"Label": ["README heading"]` to `readme` in `site/map.json`. A new main topic needs a README section and a box in `site/map.json` (and at least one entry in `links`), unless it should stay off the map: then list its title or group in `offMap`. CI warns about main topics that are neither.
- **Preview and test locally:** `python3 -m http.server 8765` from the repository root, then open `/site/`; `node --test site/parser.test.mjs` runs the checks CI runs.
## 7. Versioning and Releases
The roadmap image is versioned as `vMAJOR.MINOR.PATCH`, starting from `v2.0.0`.
+66 -1
View File
@@ -42,7 +42,9 @@ used in an aircraft or rapid transit system.
## 🗺️ The Roadmap
![Embedded Systems Engineering Roadmap](https://github.com/m3y54m/Embedded-Engineering-Roadmap/releases/latest/download/Embedded-Engineering-Roadmap.png)
🌐 **[Open the interactive roadmap](https://m3y54m.github.io/Embedded-Engineering-Roadmap/)** to explore it in your browser. Click any topic to read its description and see its learning resources, hover over it to see the topics it is related to, filter topics by area (software, hardware, soft skills) or importance, and search across all topics and resources. The roadmap can also be downloaded there as a PDF or PNG image.
[![Embedded Systems Engineering Roadmap](https://github.com/m3y54m/Embedded-Engineering-Roadmap/releases/latest/download/Embedded-Engineering-Roadmap.png)](https://m3y54m.github.io/Embedded-Engineering-Roadmap/)
The embedded systems engineering roadmap is structured into three fundamental areas: **SOFTWARE**, **HARDWARE**, and **SOFT SKILLS**.
@@ -226,6 +228,42 @@ Similar to other professions, embedded engineers require soft skills that can't
- [📝 Soft Skills For Embedded Systems Software Developers](https://www.embeddedrelated.com/showarticle/1470.php)
- [📝 10 Skills Every Embedded Engineer Should Have](https://medium.com/@lanceharvieruntime/10-skills-every-embedded-engineer-should-have-dcb867095b91)
#### 🔵 Communication Skills
- [🔗👶 Technical Writing Courses - Google for Developers](https://developers.google.com/tech-writing)
- [📝💎 How To Ask Questions The Smart Way - Eric S. Raymond](http://www.catb.org/esr/faqs/smart-questions.html)
- [🔗 Software documentation guide - Write the Docs](https://www.writethedocs.org/guide/)
#### 🔵 Problem-Solving Skills & Critical Thinking
- [📘💎 Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems - David J. Agans](https://debuggingrules.com/)
- [📝 When debugging, your attitude matters - Julia Evans](https://jvns.ca/blog/debugging-attitude-matters/)
#### 🔵 Teamwork & Collaborative Abilities
- [📝 How to Work Well on Teams - Software Engineering at Google](https://abseil.io/resources/swe-book/html/ch02.html)
- [📝 Knowledge Sharing - Software Engineering at Google](https://abseil.io/resources/swe-book/html/ch03.html)
- [🔗💎 Code Review Developer Guide - Google Engineering Practices](https://google.github.io/eng-practices/review/)
- [🔗 Atlassian Team Playbook](https://www.atlassian.com/team-playbook)
#### 🔵 Organizational & Time Management Skills
- [📘💎 Getting Things Done: The Art of Stress-Free Productivity - David Allen](https://gettingthingsdone.com/what-is-gtd/)
- [🎞️👶 Work Smarter, Not Harder: Time Management for Personal & Professional Productivity - Coursera](https://www.coursera.org/learn/work-smarter-not-harder)
- [📝💎 Maker's Schedule, Manager's Schedule - Paul Graham](https://www.paulgraham.com/makersschedule.html)
#### 🔵 Being Self-Driven and Independent
- [🎞️👶 Learning How to Learn - Coursera](https://www.coursera.org/learn/learning-how-to-learn)
- [📝💎 Teach Yourself Programming in Ten Years - Peter Norvig](https://norvig.com/21-days.html)
- [📝💎 How to Do Great Work - Paul Graham](https://paulgraham.com/greatwork.html)
- [📝 You and Your Research - Richard Hamming](https://www.paulgraham.com/hamming.html)
#### 🔵 Adaptability & Patience
- [📘 Mindset: The New Psychology of Success - Carol S. Dweck](https://www.penguinrandomhouse.com/books/44330/mindset-by-carol-s-dweck-phd/)
- [🎞️👶 The power of believing that you can improve - Carol Dweck (TED Talk)](https://www.ted.com/talks/carol_dweck_the_power_of_believing_that_you_can_improve)
_________________
### ✳️ Electronics
@@ -757,6 +795,12 @@ Embedded systems often communicate with other devices or external systems via in
- [🎞️ What is Matter? Unifying IoT Devices for the Smart Home | Digi-Key Electronics](https://youtu.be/vJIEfih9bl0)
##### 🔶 UWB
- [📘👶 Ultra-Wideband For Dummies - Qorvo](https://www.qorvo.com/design-hub/ebooks/ultra-wideband-for-dummies)
- [🔗 FiRa Consortium - Advancing Ultra-Wideband (UWB) Technology](https://www.firaconsortium.org/)
- [📝 Ultra-wideband - Wikipedia](https://en.wikipedia.org/wiki/Ultra-wideband)
#### 🔵 Industrial Protocols
##### 🔶 Modbus
@@ -774,6 +818,12 @@ Embedded systems often communicate with other devices or external systems via in
- [🎞️ Microchip University - An Introduction to EtherCAT®, EtherCAT P and the Microchip LAN925x family of Slave Controllers](https://mu.microchip.com/an-introduction-to-ethercat-ethercat-p-and-the-microchip-lan925x-family-of-slave-controllers)
##### 🔶 Profinet
- [🎞️👶 How PROFINET Works? A Beginner's Guide - PI North America](https://us.profinet.com/webinar/how-profinet-works-a-beginners-guide/)
- [🎞️ PROFINET Video Series](https://www.profinet.com/profinet-explained/explanatory-videos)
- [🔗 PROFINET University](https://profinetuniversity.com/)
##### 🔶 CoAP & MQTT
- [🎞️ #144 Internet Protocols: CoAP vs MQTT, Network Sniffing, and preparation for IKEA Tradfri Hacking](https://www.youtube.com/watch?v=pfG8uEDZj5g)
@@ -825,6 +875,21 @@ Embedded systems often communicate with other devices or external systems via in
- [🎞️ J1939 Explained - A Simple Intro [v2.0 | 2021]](https://www.youtube.com/watch?v=vlqxu9ojbHg)
- [🎞️ Unified Diagnostic Services (UDS) Explained - A Simple Intro [2022]](https://www.youtube.com/watch?v=CV_B8tJgI5E)
##### 🔶 LIN
- [📝👶 LIN Bus Explained - A Simple Intro - CSS Electronics](https://www.csselectronics.com/pages/lin-bus-protocol-intro-basics)
- [📝 Local Interconnect Network - Wikipedia](https://en.wikipedia.org/wiki/Local_Interconnect_Network)
##### 🔶 MOST
- [📝 MOST Bus - Wikipedia](https://en.wikipedia.org/wiki/MOST_(bus))
- [🔗 MOST Cooperation - Specifications](https://www.mostcooperation.com/specifications/)
##### 🔶 FlexRay
- [📝 FlexRay Automotive Communication Bus Overview - NI](https://www.ni.com/en/shop/seamlessly-connect-to-third-party-devices-and-supervisory-system/flexray-automotive-communication-bus-overview.html)
- [📝 FlexRay - Wikipedia](https://en.wikipedia.org/wiki/FlexRay)
#### 🔵 Network Protocols / Socket Programming
- [🎞️ Networking Fundamentals - Practical Networking (YouTube Playlist)](https://www.youtube.com/playlist?list=PLIFyRwBY_4bRLmKfP1KnZA6rZbRHtxmXi)
+34 -5
View File
@@ -155,8 +155,33 @@ function topicChips(nodes) {
}, swatch(n), n.title, h('span', { class: 'n' }, n.total))));
}
function related(node) {
// Hand-picked connections of a topic (from map.json), each with the reason it is listed.
function relatedTopics(node) {
const seen = new Set();
const items = [];
for (const box of node.boxes || []) {
for (const { box: other, reason } of box.related || []) {
if (seen.has(other)) continue;
seen.add(other);
items.push({ other, reason });
}
}
return items;
}
function curatedList(items) {
return h('ul', { class: 'related' }, items.map(({ other, reason }) => {
const target = other.topics[0];
return h('li', {}, h('button', { type: 'button', onclick: () => navigate(target, { reveal: true }) },
swatch(target),
h('span', {}, other.text),
h('span', { class: 'why' }, reason)));
}));
}
function related(node, skip = new Set()) {
const items = [...node.links]
.filter((link) => !skip.has(link.source === node ? link.target : link.source))
.sort((a, b) => b.weight - a.weight)
.map((link) => {
const other = link.source === node ? link.target : link.source;
@@ -282,13 +307,13 @@ function overview(root) {
section(null, h('div', { class: 'stats' },
h('div', { class: 'stat' }, h('b', {}, stats.topics), h('span', {}, 'topics')),
h('div', { class: 'stat' }, h('b', {}, stats.resources), h('span', {}, 'resources')),
h('div', { class: 'stat' }, h('b', {}, stats.links), h('span', {}, 'connections')))),
h('div', { class: 'stat' }, h('b', {}, state.plan ? state.plan.connections.filter((c) => c.reason).length : stats.links), h('span', {}, 'connections')))),
state.plan ? section('Diagram topics by area', areaSummary()) : null,
...groups.map((g) => section(g.title, topicChips(g.topics))),
section('How to explore', h('ul', { class: 'howto' },
h('li', {}, 'Map follows the roadmap diagram: topics sit in Software, Hardware or both, and are colored by importance.'),
h('li', {}, 'Use the Areas chips to highlight a single area or only the Software ∩ Hardware cross-section.'),
h('li', {}, 'Hover or select a topic to see lines to the topics it shares resources with (solid) or is mentioned by (dotted).'),
h('li', {}, 'Hover or select a topic to see solid lines to its related topics. Fainter dashed lines come from resources that share or mention a topic.'),
h('li', {}, 'Outline lists every topic of the README; use it to browse topics that are not on the map.'),
h('li', {}, 'Press ', h('kbd', {}, '/'), ' to search topics and resources.'))),
section(null, h('p', { class: 'res-note' }, 'Built from the ',
@@ -325,7 +350,7 @@ function renderPanel(node) {
node.importance ? h('span', { class: `pill imp-pill ${node.importance}` }, IMPORTANCE[node.importance]) : null,
h('span', { class: 'pill' }, plural(node.total, 'resource')),
node.children.length ? h('span', { class: 'pill' }, plural(node.children.length, 'subtopic')) : null,
node.links.length ? h('span', { class: 'pill' }, `${node.links.length} connected`) : null)));
relatedTopics(node).length ? h('span', { class: 'pill' }, `${relatedTopics(node).length} related`) : null)));
if (node === root) {
panel.append(...overview(root));
@@ -333,7 +358,11 @@ function renderPanel(node) {
}
if (node.description.length) panel.append(section('About', description(node.description)));
if (node.children.length) panel.append(section('Subtopics', topicChips(node.children)));
if (node.links.length) panel.append(section('Connected topics', related(node)));
const curated = relatedTopics(node);
if (curated.length) panel.append(section('Related topics', curatedList(curated)));
const listed = new Set(curated.flatMap(({ other }) => other.topics));
const evidence = node.links.filter((link) => !listed.has(link.source === node ? link.target : link.source));
if (evidence.length) panel.append(section(curated.length ? 'Also linked by resources' : 'Linked by resources', related(node, listed)));
panel.append(resources(node));
}
+53 -47
View File
@@ -1,40 +1,6 @@
// Links the topics drawn on the map (map.json) to README topics, so every topic knows
// which areas (Software, Hardware, Soft skills) it sits in and how important the map marks it.
// Map labels whose README heading is worded differently.
const ALIASES = {
'ADC / DAC': ['ADC', 'DAC'],
'Buildroot / Yocto': ['Buildroot', 'Yocto'],
'TDD & Unit Testing': ['Test Driven Development (TDD)', 'Unit Testing'],
'Threading / Parallelism': ['Multithreading & Parallel Processing'],
'Device Drivers': ['Linux Device Drivers'],
'Real-Time OS': ['Real-Time Operating Systems'],
'Interfaces & Protocols': ['Interfaces, Protocols & Communication Technologies'],
Basic: ['Basic Protocols'],
'High-Speed': ['High-Speed Protocols'],
Wireless: ['Wireless Protocols'],
Industrial: ['Industrial Protocols'],
Automotive: ['Automotive Protocols'],
Network: ['Network Protocols / Socket Programming'],
'TCP/IP': ['Network Protocols / Socket Programming'],
UDP: ['Network Protocols / Socket Programming'],
Cellular: ['Cellular Communication'],
MQTT: ['CoAP & MQTT'],
CoAP: ['CoAP & MQTT'],
'LTE-M / 5G': ['LTE-M & NB-IoT'],
'NB-IoT': ['LTE-M & NB-IoT'],
'Basic Math & Calculus': ['Basic Calculus'],
'SDLC Models': ['Software Development Life Cycle (SDLC) Models'],
'Version Control': ['Version Control Systems'],
AUTOSAR: ['AUTOSAR Architecture'],
// Drawn on the map but without their own README section: open the protocol family instead.
Profinet: ['Industrial Protocols'],
LIN: ['Automotive Protocols'],
MOST: ['Automotive Protocols'],
FlexRay: ['Automotive Protocols'],
UWB: ['Wireless Protocols'],
};
export const AREA_ORDER = ['SOFTWARE', 'HARDWARE', 'SOFT SKILLS'];
export const IMPORTANCE_LEVELS = ['required', 'recommended', 'possible'];
const RANK = { required: 3, recommended: 2, possible: 1 };
@@ -60,13 +26,12 @@ function topicIndex(topics) {
export function linkDiagram(map, data) {
const index = topicIndex(data.topics);
const softSkills = data.topics.find((t) => t.depth === 1 && key(t.title) === 'soft skills');
const resolve = (text, areas = []) => {
if (ALIASES[text]) return ALIASES[text].map((title) => index.get(key(title))).filter(Boolean);
// map.json "readme": map labels whose README heading is worded differently (or has no section of its own).
const aliases = map.readme || {};
const resolve = (text) => {
if (Object.hasOwn(aliases, text)) return [aliases[text]].flat().map((title) => index.get(key(title))).filter(Boolean);
const topic = index.get(key(text));
if (topic) return [topic];
if (softSkills && areas.includes('SOFT SKILLS')) return [softSkills];
return [];
return topic ? [topic] : [];
};
// A cluster's areas are those of the band it sits in; a group's are the areas all its clusters share.
@@ -81,7 +46,7 @@ export function linkDiagram(map, data) {
for (const [text, importance] of spec.topics) boxes.push({ text, importance, areas: band.areas, cluster, header: false });
}
}
boxes.forEach((b, i) => Object.assign(b, { id: `box-${i}`, topics: resolve(b.text, b.areas) }));
boxes.forEach((b, i) => Object.assign(b, { id: `box-${i}`, topics: resolve(b.text) }));
const regions = Object.entries(map.groups).map(([name, clusters]) => {
const areas = AREA_ORDER.filter((a) => clusters.every((c) => (clusterAreas.get(c) || []).includes(a)));
return { name, clusters, areas, topics: resolve(name) };
@@ -106,8 +71,18 @@ export function linkDiagram(map, data) {
for (const area of region.areas) if (!t.areas.includes(area)) t.areas.push(area);
}
}
// Subtopics that are not drawn in the diagram inherit the areas of their closest drawn ancestor.
// Subtopics that are not drawn in the diagram inherit the areas of their closest drawn ancestor;
// a topic that is not drawn itself but has drawn subtopics (Soft Skills) takes theirs.
const fromChildren = new Map();
for (const t of data.topics) {
if (!t.areas.length) continue;
for (let p = t.parent; p; p = p.parent) {
if (!fromChildren.has(p)) fromChildren.set(p, new Set());
for (const area of t.areas) fromChildren.get(p).add(area);
}
}
for (const t of data.topics) {
if (!t.areas.length && fromChildren.has(t)) t.areas = [...fromChildren.get(t)];
if (!t.areas.length) for (let p = t.parent; p && !t.areas.length; p = p.parent) t.areas = [...(p.areas || [])];
t.areas.sort((a, b) => AREA_ORDER.indexOf(a) - AREA_ORDER.indexOf(b));
}
@@ -118,15 +93,41 @@ export function linkDiagram(map, data) {
return null;
};
// A connection joins two boxes. `reason` is set for the hand-picked ones in map.json; the rest
// are evidence only (a shared resource, or one topic's text naming another).
const pairs = new Map();
const pairFor = (a, b) => {
const [p, q] = a.id < b.id ? [a, b] : [b, a];
const id = `${p.id}|${q.id}`;
if (!pairs.has(id)) pairs.set(id, { a: p, b: q, reason: null, weight: 0, shared: 0, links: [] });
return pairs.get(id);
};
const topicBoxes = new Map(boxes.filter((b) => !b.header).map((b) => [b.text, b]));
const invalidLinks = [];
for (const box of boxes) box.related = [];
for (const [from, to, reason] of map.links || []) {
const a = topicBoxes.get(from);
const b = topicBoxes.get(to);
if (!a || !b || a === b || typeof reason !== 'string' || !reason.trim()) {
invalidLinks.push(`${from} - ${to}`);
continue;
}
const pair = pairFor(a, b);
if (pair.reason) {
invalidLinks.push(`${from} - ${to} (listed twice)`);
continue;
}
pair.reason = reason.trim();
a.related.push({ box: b, reason: pair.reason });
b.related.push({ box: a, reason: pair.reason });
}
for (const link of data.links) {
const a = boxFor(link.source);
const b = boxFor(link.target);
if (!a || !b || a === b) continue;
const [p, q] = a.id < b.id ? [a, b] : [b, a];
const id = `${p.id}|${q.id}`;
if (!pairs.has(id)) pairs.set(id, { a: p, b: q, weight: 0, shared: 0, links: [] });
const pair = pairs.get(id);
const pair = pairFor(a, b);
pair.weight += link.weight;
pair.shared += link.shared.length;
pair.links.push(link);
@@ -139,6 +140,11 @@ export function linkDiagram(map, data) {
boxFor,
connections: [...pairs.values()],
unmatched: boxes.filter((b) => !b.header && !b.topics.length).map((b) => b.text),
invalid: boxes.filter((b) => !b.header && !IMPORTANCE_LEVELS.includes(b.importance)).map((b) => `${b.text}: ${b.importance}`),
invalidLinks,
invalidReadme: Object.entries(aliases).flatMap(([label, titles]) => {
const onMap = boxes.some((b) => b.text === label) || label in map.groups;
const missing = [titles].flat().filter((title) => !index.has(key(title)));
return onMap && !missing.length ? [] : [`${label} -> ${[titles].flat().join(' + ')}`];
}), invalid: boxes.filter((b) => !b.header && !IMPORTANCE_LEVELS.includes(b.importance)).map((b) => `${b.text}: ${b.importance}`),
};
}
+2 -1
View File
@@ -29,6 +29,7 @@ const spanW = (n) => n * COL + (n - 1) * GUTTER;
export const PLACED = [
'languages', 'fundamentals', 'debugging', 'sdlc', 'vcs', 'build', 'testing', 'specialized', 'osBase', 'linux',
'rtos', 'mcu', 'bridge', 'basic', 'wireless', 'highSpeed', 'industrial', 'cellular', 'network', 'automotive',
'display',
'electronics', 'equipment', 'prototyping', 'fpga', 'soft',
];
@@ -106,7 +107,7 @@ export function buildLayout(plan) {
const ifBottom = Math.max(
stack(['basic', 'wireless'], sub[0], ifContent, subW),
stack(['highSpeed', 'industrial', 'cellular'], sub[1], ifContent, subW),
stack(['network', 'automotive'], sub[2], ifContent, subW),
stack(['network', 'automotive', 'display'], sub[2], ifContent, subW),
);
const interfaces = { x: c[2] - SPACE, y: groupTop, w: wide + 2 * SPACE, h: ifBottom + SPACE - groupTop };
const bridgeBottom = add(place(clusters.bridge, c[4], groupTop, COL));
+42 -22
View File
@@ -21,12 +21,9 @@
.start-strip,
.map-toolbar {
display: flex;
flex-wrap: nowrap;
flex-wrap: wrap;
align-items: center;
gap: 6px;
overflow-x: auto;
scrollbar-width: thin;
padding-bottom: 2px;
}
.start-strip > *,
@@ -278,14 +275,19 @@
stroke-width: 2.2;
}
.box.header.target:hover .header-text,
.box.header.lit .header-text,
.group-tag.target:hover .tag-text {
text-decoration: underline;
.header-bg {
fill: transparent;
transition: fill 0.15s;
}
.box.header.target:hover .header-bg,
.box.header.selected .header-bg {
fill: color-mix(in srgb, var(--accent) 14%, transparent);
}
.group-tag.target:hover .tag-bg {
fill: var(--chip-hover);
fill: color-mix(in srgb, var(--surface-solid) 85%, var(--accent));
stroke-width: 3;
}
.box.selected .box-bg {
@@ -293,19 +295,15 @@
stroke-width: 3;
}
.box.header.selected .header-text {
fill: var(--accent);
}
.box.target:focus-visible .box-bg,
.group-tag.target:focus-visible .tag-bg {
stroke: var(--accent);
stroke-width: 3.5;
}
.box.header.target:focus-visible .header-text {
fill: var(--accent);
text-decoration: underline;
.box.header.target:focus-visible .header-bg {
stroke: var(--accent);
stroke-width: 2;
}
.layer-boxes .box {
@@ -328,15 +326,25 @@
pointer-events: none;
}
.link.mention,
.link-all.mention {
.link.evidence,
.link-all.evidence {
stroke-dasharray: 5 5;
stroke-opacity: 0.45;
}
.link-end {
fill: var(--accent);
}
.link-all {
stroke-opacity: 0.4;
}
.link-all.curated {
stroke-width: 1.2px;
stroke-opacity: 0.28;
}
.layer-linksAll {
opacity: 0;
transition: opacity 0.25s;
@@ -350,10 +358,6 @@
opacity: 0.2;
}
.link-end {
fill: var(--accent);
}
.credit-main {
fill: var(--text);
font-size: 14px;
@@ -411,11 +415,19 @@
padding: 8px 10px;
}
/* One swipeable row per strip; the fade at the right edge shows there is more. */
.start-strip,
.map-toolbar {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
padding-right: 28px;
mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
}
.start-strip::-webkit-scrollbar,
.map-toolbar::-webkit-scrollbar {
display: none;
}
.start-chip,
@@ -439,6 +451,14 @@
}
@media print {
/* Print the final colors, not a frame of the screen-to-print color transition. */
*,
*::before,
*::after {
transition: none !important;
animation: none !important;
}
html[data-theme] {
color-scheme: light;
--bg: #ffffff;
+254 -1
View File
@@ -198,6 +198,14 @@
["MOST", "possible"],
["FlexRay", "possible"]
]
},
"display": {
"header": "Display & Camera",
"per": 2,
"topics": [
["MIPI CSI-2", "possible"],
["MIPI DSI", "possible"]
]
}
}
},
@@ -258,9 +266,254 @@
}
}
],
"links": [
["C", "Memory Management", "Pointers, the stack and the heap are managed by hand in C"],
["C++", "Memory Management", "Ownership, RAII and smart pointers decide who frees memory"],
["Rust", "Memory Management", "Ownership and borrowing replace manual memory management"],
["Python", "Memory Management", "Garbage collection; MicroPython has to fit in small RAM"],
["C", "C++", "C++ builds on C, and both are mixed in one firmware project"],
["C", "Compilers / GCC", "The toolchain that turns C code into firmware"],
["C", "Linux Kernel", "The kernel and its drivers are written in C"],
["C", "GDB", "Source-level debugging of C programs"],
["C++", "Design Patterns", "Classic object-oriented patterns are taught in C++"],
["C++", "Qt Framework", "Qt is a C++ framework"],
["Python", "Bash Scripting", "Both automate tooling, tests and build tasks"],
["Python", "SIL / HIL Testing", "Test rigs and HIL benches are often scripted in Python"],
["Python", "Edge AI", "Models are usually trained in Python before deployment"],
["Rust", "Embedded Security", "Memory safety removes a whole class of vulnerabilities"],
["Assembly", "Computer Architecture", "Instructions map directly to registers and the memory model"],
["Assembly", "Compilers / GCC", "Reading compiler output and writing startup code"],
["Assembly", "Interrupts", "Vector tables and context saving are done in assembly"],
["Algorithms & Data Structures", "Memory Management", "The data structure you pick decides memory use and allocation"],
["Algorithms & Data Structures", "Digital Signal Processing", "Filters and transforms are algorithms with tight time and memory budgets"],
["Algorithms & Data Structures", "Problem-Solving Skills & Critical Thinking", "Choosing and breaking down an approach"],
["Design Patterns", "State Machines", "The State pattern; many embedded designs are state machines"],
["State Machines", "Interrupts", "Events and interrupts drive state transitions"],
["State Machines", "RTOS Basics", "RTOS tasks are usually written as event-driven state machines"],
["State Machines", "Digital Design", "Finite-state machines are also the core of sequential logic in hardware"],
["JTAG / SWD", "OpenOCD", "OpenOCD drives JTAG/SWD debug probes"],
["JTAG / SWD", "GDB", "GDB reaches the target through a JTAG/SWD probe"],
["JTAG / SWD", "Bootloader / DFU", "Flashes the first bootloader and recovers bricked boards"],
["OpenOCD", "GDB", "OpenOCD is the GDB server for most microcontrollers"],
["GDB", "Compilers / GCC", "GDB reads the debug information the compiler emits"],
["GDB", "Problem-Solving Skills & Critical Thinking", "Debugging is structured problem solving"],
["Agile / SCRUM", "V-Model", "Two common process models; safety projects often combine them"],
["Agile / SCRUM", "CI/CD Pipelines", "Short iterations rely on automated builds and tests"],
["Agile / SCRUM", "Teamwork & Collaborative Abilities", "Scrum is built around close teamwork"],
["Agile / SCRUM", "Organizational & Time Management Skills", "Sprint planning and prioritising work"],
["V-Model", "Standards & Certifications", "Certified projects follow V-model verification"],
["V-Model", "SIL / HIL Testing", "Integration and system testing on SIL and HIL benches"],
["Git", "SVN", "Both are version control systems; Git is the common choice today"],
["Git", "CI/CD Pipelines", "Pipelines start from commits and pull requests"],
["Git", "Teamwork & Collaborative Abilities", "Branches and code review are how teams share code"],
["Communication Skills", "Teamwork & Collaborative Abilities", "Collaboration depends on clear communication"],
["Compilers / GCC", "Make / CMake", "Build systems drive the compiler and linker"],
["Make / CMake", "CI/CD Pipelines", "Pipelines run the same build commands"],
["Make / CMake", "Buildroot / Yocto", "Buildroot and Yocto are build systems for whole Linux images"],
["Bash Scripting", "CI/CD Pipelines", "Most pipeline steps are shell commands"],
["Bash Scripting", "Operating System Fundamentals", "The shell and processes are core OS concepts"],
["Docker", "CI/CD Pipelines", "Containers give pipelines a reproducible toolchain"],
["Docker", "Buildroot / Yocto", "Yocto builds are commonly run in containers"],
["TDD & Unit Testing", "CI/CD Pipelines", "Tests run automatically on every change"],
["TDD & Unit Testing", "Hardware Simulation / Emulation", "Tests run on a host against simulated hardware"],
["SIL / HIL Testing", "Hardware Simulation / Emulation", "SIL runs the firmware against simulated hardware"],
["SIL / HIL Testing", "CI/CD Pipelines", "HIL rigs can run as pipeline stages"],
["Standards & Certifications", "Embedded Security", "Security standards such as IEC 62443"],
["Standards & Certifications", "AUTOSAR", "Automotive software is developed to ISO 26262"],
["Standards & Certifications", "PCB Design / EMC", "EMC compliance testing is a certification requirement"],
["Embedded Security", "Bootloader / DFU", "Secure boot and signed firmware updates"],
["Embedded Security", "IoT", "Connected devices are exposed to attacks and need secure updates"],
["Embedded Security", "TCP/IP", "TLS runs on top of TCP/IP"],
["Embedded GUI", "Qt Framework", "Qt is a common GUI toolkit on embedded Linux"],
["Embedded GUI", "SPI", "Small displays are often driven over SPI"],
["Embedded GUI", "DMA", "DMA moves frame buffers to the display"],
["IoT", "MQTT", "A common messaging protocol for IoT devices"],
["IoT", "CoAP", "A lightweight request/response protocol for constrained devices"],
["IoT", "Wi-Fi", "A typical uplink for mains-powered devices"],
["IoT", "LoRa", "Long-range, low-power links"],
["IoT", "NB-IoT", "Cellular connectivity for low-power devices"],
["IoT", "Power Management", "Battery-powered nodes need low-power design"],
["Edge AI", "Digital Signal Processing", "Features are extracted from sensor signals before inference"],
["Edge AI", "Sensors & Actuators", "Models run on sensor data"],
["Edge AI", "Power Management", "Inference has to fit a small energy budget"],
["Edge AI", "FPGA Development", "FPGAs are used as inference accelerators"],
["AUTOSAR", "CAN", "AUTOSAR communication stacks run over CAN"],
["AUTOSAR", "Ethernet", "Automotive Ethernet is part of the AUTOSAR stack"],
["AUTOSAR", "RTOS Basics", "AUTOSAR OS is an OSEK-based real-time operating system"],
["Operating System Fundamentals", "RTOS Basics", "Scheduling, tasks and synchronisation carry over"],
["Operating System Fundamentals", "Linux Kernel", "Processes, memory and drivers as implemented in Linux"],
["Operating System Fundamentals", "Threading / Parallelism", "Threads and scheduling basics"],
["Operating System Fundamentals", "Memory Management", "Virtual memory and allocators"],
["Operating System Fundamentals", "Computer Architecture", "MMU, privilege levels and caches"],
["Linux Kernel", "Device Drivers", "Drivers are kernel modules"],
["Linux Kernel", "U-Boot", "U-Boot loads the kernel and the device tree"],
["Linux Kernel", "Buildroot / Yocto", "The kernel is configured and built as part of the image"],
["Linux Kernel", "Memory Technologies & File Systems", "Flash-friendly file systems and block layers"],
["Device Drivers", "I2C", "Linux drivers talk to devices over buses like I2C"],
["Device Drivers", "SPI", "Linux drivers talk to devices over buses like SPI"],
["Device Drivers", "GPIO", "Pins are exposed to drivers through the GPIO subsystem"],
["Device Drivers", "Interrupts", "Drivers handle device interrupts"],
["Device Drivers", "USB", "The USB stack needs host and device drivers"],
["Device Drivers", "PCIe", "PCIe devices need kernel drivers"],
["U-Boot", "Bootloader / DFU", "The same job on Linux boards: initialise hardware and load the OS"],
["U-Boot", "Buildroot / Yocto", "Both build and ship the bootloader with the image"],
["Threading / Parallelism", "IPC", "Threads and processes exchange data through IPC"],
["Threading / Parallelism", "RTOS Basics", "RTOS tasks, mutexes and priorities"],
["IPC", "RTOS Basics", "Queues and semaphores are the RTOS form of IPC"],
["RTOS Basics", "FreeRTOS", "The most common first RTOS"],
["RTOS Basics", "Zephyr", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "QNX", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "µC/OS", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "RT-Thread", "The same scheduling, synchronisation and timing concepts"],
["RTOS Basics", "Interrupts", "ISRs hand work over to tasks"],
["RTOS Basics", "Timers / Counters", "The tick timer and software timers"],
["FreeRTOS", "Memory Management", "FreeRTOS offers several heap allocation schemes"],
["FreeRTOS", "TCP/IP", "FreeRTOS+TCP is its TCP/IP stack"],
["Zephyr", "Bluetooth", "Zephyr ships its own Bluetooth stack"],
["Zephyr", "Thread", "Zephyr integrates OpenThread"],
["QNX", "IPC", "A microkernel built around message passing"],
["QNX", "Standards & Certifications", "Certified for safety-critical use"],
["GPIO", "Interrupts", "Pin edges raise interrupts"],
["GPIO", "PWM", "PWM is a timer-driven GPIO function"],
["GPIO", "Principles of Electric Circuits", "Pull-ups, drive strength and current limits"],
["GPIO", "Breadboarding", "The first experiment: an LED and a button on a breadboard"],
["ADC / DAC", "Sensors & Actuators", "Analog sensors are read through an ADC"],
["ADC / DAC", "DMA", "DMA moves samples without using the CPU"],
["ADC / DAC", "Digital Signal Processing", "Sampling theory and filtering"],
["ADC / DAC", "Timers / Counters", "Timers trigger conversions at a fixed rate"],
["Timers / Counters", "PWM", "PWM is generated by timer hardware"],
["Timers / Counters", "Interrupts", "Timer events raise interrupts"],
["Timers / Counters", "Clock Management", "Timer rates derive from the clock tree"],
["Timers / Counters", "Watchdog", "A watchdog is a special timer"],
["PWM", "Sensors & Actuators", "Drives motors, LEDs and servos"],
["PWM", "Control Theory", "A PID loop outputs a PWM duty cycle"],
["PWM", "Oscilloscope", "Duty cycle and frequency are checked on a scope"],
["Watchdog", "RTOS Basics", "Tasks must check in; a stuck task triggers a reset"],
["Interrupts", "Computer Architecture", "Vector table, priorities and the exception model"],
["DMA", "SPI", "High-throughput transfers without CPU load"],
["DMA", "UART", "Streaming serial data without CPU load"],
["Clock Management", "Power Management", "Clock gating and frequency scaling save power"],
["Clock Management", "UART", "Baud rates derive from the clock"],
["Clock Management", "Oscilloscope", "Checking oscillator and clock output"],
["Power Management", "Interrupts", "Wake-up sources are interrupts"],
["Power Management", "Bluetooth", "BLE is designed around duty cycling"],
["Power Management", "Hardware Design Basics", "Power supply and battery design"],
["Bootloader / DFU", "Memory Technologies & File Systems", "Firmware lives in flash and updates need safe writes"],
["Bootloader / DFU", "USB", "The USB DFU class updates firmware"],
["Memory Technologies & File Systems", "Memory Management", "RAM, flash and EEPROM sizes set the memory budget"],
["Memory Technologies & File Systems", "SPI", "SPI flash and SD cards"],
["Hardware Simulation / Emulation", "Digital Design", "Logic is verified in simulation before hardware exists"],
["Hardware Simulation / Emulation", "FPGA Development", "HDL designs are simulated before synthesis"],
["Hardware Simulation / Emulation", "Principles of Electric Circuits", "Circuits can be simulated (SPICE) before they are built"],
["Sensors & Actuators", "I2C", "Most digital sensors use I2C"],
["Sensors & Actuators", "SPI", "Faster sensors use SPI"],
["Sensors & Actuators", "Digital Signal Processing", "Raw readings are filtered and calibrated"],
["Sensors & Actuators", "Control Theory", "Closed-loop control reads sensors and drives actuators"],
["Digital Signal Processing", "Control Theory", "Filters and controllers share the same math"],
["Digital Signal Processing", "Basic Math & Calculus", "Transforms and filters are applied calculus"],
["Control Theory", "Basic Math & Calculus", "Differential equations and transfer functions"],
["UART", "Logic / Protocol Analyzer", "Inspect serial frames and baud rate"],
["UART", "USB", "USB-to-UART bridges are everywhere"],
["UART", "GSM / LTE", "Modems are controlled with AT commands over UART"],
["I2C", "Logic / Protocol Analyzer", "Decode addresses and ACK/NACK"],
["I2C", "SPI", "Both are on-board buses; learn when to choose which"],
["I2C", "Hardware Design Basics", "Pull-up sizing and bus capacitance"],
["SPI", "Logic / Protocol Analyzer", "Decode the clock, chip select and data lines"],
["Bluetooth", "Wi-Fi", "Both share the 2.4 GHz band, often on one radio chip"],
["Bluetooth", "Matter", "Matter devices are commissioned over Bluetooth LE"],
["Bluetooth", "UWB", "UWB is often paired with Bluetooth for secure ranging"],
["Wi-Fi", "TCP/IP", "Wi-Fi carries IP traffic"],
["Wi-Fi", "Matter", "Matter runs on Wi-Fi and Thread"],
["Thread", "Matter", "Matter runs on Wi-Fi and Thread"],
["Thread", "Zigbee", "Both are mesh networks on IEEE 802.15.4 radios"],
["LoRa", "NB-IoT", "Competing low-power wide-area options"],
["Ethernet", "TCP/IP", "Ethernet is the link layer under TCP/IP"],
["Ethernet", "EtherCAT", "Industrial protocols that run on Ethernet hardware"],
["Ethernet", "Profinet", "Industrial protocols that run on Ethernet hardware"],
["USB", "PCIe", "Both are high-speed serial links with host and device roles"],
["PCIe", "FPGA Development", "FPGAs often connect to hosts over PCIe"],
["Modbus", "UART", "Modbus RTU runs over serial lines such as RS-485"],
["Modbus", "TCP/IP", "Modbus TCP runs over TCP/IP"],
["EtherCAT", "FPGA Development", "EtherCAT slave controllers are often built in FPGAs"],
["GSM / LTE", "LTE-M / 5G", "The same cellular family, newer generations"],
["LTE-M / 5G", "NB-IoT", "Both are LTE-based low-power IoT standards"],
["MQTT", "TCP/IP", "MQTT runs over TCP"],
["MQTT", "CoAP", "Two IoT messaging styles: broker-based and request/response"],
["CoAP", "UDP", "CoAP runs over UDP"],
["TCP/IP", "UDP", "The two main transport protocols"],
["CAN", "LIN", "LIN is the low-cost sub-bus beneath CAN in vehicles"],
["CAN", "FlexRay", "FlexRay adds determinism and bandwidth beyond CAN"],
["CAN", "Logic / Protocol Analyzer", "Decode frames and arbitration"],
["CAN", "Oscilloscope", "Check the differential signal and bus termination"],
["MOST", "CAN", "MOST carries media in a vehicle while CAN carries control"],
["MOST", "Ethernet", "Automotive Ethernet is replacing MOST"],
["MIPI CSI-2", "MIPI DSI", "Camera and display links share the MIPI D-PHY physical layer"],
["MIPI CSI-2", "I2C", "Camera sensors are configured over a control bus such as I2C"],
["MIPI CSI-2", "Device Drivers", "Linux exposes camera sensors through V4L2 drivers"],
["MIPI CSI-2", "Edge AI", "Camera frames are the main input for on-device vision models"],
["MIPI DSI", "Device Drivers", "Linux drives DSI panels through the DRM/KMS subsystem"],
["MIPI DSI", "Embedded GUI", "The display link carries the interface the GUI renders"],
["MIPI DSI", "DMA", "Frame buffers are streamed to the display with DMA"],
["Basic Math & Calculus", "Principles of Electric Circuits", "Circuit analysis uses algebra and calculus"],
["Principles of Electric Circuits", "Electronics Fundamentals", "Components build on circuit laws"],
["Principles of Electric Circuits", "Multimeter", "Verify voltages and currents in real circuits"],
["Electronics Fundamentals", "Digital Design", "Logic gates are built from transistors"],
["Electronics Fundamentals", "Hardware Design Basics", "Choosing components and sizing circuits"],
["Electronics Fundamentals", "Oscilloscope", "Observe how signals change over time"],
["Digital Design", "Computer Architecture", "CPUs are built from digital logic"],
["Digital Design", "FPGA Development", "HDL describes digital logic"],
["Multimeter", "Breadboarding", "Continuity and voltage checks on prototypes"],
["Multimeter", "Soldering / Rework", "Checking for shorts after soldering"],
["Oscilloscope", "Logic / Protocol Analyzer", "Analog view and decoded digital view of the same signals"],
["Oscilloscope", "PCB Design / EMC", "Measuring noise and ringing"],
["Breadboarding", "Hardware Design Basics", "Prototype first, then design the board"],
["Breadboarding", "Soldering / Rework", "From breadboard to soldered prototype"],
["Hardware Design Basics", "PCB Design / EMC", "Schematics become board layout"],
["PCB Design / EMC", "Soldering / Rework", "Boards must allow assembly and repair"]
],
"groups": {
"Operating Systems": ["osBase", "linux", "rtos"],
"Microcontrollers": ["rtos", "mcu"],
"Interfaces & Protocols": ["basic", "wireless", "highSpeed", "industrial", "cellular", "network", "automotive"]
"Interfaces & Protocols": ["basic", "wireless", "highSpeed", "industrial", "cellular", "network", "automotive", "display"]
},
"offMap": ["Don't Know Where to Start!", "Appendix-A: Advanced Topics"],
"readme": {
"ADC / DAC": ["ADC", "DAC"],
"Buildroot / Yocto": ["Buildroot", "Yocto"],
"TDD & Unit Testing": ["Test Driven Development (TDD)", "Unit Testing"],
"Threading / Parallelism": ["Multithreading & Parallel Processing"],
"Device Drivers": ["Linux Device Drivers"],
"Real-Time OS": ["Real-Time Operating Systems"],
"Interfaces & Protocols": ["Interfaces, Protocols & Communication Technologies"],
"Basic": ["Basic Protocols"],
"High-Speed": ["High-Speed Protocols"],
"Wireless": ["Wireless Protocols"],
"Industrial": ["Industrial Protocols"],
"Automotive": ["Automotive Protocols"],
"Display & Camera": ["Display & Camera Protocols"],
"Network": ["Network Protocols / Socket Programming"],
"TCP/IP": ["Network Protocols / Socket Programming"],
"UDP": ["Network Protocols / Socket Programming"],
"Cellular": ["Cellular Communication"],
"MQTT": ["CoAP & MQTT"],
"CoAP": ["CoAP & MQTT"],
"LTE-M / 5G": ["LTE-M & NB-IoT"],
"NB-IoT": ["LTE-M & NB-IoT"],
"Basic Math & Calculus": ["Basic Calculus"],
"SDLC Models": ["Software Development Life Cycle (SDLC) Models"],
"Version Control": ["Version Control Systems"],
"AUTOSAR": ["AUTOSAR Architecture"]
}
}
+81 -14
View File
@@ -206,11 +206,12 @@ export function createMapView({ root, plan, data, build, onSelect }) {
};
creditLine(fy + 35, 'Creator & Maintainer: ', CREDITS.author);
if (build) creditLine(fy + 53, 'Revision: ', `${build.version} · Last update: ${build.date}`);
const logoSize = 52;
const badge = { w: 103, h: 36 };
svg('image', { href: 'assets/logo.svg', x: fr - logoSize, y: fy, width: logoSize, height: logoSize }, credits);
const logoSize = badge.h;
const iconY = fy + 9;
svg('image', { href: 'assets/logo.svg', x: fr - logoSize, y: iconY, width: logoSize, height: logoSize }, credits);
const badgeX = fr - logoSize - SPACE - badge.w;
svg('image', { href: 'assets/cc-by-sa.svg', x: badgeX, y: fy + (logoSize - badge.h) / 2, width: badge.w, height: badge.h }, link(CREDITS.licenseUrl));
svg('image', { href: 'assets/cc-by-sa.svg', x: badgeX, y: iconY, width: badge.w, height: badge.h }, link(CREDITS.licenseUrl));
const licenseText = svg('text', { x: badgeX - 14, y: fy + 22, 'text-anchor': 'end', class: 'credit-text' }, link(CREDITS.licenseUrl));
licenseText.textContent = 'This work is licensed under the Creative Commons';
const licenseText2 = svg('text', { x: badgeX - 14, y: fy + 40, 'text-anchor': 'end', class: 'credit-text' }, link(CREDITS.licenseUrl));
@@ -228,9 +229,10 @@ export function createMapView({ root, plan, data, build, onSelect }) {
const g = svg('g', { class: `box ${item.kind === 'header' ? 'header' : box.importance}`, 'data-box': box.id }, layers.boxes);
rectOf.set(box, item);
if (item.kind === 'header') {
const bg = svg('rect', { class: 'header-bg', rx: 6, x: item.x - 6, y: item.y - 1, height: 24 }, g);
const t = svg('text', { x: item.x, y: item.y + 16, class: 'box-text header-text' }, g);
t.textContent = item.text;
boxEls.set(box, { g, text: t, width: item.w, header: true });
boxEls.set(box, { g, text: t, bg, x: item.x, width: item.w, header: true });
} else {
rect(item, { rx: 5, class: 'box-bg' }, g);
const lines = item.h >= 36 ? splitInTwo(item.text) : [item.text];
@@ -290,11 +292,11 @@ export function createMapView({ root, plan, data, build, onSelect }) {
}
const importance = (box && box.importance) || topic.importance;
tip.replaceChildren(...[
h('strong', {}, topic.title),
h('strong', {}, box ? box.text : topic.title),
topic.areas.length ? h('div', { class: 'tip-areas' }, areaDots(topic.areas), areaLabel(topic.areas)) : null,
h('div', { class: 'tip-meta' }, [importance ? IMPORTANCE[importance] : null, plural(topic.total, 'resource'),
topic.links && topic.links.length ? `${topic.links.length} connected` : null].filter(Boolean).join(' · ')),
h('div', { class: 'tip-hint' }, 'Click for details and resources'),
box && box.related.length ? `${box.related.length} related` : null].filter(Boolean).join(' · ')),
h('div', { class: 'tip-hint' }, box && box.text !== topic.title ? `Click to open “${topic.title}”` : 'Click this topic to open its details'),
].filter(Boolean));
tip.hidden = false;
moveTip(e);
@@ -325,8 +327,8 @@ export function createMapView({ root, plan, data, build, onSelect }) {
function drawConnection(connection, layer, cls) {
svg('path', {
d: curve(connection.a, connection.b),
class: `${cls} ${connection.shared ? 'shared' : 'mention'}`,
'stroke-width': (1.4 + Math.min(3, Math.sqrt(connection.weight) * 0.7)).toFixed(2),
class: `${cls} ${connection.reason ? 'curated' : 'evidence'}`,
'stroke-width': connection.reason ? 2.4 : 1.4,
}, layer);
if (cls !== 'link') return;
for (const box of [connection.a, connection.b]) {
@@ -335,7 +337,8 @@ export function createMapView({ root, plan, data, build, onSelect }) {
}
}
for (const connection of connections) drawConnection(connection, layers.linksAll, 'link-all');
// The overview of all connections only shows the hand-picked ones.
for (const connection of connections) if (connection.reason) drawConnection(connection, layers.linksAll, 'link-all');
const isWithin = (node, topic) => {
for (let n = node; n; n = n.parent) if (n === topic) return true;
@@ -353,7 +356,7 @@ export function createMapView({ root, plan, data, build, onSelect }) {
for (const b of own) lit.add(b);
for (const connection of connections) {
if (!(own.has(connection.a) || own.has(connection.b))) continue;
if (!connection.links.some((l) => isWithin(l.source, topic) || isWithin(l.target, topic))) continue;
if (!connection.reason && !connection.links.some((l) => isWithin(l.source, topic) || isWithin(l.target, topic))) continue;
drawConnection(connection, layers.links, 'link');
lit.add(connection.a);
lit.add(connection.b);
@@ -423,10 +426,11 @@ export function createMapView({ root, plan, data, build, onSelect }) {
fitButton.title = 'Show the whole page';
}
const pageFitWidth = (size) => Math.max(page.w, (page.h + 40) / (size ? size.h / size.w : page.h / page.w)) + 40;
function fitPage() {
const size = canvasSize();
const aspect = size ? size.h / size.w : page.h / page.w;
const width = Math.max(page.w, (page.h + 40) / aspect) + 40;
const width = pageFitWidth(size);
fitMode = 'page';
setView({ x: (page.w - width) / 2, y: 0, w: width });
fitButton.title = 'Fit the width';
@@ -446,12 +450,49 @@ export function createMapView({ root, plan, data, build, onSelect }) {
const size = canvasSize();
if (!size) return;
const scale = (size.w / v.w) * factor;
if (scale < (size.w / page.w) * MIN_ZOOM || scale > MAX_ZOOM) return;
// Limits only stop a step that goes further out of range; the whole-page view is always reachable.
const minScale = Math.min((size.w / page.w) * MIN_ZOOM, size.w / pageFitWidth(size), size.w / v.w);
if (factor > 1 ? scale > MAX_ZOOM : scale < minScale) return;
const [px, py] = clientX == null ? [v.x + v.w / 2, v.y + v.h / 2] : toChart(clientX, clientY);
fitMode = 'custom';
setView({ x: px - (px - v.x) / factor, y: py - (py - v.y) / factor, w: v.w / factor });
}
// Two fingers: zoom around their midpoint and pan with it.
const touches = new Map();
let pinch = null;
const spread = () => {
const [a, b] = [...touches.values()];
return { dist: Math.hypot(a.x - b.x, a.y - b.y), x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
};
function beginPinch() {
pinch = spread();
// The click that ends the gesture must not open a topic.
state.drag = { moved: true, id: null };
chart.classList.add('panning');
hover(null);
}
function movePinch() {
if (touches.size < 2) return;
const now = spread();
if (pinch.dist > 0 && now.dist > 0) zoomAt(now.dist / pinch.dist, now.x, now.y);
const unit = state.view.w / chart.getBoundingClientRect().width;
fitMode = 'custom';
setView({ ...state.view, x: state.view.x - (now.x - pinch.x) * unit, y: state.view.y - (now.y - pinch.y) * unit });
pinch = now;
}
function endTouch(e) {
if (!touches.delete(e.pointerId)) return;
if (pinch && touches.size < 2) {
pinch = null;
chart.classList.remove('panning');
}
if (!touches.size && state.drag && state.drag.id === null) window.setTimeout(() => { state.drag = null; }, 0);
}
chart.addEventListener('wheel', (e) => {
e.preventDefault();
if (e.ctrlKey || e.metaKey) {
@@ -466,10 +507,20 @@ export function createMapView({ root, plan, data, build, onSelect }) {
setView({ ...state.view, x: state.view.x + dx * unit, y: state.view.y + dy * unit });
}, { passive: false });
chart.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'touch') touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (touches.size === 2) {
beginPinch();
return;
}
if (e.button !== 0) return;
state.drag = { x: e.clientX, y: e.clientY, view: { ...state.view }, moved: false, id: e.pointerId };
});
chart.addEventListener('pointermove', (e) => {
if (touches.has(e.pointerId)) touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (pinch) {
movePinch();
return;
}
const drag = state.drag;
if (!drag || drag.id !== e.pointerId) return;
const dx = e.clientX - drag.x;
@@ -485,6 +536,7 @@ export function createMapView({ root, plan, data, build, onSelect }) {
setView({ ...drag.view, x: drag.view.x - dx * scale, y: drag.view.y - dy * scale });
});
const endDrag = (e) => {
endTouch(e);
if (!state.drag || state.drag.id !== e.pointerId) return;
chart.classList.remove('panning');
// The click that follows a drag still needs to see `moved`.
@@ -494,6 +546,20 @@ export function createMapView({ root, plan, data, build, onSelect }) {
chart.addEventListener('pointercancel', endDrag);
chart.addEventListener('dblclick', toggleFit);
// Safari on a Mac sends its own gesture events for trackpad pinches (other browsers send Ctrl + wheel).
// On iOS the same pinch also arrives as touches, which are handled above, so skip it there.
let gestureScale = 1;
chart.addEventListener('gesturestart', (e) => {
e.preventDefault();
gestureScale = 1;
});
chart.addEventListener('gesturechange', (e) => {
e.preventDefault();
if (touches.size) return;
zoomAt(e.scale / gestureScale, e.clientX, e.clientY);
gestureScale = e.scale;
});
const zoomLevel = h('span', { class: 'zoom-level', 'aria-live': 'polite' }, '100%');
const fitButton = h('button', { type: 'button', class: 'icon-btn', 'aria-label': 'Toggle between fitting the width and the whole page', title: 'Show the whole page', onclick: toggleFit }, '⤢');
const zoomControls = h('div', { class: 'zoom-controls' },
@@ -534,6 +600,7 @@ export function createMapView({ root, plan, data, build, onSelect }) {
span.setAttribute('lengthAdjust', 'spacingAndGlyphs');
}
}
if (el.bg) el.bg.setAttribute('width', (el.text.getComputedTextLength() + 12).toFixed(1));
}
}
+13 -1
View File
@@ -56,7 +56,8 @@ const map = JSON.parse(readFileSync(new URL('./map.json', import.meta.url), 'utf
test('places topics in the map areas and their cross-section', () => {
const plan = linkDiagram(map, data);
assert.deepEqual(plan.unmatched, [], 'every map topic matches a README heading (or an alias in diagram.js)');
assert.deepEqual(plan.unmatched, [], 'every map topic matches a README heading (or a "readme" entry in map.json)');
assert.deepEqual(plan.invalidReadme, [], 'map.json "readme" entries name a topic on the map and existing README headings');
assert.deepEqual(plan.invalid, [], 'importance is required, recommended or possible');
assert.deepEqual(find('GPIO').areas, ['SOFTWARE', 'HARDWARE']);
assert.deepEqual(find('I2C').areas, ['SOFTWARE', 'HARDWARE']);
@@ -71,6 +72,17 @@ test('places topics in the map areas and their cross-section', () => {
assert.equal(find('Projects').areas.length, 0, 'getting-started topics are not on the map');
});
test('every map topic is connected to related topics, each connection with a reason', () => {
const plan = linkDiagram(map, data);
assert.deepEqual(plan.invalidLinks, [], 'map.json "links" name existing topics, once, with a reason');
// Soft skills describe the engineer, not the technology, so some of them stand alone.
const alone = plan.boxes.filter((b) => !b.header && !b.areas.includes('SOFT SKILLS') && !b.related.length);
assert.deepEqual(alone.map((b) => b.text), [], 'topics without a connection');
const has = (a, b) => plan.connections.some((c) => c.reason && [c.a.text, c.b.text].sort().join('|') === [a, b].sort().join('|'));
assert.ok(has('C', 'Memory Management'));
assert.ok(has('Rust', 'Memory Management'));
});
test('lays the map out on one A4 page with a minimum spacing between boxes', () => {
const layout = buildLayout(linkDiagram(map, data));
assert.deepEqual(layout.missing, [], 'every cluster in map.json has a place in layout.js');
+7 -7
View File
@@ -409,7 +409,8 @@ button {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface-solid);
background: color-mix(in srgb, var(--surface-solid) 72%, transparent);
backdrop-filter: blur(3px);
box-shadow: var(--shadow);
font-size: 13px;
pointer-events: none;
@@ -426,10 +427,11 @@ button {
}
.tip-hint {
margin-top: 4px;
color: var(--accent);
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid var(--border);
color: var(--muted);
font-size: 12px;
font-weight: 600;
}
/* Outline view */
@@ -715,11 +717,9 @@ button {
.related .why {
grid-column: 2;
overflow: hidden;
color: var(--muted);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.res-head {
+20
View File
@@ -0,0 +1,20 @@
// Warns (never fails) about README sections that have no place on the map yet.
// Usage: node site/tools/coverage.mjs Sections listed in map.json "offMap" (titles or group names) are skipped.
import { readFileSync } from 'node:fs';
import { parseRoadmap } from '../parser.js';
import { linkDiagram } from '../diagram.js';
const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8');
const map = JSON.parse(read('../map.json'));
const data = parseRoadmap(read('../../README.md'));
const plan = linkDiagram(map, data);
const drawn = new Set(plan.boxes.flatMap((b) => b.topics));
const onMap = (t) => drawn.has(t) || t.children.some(onMap);
const skipped = new Set(map.offMap || []);
const missing = data.root.children.filter((t) => !skipped.has(t.title) && !skipped.has(t.group) && !onMap(t));
for (const t of missing) {
console.log(`::warning file=README.md,title=Not on the roadmap map::"${t.title}" has no box on the map. Add it to site/map.json, or to "offMap" if it should stay off the map (see CONTRIBUTING.md).`);
}
console.log(missing.length ? `${missing.length} README section(s) are not on the map.` : 'Every README section is on the map or listed in "offMap".');