mirror of
https://github.com/m3y54m/Embedded-Engineering-Roadmap.git
synced 2026-10-12 01:08:13 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6dec3d394 | ||
|
|
da88157952 | ||
|
|
3150cc358d | ||
|
|
b08f3a9699 | ||
|
|
8cf40b4993 | ||
|
|
95aff195b4 | ||
|
|
2c4faffbd8 | ||
|
|
558cc859f5 | ||
|
|
71adde583f | ||
|
|
334e7d0456 |
@@ -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
|
||||
|
||||
+10
-1
@@ -32,7 +32,16 @@ Contributions from everyone are welcomed. To keep the roadmap practical, accessi
|
||||
- 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`.
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ used in an aircraft or rapid transit system.
|
||||
|
||||
## 🗺️ The Roadmap
|
||||
|
||||

|
||||
🌐 **[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.
|
||||
|
||||
[](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)
|
||||
|
||||
+22
-43
@@ -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));
|
||||
}
|
||||
@@ -166,6 +141,10 @@ export function linkDiagram(map, data) {
|
||||
connections: [...pairs.values()],
|
||||
unmatched: boxes.filter((b) => !b.header && !b.topics.length).map((b) => b.text),
|
||||
invalidLinks,
|
||||
invalid: boxes.filter((b) => !b.header && !IMPORTANCE_LEVELS.includes(b.importance)).map((b) => `${b.text}: ${b.importance}`),
|
||||
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
@@ -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));
|
||||
|
||||
+21
-12
@@ -275,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 {
|
||||
@@ -290,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 {
|
||||
@@ -450,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;
|
||||
|
||||
+44
-1
@@ -198,6 +198,14 @@
|
||||
["MOST", "possible"],
|
||||
["FlexRay", "possible"]
|
||||
]
|
||||
},
|
||||
"display": {
|
||||
"header": "Display & Camera",
|
||||
"per": 2,
|
||||
"topics": [
|
||||
["MIPI CSI-2", "possible"],
|
||||
["MIPI DSI", "possible"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -450,6 +458,13 @@
|
||||
["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"],
|
||||
@@ -471,6 +486,34 @@
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
|
||||
+65
-3
@@ -229,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];
|
||||
@@ -291,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'),
|
||||
box && box.related.length ? `${box.related.length} related` : null].filter(Boolean).join(' · ')),
|
||||
h('div', { class: 'tip-hint' }, 'Click this topic to open its details'),
|
||||
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);
|
||||
@@ -457,6 +458,41 @@ export function createMapView({ root, plan, data, build, onSelect }) {
|
||||
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) {
|
||||
@@ -471,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;
|
||||
@@ -490,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`.
|
||||
@@ -499,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' },
|
||||
@@ -539,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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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".');
|
||||
Reference in New Issue
Block a user